19
votes

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

3
It might be your browser. try to send the initial request using curl and see if it still happens.Nir Alfasi
Yes you are right. Curl stopped with one request.John
I fell for the same one... at least I was able to save you the time & frustration ;)Nir Alfasi
@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
@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

3 Answers

23
votes

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.

2
votes

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.

0
votes

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.