Home | Send Feedback

Server-Sent Events with Spring

Published: 5. March 2017  •  Updated: 16. February 2018  •  java, spring

A popular choice for sending real-time data from the server to a web application is WebSocket. WebSocket opens a bidirectional connection between client and server. Both parties can send and receive messages. In scenarios where the application only needs one-way communication from the server to the client, a simpler alternative exists Server-Sent Events (SSE). It's a HTML5 standard and utilizes HTTP as the transport protocol, the protocol only supports text messages, and it's unidirectional, only the server can send messages to the client.

Support

Server-Sent Events are supported in most modern browsers. Only Microsoft's browser IE does not have a built-in implementation. Fortunately, this is not a problem because Server-Sent Events use standard HTTP connections and can, therefore, implemented with a polyfill. The following polyfill libraries area available and add Server-Sent Event support to IE.

We use the Yaffle/EventSource library in our productive applications, and it works so far, very reliable.

With SSE, the server cannot immediately start sending messages to the client. It's always the client (browser) that establishes the connection, and after that, the server can send messages. An SSE connection is a long streaming HTTP connection.

  1. Client opens the HTTP connection
  2. Server sends zero, one or more messages over this connection.
  3. Connection is closed by the server or due to a network error.
  4. Client opens a new HTTP connection and so on...

The useful thing about Server-Sent Events is that they have a built-in reconnection feature when the client loses the connection. He tries to reconnect to the server automatically. WebSocket does not have such built-in functionality.

Even though only the server can send messages to the client over SSE, you could develop applications like a chat application with this technology. Because a client application can always open a second HTTP connection with the Fetch API or XMLHttpRequest and send data to the server.

Client API

The Server-Sent Events API in the browser is simple and consist of only one object: EventSource
To open a connection, an application needs to instantiate the object and provide the URL of the server endpoint.

const eventSource = new EventSource('http://localhost:8080/stream'); 

The browser immediately sends a GET request to the URL with the accept header text/event-stream

GET /stream HTTP/1.1
Accept: text/event-stream
...

Because this is a normal GET request, an application could add query parameters to the URL like with any other GET request.

const eventSource = new EventSource('http://localhost:8080/stream?event=news'); 

Query parameters cannot be changed during the lifecycle of the EventSource object. Every time the client reconnects he uses the same URL. But an application can always close the connection with the close() method and instantiate a new EventSource object.

let eventSource = new EventSource('http://localhost:8080/stream?event=worldnews'); 
...
eventSource.close();
eventSource = new EventSource('http://localhost:8080/stream?event=sports'); 

The HTTP response to a EventSource GET request must contain the Content-Type header with the value text/event-stream and the response must be encoded with UTF-8.

HTTP/1.1 200
Content-Type: text/event-stream;charset=UTF-8

The protocol that SSE uses is text-based, starts with a keyword, then a colon (:), and a string value.
The data keyword specifies a message for the client. Spaces before and after the message are being ignored. Every line is separated with a carriage return (0d) or a line feed (0a) or both (0d 0a).

data: the server message

The server can split a message over several lines

data:line1
data:line2
data:line3

The browser concatenates these three lines and emits one event. To separate the message from each other, the server needs to send a blank line after each message.

data:{"heap":148713928,"nonHeap":49888752,"ts":1488640735925}

data:{"heap":149344096,"nonHeap":49889392,"ts":1488640736927}

data:{"heap":149344096,"nonHeap":49889392,"ts":1488640736929}

You see that the payload does not have to be a simple string. It's perfectly legal to send JSON strings and parse them on the client with JSON.parse.

To process these events in the browser, an application needs to register a listener for the message event. The property data of the event object contains the message. The browser filters out the keyword data and the colon and only assigns the string after the colon to event.data.

eventSource.onmessage = event => {
  const msg = JSON.parse(event.data);
  //access msg.ts, msg.heap, msg.nonHeap
};

An application can listen for the open and error event. The open event is emitted as soon as the server sends a 200 response back. The error event is fired whenever a network error occurs. It is also emitted when the server closes the connection.

eventSource.onopen = e => console.log('open');
eventSource.onerror = e => {
  if (e.readyState == EventSource.CLOSED) {
   console.log('close');
  }
  else {
    console.log(e);
  }
};

Named Events

A server can assign an event name to a message with the event: keyword. The event: line can precede or follow the data: line. In this example, the server sends 4 messages. The first message is an add event, the second a remove event, then follows an add event again, and the last message is an unnamed event.

event:add
data:100

data:56
event:remove

event:add
data:101

data:simple event

Named events are processed differently on the client. They do not trigger the message handlers. Named events emit an event that has the same name as the event itself. For this example, we need 3 listeners to process all the messages. You cannot use the on... syntax for registering listeners to these events. They have to be registered with the addEventListener function.

eventSource.onmessage = e => {
  // receives: 'simple event'
};
/* OR
eventSource.addEventListener('message', function(e) {
  // receives: 'simple event'
}, false);
*/

source.addEventListener('add', function(e) {
  //receives "100" and "101" 
}, false);
    
source.addEventListener('remove', function(e) {
  //receives "56" 
}, false);

Reconnect

Browsers keep the Server-Sent Events HTTP connection open as long as possible. When the connection is closed by the server or due to a network error, the browser waits by default 3 seconds and then opens a new HTTP connection. The browser tries to reconnect forever until he gets a 200 HTTP response back. With a call to close(), an application can stop this.

The 3 seconds wait time between connections can be changed by the server. To change it, the server sends a retry: line together with the message. The number after the colon specifies the number of milliseconds the browser has to wait before tries to reconnect.

event:add
data:100
retry:10000

After the browser receives this message, he changes the wait time between connections to 10 seconds. With retry:0, the browser immediately tries to reconnect after the previous connection was closed.

ID

The server can assign an id to every message with the id: keyword. Valid values for the id are any arbitrary string.

id:2012-08-19T10:11:20
data:648

A client can access this id with the property lastEventId of the event object.

source.addEventListener('message', function(e) {
  console.log(e.data); // "648"
  console.log(e.lastEventId); // "2012-08-19T10:11:20"
}, false);

The primary use case for this id is to keep track of what messages the client successfully received. When the SSE connection was closed, the browser sends a new GET request, and in this request, he sends the last received message id as an additional HTTP header Last-Event-ID to the server.

GET /memory HTTP/1.1
Accept: text/event-stream
Last-Event-ID: 2012-08-19T10:11:20

The server can then read this header and send all newly created messages since this id to the client, to make sure that the client receives all messages without any gap.

SSE support in Spring

Spring introduced support for Server-Sent Events with version 4.2 (Spring Boot 1.3). In the following example, we create a Spring Boot application that sends the currently used heap and non-heap memory of the Java virtual machine as Server-Sent Events to the client. The client is a simple HTML page that displays these values.

We create the server application with the https://start.spring.io/ website and select Web as the only dependency.

Next, we create a POJO that holds the memory information

public class MemoryInfo {
  private final long heap;

  private final long nonHeap;

  private final long ts;

MemoryInfo.java

Then we create a scheduled service that reads the memory information every second, creates an instance of the MemoryInfo class and publishes it with Spring's event bus infrastructure

@Service
public class MemoryObserverJob {

  public final ApplicationEventPublisher eventPublisher;

  public MemoryObserverJob(ApplicationEventPublisher eventPublisher) {
    this.eventPublisher = eventPublisher;
  }

  @Scheduled(fixedRate = 1000)
  public void doSomething() {
    MemoryMXBean memBean = ManagementFactory.getMemoryMXBean();
    MemoryUsage heap = memBean.getHeapMemoryUsage();
    MemoryUsage nonHeap = memBean.getNonHeapMemoryUsage();

    MemoryInfo mi = new MemoryInfo(heap.getUsed(), nonHeap.getUsed());
    this.eventPublisher.publishEvent(mi);
  }

MemoryObserverJob.java

Next, we create a RestController that handles the EventSource GET request from the client. The get handler needs to return an instance of the class SseEmitter. Each client connection is represented with its own instance of SseEmitter. Spring does not give you the tools to manage these SseEmitter instances. In this application, we store the emitters in a simple list(emitters) and add handlers to the emitter's completion and timeout event to remove them from the list.

@Controller
public class SSEController {

  private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();

  @GetMapping("/memory")
  public SseEmitter handle(HttpServletResponse response) {
    response.setHeader("Cache-Control", "no-store");

    SseEmitter emitter = new SseEmitter();
    // SseEmitter emitter = new SseEmitter(180_000L);

    this.emitters.add(emitter);

    emitter.onCompletion(() -> this.emitters.remove(emitter));
    emitter.onTimeout(() -> this.emitters.remove(emitter));

    return emitter;
  }

  @EventListener
  public void onMemoryInfo(MemoryInfo memoryInfo) {
    List<SseEmitter> deadEmitters = new ArrayList<>();
    this.emitters.forEach(emitter -> {
      try {
        emitter.send(memoryInfo);

        // close connnection, browser automatically reconnects
        // emitter.complete();

        // SseEventBuilder builder = SseEmitter.event().name("second").data("1");
        // SseEventBuilder builder =
        // SseEmitter.event().reconnectTime(10_000L).data(memoryInfo).id("1");
        // emitter.send(builder);
      }
      catch (Exception e) {
        deadEmitters.add(emitter);
      }
    });

    this.emitters.removeAll(deadEmitters);
  }

SSEController.java

By default, Spring Boot with the embedded Tomcat server keeps the SSE HTTP connection open for 30 seconds. An application can change that with an entry to the application.properties file

spring.mvc.async.request-timeout=45000

This setting keeps the HTTP connection open for 45 seconds. Alternatively, an application can set the timeout directly on the SseEmitter constructor.

SseEmitter emitter = new SseEmitter(180_000L); //keep connection open for 180 seconds

The method onMemoryInfo is annotated with the @EventListener annotation and listens for the events that are sent from the MemoryObserverJob class. When a new object is emitted, the method loops over all registered clients and tries to send the MemoryInfo to the clients. The send call can always fail when the client is no longer connected. Servers do not get informed when the EventSource connection is closed by the client either normally with close() or due to a network error, or the user just closed the browser. Because of that, we add a try-catch around the send method, and when sending fails, the application removes that client from the emitters list.

To send messages to the client, the application calls the emitter's send method. This method expects either an object or a SseEventBuilder. Objects are going to be converted to a JSON string and sent in a data: line to the client. The SseEventBuilder allows the application to set all the previously mentioned message attributes like retry, id, and event name. The static method event() of the SseEmitter class creates a new SseEventBuilder.

SseEventBuilder builder = SseEmitter.event()
                                    .data(memoryInfo)
                                    .id("1")
                                    .name("eventName")
                                    .reconnectTime(10_000L);
emitter.send(builder);

Every method of the builder corresponds to a keyword in the SSE message:

data(...) -> data:
id(...) -> id:
name(...) -> event: 
reconnectTime(...) -> retry:

Spring provides an easy way to access the Last-Event-ID HTTP header when the application needs it. You have to make the parameter optional with required=false because the initial GET request from the client does not contain this HTTP header.

@GetMapping("/memory")
public SseEmitter handle(@RequestHeader(name = "Last-Event-ID", required = false) String lastId) {
  ...
}

The client of our example opens the SSE connection with

const eventSource = new EventSource('http://localhost:8080/memory');

and registers a message listener that parses the JSON and sets innerHTML of three DOM elements to display the received data.

<!DOCTYPE html>
<html>
<head>
<title>Server Memory Monitor</title>
<script>
function initialize() {
  const eventSource = new EventSource('http://localhost:8080/memory');

  eventSource.onmessage = e => {
    const msg = JSON.parse(e.data);    
    document.getElementById("timestamp").innerHTML = new Date(msg.ts);
    document.getElementById("heap").innerHTML = msg.heap;
    document.getElementById("nonheap").innerHTML = msg.nonHeap;
  };
  
  eventSource.onopen = e => console.log('open');

  eventSource.onerror = e => {
    if (e.readyState == EventSource.CLOSED) {
      console.log('close');
    }
    else {
      console.log(e);
    }
  };
  
  eventSource.addEventListener('second', function(e) {
      console.log('second', e.data);
    }, false);  
}

window.onload = initialize;
</script>
</head>
<body>
  <h1>Memory Observer</h1>

  <h3>Timestamp</h3>
  <div id="timestamp"></div>

  <h3>Heap Memory Usage</h3>
  <div id="heap"></div>

  <h3>Non Heap Memory Usage</h3>
  <div id="nonheap"></div>
</body>
</html>

index.html

You find the source code for the entire project on GitHub.

More information

The following articles provide you more information about Server-Sent Events.

Keeping track

At the end of this blog post, a shameless self-plug. Because Spring does not provide support for keeping track of the SseEmitter instances, I wrote a library that does that for Spring Boot applications. You can add the library with this dependency to a project.

<dependency>
  <groupId>ch.rasc</groupId>
  <artifactId>sse-eventbus</artifactId>
  <version>1.1.9</version>
</dependency>  

To enable the library, you need to add the @EnableSseEventBus annotation to an arbitrary @Configuration class.

@SpringBootApplication
@EnableSseEventBus
public class Application {
    ...
}

The library creates a bean of type SseEventBus that an application can inject into any Spring-managed bean.

@Controller
public class SseController {
  private final SseEventBus eventBus;
  public SseController(SseEventBus eventBus) {
    this.eventBus = eventBus;
  }

  @GetMapping("/register/{id}")
  public SseEmitter register(@PathVariable("id") String id) {
    return this.eventBus.createSseEmitter(id, SseEvent.DEFAULT_EVENT)
  }
}

The library expects that each client sends a unique id. An application can create such an id with a UUID library like https://github.com/uuidjs/uuid. For starting the SSE connection, the client calls the endpoint with the createSseEmitter method and sends the id and optionally the names of the events he is interested in.

const uuid = uuid();
const eventSource = new EventSource(`/register/${uuid}`);
eventSource.addEventListener('message', response => {
    //handle the response from the server
    //response.data contains the data line 
}, false);

To publish messages, an application can either call the handleEvent method on the SseEventBus bean or publishes a SseEvent object with Spring's event infrastructure

@Service
public class DataEmitterService {
  private final SseEventBus eventBus;
  public DataEmitterService(SseEventBus eventBus) {
    this.eventBus = eventBus;
  }

  public void broadcastEvent() {
    this.eventBus.handleEvent(SseEvent.ofData("some useful data"));
  }
}
@Service
public class DataEmitterService {
  private final ApplicationEventPublisher eventPublisher;
  public DataEmitterService(ApplicationEventPublisher eventPublisher) {
    this.eventPublisher = eventPublisher;
  }

  public void broadcastEvent() {
    this.eventPublisher.publishEvent(SseEvent.ofData("some useful data"));
  }
}

Visit the GitHub project page for more information: https://github.com/ralscha/sse-eventbus
You find a demo application that uses the library in this repository: https://github.com/ralscha/sse-eventbus-demo