How does Server-Sent-Events work

John picture John · Aug 2, 2015 · Viewed 24.5k times · Source

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

Answer

xfeep picture xfeep · Aug 3, 2015

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.