How to Build Real-Time Notification Service Using Server-Sent Events (SSE)
For the SSE to work, the server needs to tell the client that the response’s content-type is text/eventstream.
Join the DZone community and get the full member experience.
Join For FreeMost of the communication on the Internet comes directly from the clients to the servers. The client usually sends a request, and the server responds to that request. It is known as a client-server model, and it works well in most cases. However, there are some scenarios in which the server needs to send a message to the client without the preceding request.
In such cases, we have a couple of options: we can use short and long polling, webhooks, websockets, or event streaming platforms like Kafka. However, there is another technology, not popularized enough, which in many cases, is just perfect for the job. This technology is the Server-Sent Events (SSE) standard.
What Are Server-Sent Events?
SSE definition states that it is an HTTP standard that allows a web application to handle a unidirectional event stream and receive updates whenever the server emits data. In simple terms, it is a mechanism for unidirectional event streaming.
Browsers Support
It is currently supported by all major browsers except Internet Explorer.
Message Format
The events are just a stream of UTF-8 encoded text data in a format defined by the Specification. The important aspect here is that the format defines the fields that the SSE message should have, but it does not mandate a specific type for the payload, leaving the freedom of choice to the users.
xxxxxxxxxx
{
"id": "message id <optional>",
"event": "event type <optional>",
"data": "event data –plain text, JSON, XML… <mandatory>"
}
SSE Implementation
For the SSE to work, the server needs to tell the client that the response’s content-type is text/eventstream. Next, the server receives a regular HTTP request, and leaves the HTTP connection open until no more events are left or until the timeout occurs. If the timeout occurs before the client receives all the events it expects, it can use the built-in reconnection mechanism to re-establish the connection.
Simple Endpoint (Flux):
The simplest implementation of the SSE endpoint in Spring can be achieved by:
- Specifying the produced media type as text/event-stream,
- Returning Flux type, which is a reactive representation of a stream of events in Java.
@GetMapping
(path =
"/stream-flux"
, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public
Flux<String> streamFlux() {
return
Flux.interval(Duration.ofSeconds(
1
))
.map(sequence ->
"Flux - "
+ LocalTime.now().toString());
ServerSentEvent Class
Spring introduced support for SSE specification in version 4.2 together with a ServerSentEvent class. The benefit here is that we can skip the text/event-stream media type explicit specification, as well as we can add metadata such as id or event type.
@GetMapping
(
"/sse-flux-2"
)
public
Flux<ServerSentEvent> sseFlux2() {
return
Flux.interval(Duration.ofSeconds(
1
))
.map(sequence -> ServerSentEvent.builder()
.id(String.valueOf(sequence))
.event(
"EVENT_TYPE"
)
.data(
"SSE - "
+ LocalTime.now().toString())
.build());
}
SseEmitter Class:
However, the full power of SSE comes with the SseEmitter class. It allows for asynchronous processing and publishing of the events from other threads. What is more, it is possible to store the reference to SseEmitter and retrieve it on subsequent client calls. This provides a huge potential for building powerful notification scenarios.
@GetMapping
(
"/sse-emitter"
)
public
SseEmitter sseEmitter() {
SseEmitter emitter =
new
SseEmitter();
Executors.newSingleThreadExecutor().execute(() -> {
try
{
for
(
int
i =
0
;
true
; i++) {
SseEmitter.SseEventBuilder event = SseEmitter.event()
.id(String.valueOf(i))
.name(
"SSE_EMITTER_EVENT"
)
.data(
"SSE EMITTER - "
+ LocalTime.now().toString());
emitter.send(event);
Thread.sleep(
1000
);
}
}
catch
(Exception ex) {
emitter.completeWithError(ex);
}
});
return
emitter;
Client example:
Here is a basic SSE client example written in Javascript. It simply defines an EventSource and subscribes to the message event stream in two different ways.
// Declare an EventSource
const eventSource =
new
EventSource(
'http://some.url'
);
// Handler for events without an event type specified
eventSource.onmessage = (e) => {
// Do something - event data etc will be in e.data
};
// Handler for events of type 'eventType' only
eventSource.addEventListener(
'eventType'
, (e) => {
// Do something - event data will be in e.data,
// message will be of type 'eventType'
});
SSE vs. Websockets
When it comes to SSE, it is often compared to Websockets due to usage similarities between both of the technologies.
- Both are capable of pushing data to the client,
- Websockets are bidirectional – SSE unidirectional,
- In practice, everything that can be done with SSE, and can also be achieved with Websockets,
- SSE can be easier,
- SSE is transported over a simple HTTP connection,
- Websockets require full duplex-connection and servers to handle the protocol,
- Some enterprise firewalls with packet inspection have trouble dealing with Websockets – for SSE that’s not the case,
- SSE has a variety of features that Websockets lack by design, e.g., automatic reconnection, event ids,
- Only Websockets can send both binary and UTF-8 data, SSE is limited to UTF-8,
- SSE suffers from a limitation to the maximum number of open connections (6 per browser + domain). The issue was marked as Won’t fix in Chrome and Firefox.
Use Cases:
Notification Service Example:
A controller providing a subscribe to events and a publish events endpoints.
xxxxxxxxxx
"/events") (
public class EventController {
public static final String MEMBER_ID_HEADER = "MemberId";
private final EmitterService emitterService;
private final NotificationService notificationService;
public SseEmitter subscribeToEvents( (name = MEMBER_ID_HEADER) String memberId) {
log.debug("Subscribing member with id {}", memberId);
return emitterService.createEmitter(memberId);
}
HttpStatus.ACCEPTED) (
public void publishEvent( (name = MEMBER_ID_HEADER) String memberId, EventDto event) {
log.debug("Publishing event {} for member with id {}", event, memberId);
notificationService.sendNotification(memberId, event);
}
}
A service for sending the events:
xxxxxxxxxx
public class SseNotificationService implements NotificationService {
private final EmitterRepository emitterRepository;
private final EventMapper eventMapper;
public void sendNotification(String memberId, EventDto event) {
if (event == null) {
log.debug("No server event to send to device.");
return;
}
doSendNotification(memberId, event);
}
private void doSendNotification(String memberId, EventDto event) {
emitterRepository.get(memberId).ifPresentOrElse(sseEmitter -> {
try {
log.debug("Sending event: {} for member: {}", event, memberId);
sseEmitter.send(eventMapper.toSseEventBuilder(event));
} catch (IOException | IllegalStateException e) {
log.debug("Error while sending event: {} for member: {} - exception: {}", event, memberId, e);
emitterRepository.remove(memberId);
}
}, () -> log.debug("No emitter for member {}", memberId));
}
}
To sum up, Server-Sent Events standard is a great technology when it comes to a unidirectional stream of data and often can save us a lot of trouble compared to more complex approaches such as Websockets or distributed streaming platforms.
A full notification service example implemented with the use of Server-Sent Events can be found on my GitHub: https://github.com/mkapiczy/server-sent-events
Sources:
- https://www.baeldung.com/spring-server-sent-events
- https://www.w3.org/TR/eventsource/
- https://stackoverflow.com/questions/5195452/websockets-vs-server-sent-events-eventsource
- https://www.telerik.com/blogs/websockets-vs-server-sent-events
- https://simonprickett.dev/a-look-at-server-sent-events/
Published at DZone with permission of Michal Kapiczynski. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments