19

I tried an SSE (Server-Sent-Events) using java on tomcat 8.0. Here are few things I noticed.

I click a button that automatically makes a request to the servlet. Servlet's GET method gets executed which returns an event stream. Once the full stream is received, the page again automatically makes another request which receives the same data again!!! I don't have an infinite loop there!!!

  1. What is actually happening on the server? In normal scenarios, tomcat creates a thread to handle every request. What is happening now?

  2. What is the correct way to ensure that the event stream is sent only once to the same connection/browser session?

  3. What is the correct way to ensure that the event stream is closed and no resource overhead incurs on the server?

  4. How to differentiate between GET and POST requests. Why did it choose GET?

  5. Is it too early to use SSE on Tomcat? Any performance issues?

Here is the code for the curious,

@WebServlet("/TestServlet")
public class TestServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        //content type must be set to text/event-stream
        response.setContentType("text/event-stream"); 
        //cache must be set to no-cache
        response.setHeader("Cache-Control", "no-cache");     
        //encoding is set to UTF-8
        response.setCharacterEncoding("UTF-8");

        PrintWriter writer = response.getWriter();

        for(int i=0; i<10; i++) {
            System.out.println(i);
            writer.write("data: "+ i +"\n\n");
            writer.flush();
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        writer.close(); 
    }
}

Javascript on the page (I don't have anything else on the page),

<button onclick="start()">Start</button>

<script type="text/javascript">
    function start() {
        var eventSource = new EventSource("TestServlet");
        eventSource.onmessage = function(event) {
            console.log("data: "+event.data)
            document.getElementById('foo').innerHTML = event.data;
        };
    }
</script>

Tried this using CURL. And the response came just once. I'm using chrome, so this must be a issue with chorme??

EDIT:

What I have learned and learning is now documented in my blog - Server Sent Events

John Eipe
  • 10,922
  • 24
  • 72
  • 114
  • It might be your browser. try to send the initial request using curl and see if it still happens. – Nir Alfasi Aug 02 '15 at 03:46
  • Yes you are right. Curl stopped with one request. – John Eipe Aug 02 '15 at 03:55
  • I fell for the same one... at least I was able to save you the time & frustration ;) – Nir Alfasi Aug 02 '15 at 03:55
  • 1
    @John I don't think its problem with browser. I am using such think from last 6-8 months and it's working fine on production. Check with your script may be start() is getting called multiple times? – Amogh Aug 02 '15 at 06:00
  • @Amogh But how is that possible? I commented the code and put a console.log and it is getting called just once when I click the button. If you have been using this for months then you might have answers to my question. – John Eipe Aug 02 '15 at 13:22
  • @John, My first guess is, in servlet you should check for error using [checkError()](http://docs.oracle.com/javase/7/docs/api/java/io/PrintWriter.html#checkError()) method of writer. Sometime connection won't get close because of some exception and as per some docs writer class don't throw any exception. So I suggest you to use that method to check for errors, this method automatically calls flush method. hope this will help you. – Amogh Aug 02 '15 at 16:39

3 Answers3

23

Change this line

writer.write("data: "+ i +"\n\n");

to

writer.write("data: "+ i +"\r\n");

BTW, your code will have a serious performance issue because it will hold a thread until all events are sent.Please use Asynchronous processing API instead. e.g.

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    AsyncContext actx = req.startAsync();
    actx.setTimeout(30*1000);
    //save actx and use it when we need sent data to the client.
}

Then we can use AsyncContext later

//write some data to client when a certain event happens
actx.getResponse().getWriter().write("data: " + mydata + "\r\n");
actx.getResponse().getWriter().flush();

if all events sent we can close it

actx.complete();

UPDATE 1:

We need close the event source at browser if we do not want browser reconnect the server again when server completes the response.

eventSource.close();

Another method maybe helps, viz. we set a quite large retry time but I have not tried it, e.g.

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    AsyncContext actx = req.startAsync();
    actx.getResponse().getWriter().write("retry: 36000000000\r\n"); // 10000 hours!
    actx.getResponse().getWriter().flush();
    //save actx and use it when we need sent data to the client.
}

UPDATE 2:

I think Websocket maybe is better for your case.

UPDATE 3: (answer the questions)

  1. What is actually happening on the server? In normal scenarios, tomcat creates a thread to handle every request. What is happening now?

If use NIO connector which is default in Tomcat 8.0.X, within the whole processing cycle HTTP I/O about a request won't hold a thread. If use BIO a thread will be hold until the whole processing cycle completes. All threads are from a thread pool, tomcat won't create a thread for each request.

  1. What is the correct way to ensure that the event stream is sent only once to the same connection/browser session?

Do eventSource.close() at browser side is the best choice.

  1. What is the correct way to ensure that the event stream is closed and no resource overhead incurs on the server?

Do not forget to invoke AsyncContext.complete() at server side.

  1. How to differentiate between GET and POST requests. Why did it choose GET?

The EventSource API in a browser only supports GET requests but at the server side there 's no such restriction. SSE is mainly used to receive events data from server. If a event happens the browser can receive it in time and no need to create a new request to poll it. If you need full-duplex communication try WebSocket instread of SSE.

  1. Is it too early to use SSE on Tomcat? Any performance issues?

There should be no performance issues if we use NIO connector & Asynchronous processing API. I don't know whether Tomcat NIO connector is mature or not but something will never be known unless we try it.

xfeep
  • 1,091
  • 11
  • 12
  • Isn't closing a much better approach than the large retry time? Isn't it possible to send POST requests? – John Eipe Aug 05 '15 at 03:19
  • Closing is better. About GET or POST there's no restriction at server side.But according to the [W3C SPEC](http://www.w3.org/TR/eventsource/ ) browser will always use GET instead of POST and there 's no option to enable POST. – xfeep Aug 05 '15 at 07:25
  • Now that's an answer. Thanks. – John Eipe Aug 08 '15 at 05:26
  • According to the spec: "Clients will reconnect if the connection is closed; a client can be told to stop reconnecting using the HTTP 204 No Content response code." I've been working on Java impl of SSE by my self, you can check it here: https://github.com/metteo/event-source – MeTTeO Sep 05 '16 at 04:49
  • Spring Boot has Server-Sent Events when returning an SseEmitter on a Controller. – User1 Dec 20 '17 at 14:49
2

I highly recommend first to read Stream Updates with Server-Sent Events to get a good general understanding of the technology. Then follow Server-Sent Events with Async Servlet By Example to see how SSE can be used specifically with the Servlet technology.

01es
  • 5,362
  • 1
  • 31
  • 40
  • 1
    Wtf is that on the second link? Does the person who wrote this belive this code is readable by humans? If so and he works for Oracle he should be fired on the spot. – shinzou Oct 13 '16 at 17:26
  • 1
    @kuhaku Looks like something was changed in the underlying blog engine that affected the code formatting -- it was not like that before (: – 01es Oct 13 '16 at 22:55
  • 3
    Second link is broken, found it in the waybackmachine: https://web.archive.org/web/20150910123522/https://weblogs.java.net/blog/swchan2/archive/2014/05/21/server-sent-events-async-servlet-example – GriffoGoes Nov 15 '18 at 15:58
0

The browser attempts to reconnect to the source roughly 3 seconds after each connection is closed. You can change that timeout by including a line beginning with "retry:", followed by the number of milliseconds to wait before trying to reconnect.