Search code examples
htmlservletsserver-sent-events

SSE and Servlet 3.0


I have registered a typical SSE when page loads:

Client:

sseTest: function(){

var source = new EventSource('mySSE');
source.onopen = function(event){
console.log("eventsource opened!");
};

source.onmessage = function(event){
var data = event.data;
console.log(data);
document.getElementById('sse').innerHTML+=event.data + "<br />";
};
}

My Javascript-Debugger says, that "eventsource opened!" was successfully.

My Server Code is a Servlet 3.0:

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Random;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(urlPatterns={"/mySSE"}, name = "hello-sse", asyncSupported=true)
public class MyServletSSE extends HttpServlet {

@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

resp.setContentType("text/event-stream");
resp.setCharacterEncoding("UTF-8");

Random random = new Random();
PrintWriter out = resp.getWriter();

//AsyncContext aCtx = req.startAsync(req, resp);
//ServletRequest sReq = aCtx.getRequest();

String next = "data: " + String.valueOf(random.nextInt(100) + 1) + "\n\n";
//out.print("retry: 600000\n"); //set the timeout to 10 mins in milliseconds
out.write(next);
out.flush();
// do not close the stream as EventSource is listening
//out.close();
//super.doGet(req, resp);
}
}

The code works! The Client-Code triggers the doGet()-Method every 3 seconds and retrieves the new data.

Questions: However, I wonder how I can make this code better by using new Servlet 3.0 Futures such as Async-Support or asyncContext.addListener(asyncListener) or something else which I do not know. As I never closes the stream, I wonder how my server will scale?

Theoretically, the best approach would be to trigger the doGet()-Method via server-side-code explicitly when new data is there, so the client does not need to trigger the client-side "onmessage()"-Method and therefore the server side "doGet()"-Method every 3 seconds for new data.


Solution

  • This is an excellent question, here is a full working example (Servlet 3.0 / Java EE 6)

    Some notes:

    1. it handles "browser tab / window closed" via out.checkError() that also calls flush()
    2. I wrote it quickly, so I'm sure it can be improved, just a POC, don't use in production before testing

    Servlet: (omitted imports for brevity, will update a full gist soon)

    @WebServlet(urlPatterns = {"/mySSE"}, asyncSupported = true)
    public class MyServletSSE extends HttpServlet {
    
      private final Queue<AsyncContext> ongoingRequests = new ConcurrentLinkedQueue<>();
      private ScheduledExecutorService service;
    
      @Override
      public void init(ServletConfig config) throws ServletException {
        final Runnable notifier = new Runnable() {
          @Override
          public void run() {
            final Iterator<AsyncContext> iterator = ongoingRequests.iterator();
            //not using for : in to allow removing items while iterating
            while (iterator.hasNext()) {
              AsyncContext ac = iterator.next();
              Random random = new Random();
              final ServletResponse res = ac.getResponse();
              PrintWriter out;
              try {
                out = res.getWriter();
                String next = "data: " + String.valueOf(random.nextInt(100) + 1) + "num of clients = " + ongoingRequests.size() + "\n\n";
                out.write(next);
                if (out.checkError()) { //checkError calls flush, and flush() does not throw IOException
                  iterator.remove();
                }
              } catch (IOException ignored) {
                iterator.remove();
              }
            }
          }
        };
        service = Executors.newScheduledThreadPool(10);
        service.scheduleAtFixedRate(notifier, 1, 1, TimeUnit.SECONDS);
      }
    
      @Override
      public void doGet(HttpServletRequest req, HttpServletResponse res) {
        res.setContentType("text/event-stream");
        res.setCharacterEncoding("UTF-8");
    
        final AsyncContext ac = req.startAsync();
        ac.setTimeout(60 * 1000);
        ac.addListener(new AsyncListener() {
          @Override public void onComplete(AsyncEvent event) throws IOException {ongoingRequests.remove(ac);}
          @Override public void onTimeout(AsyncEvent event) throws IOException {ongoingRequests.remove(ac);}
          @Override public void onError(AsyncEvent event) throws IOException {ongoingRequests.remove(ac);}
          @Override public void onStartAsync(AsyncEvent event) throws IOException {}
        });
        ongoingRequests.add(ac);
      }
    }
    

    JSP:

    <%@page contentType="text/html" pageEncoding="UTF-8"%>
    <!DOCTYPE html>
    <html>
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
            <title>JSP Page</title>
            <script>
                function test() {
                    var source = new EventSource('mySSE');
                    source.onopen = function(event) {
                        console.log("eventsource opened!");
                    };
    
                    source.onmessage = function(event) {
                        var data = event.data;
                        console.log(data);
                        document.getElementById('sse').innerHTML += event.data + "<br />";
                    };
                }
                window.addEventListener("load", test);
            </script>
        </head>
        <body>
            <h1>Hello SSE!</h1>
            <div id="sse"></div>
        </body>
    </html>