Server-Sent Events Using Spring
Here we are going to discuss sending unidirectional asynchronous events to any web app using Spring.
Join the DZone community and get the full member experience.
Join For FreeHere we are going to discuss sending unidirectional asynchronous events to any web app using Spring. You can send unidirectional events using the SseEmitter class in Spring. There is already a popular solution available for sending bi-directional events using Websockets. Using WebSockets both server and clients can communicate with each other using bi-directional connections between client and server. SSE is only used for sending uni-directional events from the server to clients using the HTTP protocol.
Basic knowledge of Spring and java threading is required.
Steps involved in SSE ar:
- The client opens an HTTP connection
- The server can send any number of messages to this connection asynchronously
- The server can close a connection or it can be closed because of some network error or any exception at the server-side.
- In case the connection is closed because of any error from the server or any network error, the client will automatically try to re-connect
Events
The server can send multiple events before closing the connection. Messages sent by the server should be text-based and the message starts with a keyword followed by a colon(:) and then a string message. ‘data’ is a keyword which represents a message for a client.
xxxxxxxxxx
data: message1
data: message2
data: message3
data: message4
In the case of multiple messages, messages should be separated by a blank line otherwise client will treat them as a single event. In the above case, all the 4 messages will be treated as a single event. SseEmitter class from package org.springframework.web.servlet.mvc.method.annotation used for emitting messages will take care of this next line and keyword format when you send multiple messages in Spring. You can also send objects in messages as a JSON string, which client can parse
xxxxxxxxxx
data:{"message":"message1","userId":"1"}
data:{"message":"message2","userId":"2"}
data:{"message":"message3","userId":"3"}
Creating Events Using SseEmitter
In Spring you can create an Async controller which will emit multiple messages to the client. You can create a normal GET type controller which can also accept multiple query parameters from the client.
xxxxxxxxxx
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
"/emitter") (
public SseEmitter eventEmitter( String userId) {
SseEmitter emitter = new SseEmitter(12000); //12000 here is the timeout and it is optional
//create a single thread for sending messages asynchronously
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
try {
for (int i = 0; i < 4; i++) {
emitter.send("message" + i);
}
} catch(Exception e) {
emitter.completeWithError(e);
} finally {
emitter.complete();
}
});
executor.shutdown();
return emitter;
}
Here, first, you have to create an object of SseEmitter with an optional timeout as an argument in the constructor.
Here we have created a single thread and send 4 messages from it. You can send multiple messages from multiple different threads as well. You can call multiple APIs in different threads and emit the response of those APIs asynchronously.
In case of error, you can send a special event with a complete error method. This will instruct the client about the error and we will check later how to handle error in the client.
Finally, after sending all the messages, you have to complete emitter by calling a complete method to close the connection.
The output of calling this controller will be multiple messages separated with a blank line so that client will treat them as separate events:
xxxxxxxxxx
data: message0
data: message1
data: message2
data: message3
Client-side Handling of Events
You can handle events in any javascript library or framework like react, angular, etc.
SSE is unidirectional, but the client can also communicate with the server by opening a separate connection with new request params.
For handling events client can open a connection with EventSource object with the URL of server API. EventSource is supported by mostly all of the popular browsers.
const eventSource = new EventSource
(
'http://localhost:8080/emitter?userId=123');
This will open up a connection with a server. You can handle events returned from the server by adding an event listener for an event named “message”. For all the successful events from the server, the callback method which is passed as the second argument while registering event listener will be called with one argument as an event object. Message sent from the server can be retrieved from event object by using property ‘data’, so event.data will hold the message sent by the server. This event.data can be a simple string or a JSON string. If it is a JSON string you have to parse it:
xxxxxxxxxx
eventSource.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
console.log(message);
})
There are two other built-in events as well apart from “message” in EventSource, one is ‘open’ which will be called once 200 status is received from the server. As the server is returning emitter without waiting for a thread to complete, the ‘open’ event will not wait for receiving all the messages and it will be called once the server responds with 200 status. Another event is ‘error’, this will be called whenever there is a network error and also when the server closes the connection by calling a ‘complete’ or ‘completeWithError’ method on the emitter.
xxxxxxxxxx
eventSource.addEventListener("open", (event) => {
console.log('connection is live');
});
eventSource.addEventListener("error", (event) => {
if (event.readyState == EventSource.CLOSED) {
console.log('connection is closed');
} else {
console.log("Error occured", event);
}
event.target.close();
});
So when you call emitter.completeWithError(exception) from the spring controller in case of any exception, then you can catch that event in an “error” event listener. Also, when you call emitter.complete() then too this “error” event listener will be called and console message from the first ‘if’ block will be printed. You can close connection at client-side by calling event.target.close();
, otherwise, the client will keep on retrying connection in case of error.
You can create named events with the help of SseEventBuilder in spring.
xxxxxxxxxx
SseEmitter emitter = new SseEmitter();
SseEmitter.SseEventBuilder sseEventBuilder = SseEmitter.event()
.id("0") // You can give nay string as id
.name("customEventName")
.data("message1")
.reconnectTime(10000); //reconnect time in millis
emitter.send(sseEventBuilder);
The output format for the above message is:
xxxxxxxxxx
event: customEventName
id: 0
data: message1
retry: 10000
Named Events
Here, from the spring controller, you are sending an event with a name, so you can create an event listener with the same name to handle messages of this event. This can be useful when you want to send different types of events from the server and the client can handle them differently by registering an event listener corresponding to them.
xxxxxxxxxx
eventSource.addEventListener("customEventName", (event)=> {
const message = JSON.parse(event.data);
console.log(message);
})
Id
Here, from spring you are sending an id, which can be any string value, you can retrieve this id in your client-side event listener by using lastEventId property of event object. This can be useful to track which events are received successfully by the client. In case of error, the browser will send a special header “Last-Event-ID” in re-connection request (This will only happen if you are not closing connection at client side in your “error” event listener, if you are closing connection then the browser will not retry connection in case of error). The server will parse this header in the request and can decide to decide which message to send next. In case you don’t want to track then there is no need to send id along with the message.
eventSource.addEventListener("customEventName", (event)=> {
console.log("Message id is " + event.lastEventId);
})
Retry
The browser will keep the server connection open. The server can close the connection by calling ‘complete’ or ‘completeWithError’ method and both of these events are handled at the client-side by “error” event listener. In our example, we are closing connection in our “error” event listener by calling event.target.close() at client side. But, if we don’t a close connection at the client-side then the browser will retry connection in case the connection is closed by the server or by some network error.
By default, the browser will wait for 3s before trying to establish the connection again and the browser will keep on retrying till it gets 200 status from the server. The server can change this default 3s wait time by sending this ‘retry’ flag. Here, in the above example server instructs the browser to wait for 10s (10000 milliseconds) before retrying connection in case of error by sending this “retry” flag. The server can send this flag value 0 as well, which will tell the browser to reconnect immediately if the connection is closed.
That’s all for SSE, hope you will find it useful.
Opinions expressed by DZone contributors are their own.
Comments