19

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.

nimo23
  • 5,170
  • 10
  • 46
  • 75
  • This is one of the best questions I've seen in SO, although I answered the question, I actually learned from it a lot, especially about EventSource! – Eran Medan Jun 25 '13 at 18:13
  • If there are 1000 clients, are those mean there will be 1000 connections to the server? – Harun Aug 17 '17 at 10:11

2 Answers2

16

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>
Eran Medan
  • 44,555
  • 61
  • 184
  • 276
  • 2
    One slow client read will slow all of the other client writes down, as this technique uses blocking writes. (which was an appropriate answer back in 2013). Modern apps should probably use Servlet 3.1 with Async I/O writes. – Joakim Erdfelt Feb 09 '15 at 19:41
  • Can you give the link of "Servlet 3.1 with Async I/O writes"? – Harun Aug 17 '17 at 10:12
  • 2
    Joakim: Is that really a problem? I just made a test run. Two clients, one blocking in an alert(). Server writes a short message to both every 30 s. Ran overnight without a hitch. Perhaps there's plenty of buffering in the chain, but still... – Per Lindberg Oct 27 '17 at 07:33
1

Useful example.

People might get "IllegalStateException: Not supported" for startAsync(), in which case either don't forget:

@WebServlet(urlPatterns = "/Sse", asyncSupported=true)

or use

request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);

from this post

Community
  • 1
  • 1