Server Sent Events — Experiment & Why We Need to Use it?
In this article, I will share about the experiment of Server Sent Events in my use case. I will explain why Server Sent Events become one of the alternatives that we can use.
Can you imagine that when the system is in a race condition between server’s processing data and client’s retrieving data? This is what happen in my use case. In this case, the processing data is via NSQ (asynchronous). Meanwhile, client retrieves data via API (synchronous).
From the above diagram:
1. Service A sends message that will be consumed by Service B.
2.1. At the same time or even faster than 2.2, Client will request to Service B to get the result from data that published by Service A.
2.2. Service B is still in the middle processing the message or even haven’t received the message yet.
The main problem here is client doesn’t know when the data is finished to process. It happened because of the race condition.
The Goals
Client no need to wait or request to Server manually. We can use one of alternatives that called Server Sent Events (SSE).
Disclaimer: This experiment is using REST API and based on personal experiment.
Server Sent Events (SSE) Overview
What is SSE?
Server-Sent Events (SSE) is a unidirectional (one way) data transmission to send events from server to client through HTTP. Client and server need to connect first. Once both of them already connect, server can send events until the connection is closed.
When to Use it?
- Progress updates. If we are in the middle of processing something and we want to know the progress, we can use SSE. For example, we are uploading file, we can show the percentage of the progress by using SSE.
- Realtime notification. We can broadcast news/information as soon as it’s available.
- Live data updates. For example, the position of other user (live location sharing) or drivers.
Pro & Cons
WebSockets Overview
WebSockets is an advanced technology that allows real-time interactive bidirectional (two way) communication between a client and a server.
SSE vs WebSockets
Why We use SSE compare to WebSockets?
For simple use case, we can implement SSE. This is a good choice. If we only need to push updates/information to user, SSE is enough. But for more complex use case, it’s better to use WebSockets. So, it depends on the use case. We can compare based on the pro and cons of SSE and WebSockets to decide which one we will use. For my use case, only Server that needs to send event. That’s why we chose SSE.
Experiment
The Flow
- Client opens connection to Server (Service B). Once connection is successfully created, Client will listen to the event that served by Service B.
- Service B sends message to Service B.
- Service B consumes the message and processes the data.
- Service B sends event that contained response to Client.
- Client handles the response, like showing something on the UI. Then, Client can close the connection.
Please do not close the connection if the system have retry mechanism. Let say, the system wanna do retry if processing data is failed. For this case, Server can send flag to retry or not.
Server side
Using a Rest API with Content-Type: text/event-stream
.
w.Header().Set("Content-Type", "text/event-stream")
Client side
Using Javascript new EventSource
with URL value is Server URL.
const esEvent = new EventSource(url);
Sample Code
Source code based on https://github.com/gurleensethi/go-server-sent-event-example.
Server Side
main.go
package main
import (
"fmt"
"net/http"
"strconv"
)
func main() {
http.HandleFunc("/sse", func(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
return
}
w.Header().Set("Content-Type", "text/event-stream")
// Enable CORS because in this case, client and server are in different server/port.
enableCors(w)
isRetry, _ := strconv.ParseBool(r.FormValue("is_retry"))
// goroutine will generate the response
eventCh := make(chan EventResponse)
go generateResponse(r.Context(), eventCh, isRetry)
// Event name: test-event
for eventData := range eventCh {
event, err := buildResponse("test-event", eventData)
if err != nil {
break
}
_, err = fmt.Fprint(w, event)
if err != nil {
break
}
flusher.Flush()
}
})
http.ListenAndServe(":8888", nil)
}
func enableCors(w http.ResponseWriter) {
// Allow URL Client
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:8000")
// If Client use option WithCredentials: true, Server also need to allow Credentials.
// w.Header().Set("Access-Control-Allow-Credentials", "true")
}
In here, we use goroutine to have the retry mechanism:
event.go
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"strings"
"time"
)
type EventResponse struct {
Message string `json:"message"`
Timestamp string `json:"timestamp"`
IsRetry bool `json:"is_retry"`
}
func buildResponse(event string, data EventResponse) (string, error) {
m := map[string]EventResponse{
"data": data,
}
buff := bytes.NewBuffer([]byte{})
encoder := json.NewEncoder(buff)
err := encoder.Encode(m)
if err != nil {
return "", err
}
sb := strings.Builder{}
sb.WriteString(fmt.Sprintf("event: %s\n", event))
sb.WriteString(fmt.Sprintf("data: %v\n\n", buff.String()))
return sb.String(), nil
}
func generateResponse(ctx context.Context, eventCh chan<- EventResponse, isRetry bool) {
ticker := time.NewTicker(time.Second)
retryLoop:
for {
select {
case <-ctx.Done():
break retryLoop
case <-ticker.C:
time.Sleep(5 * time.Second)
if isRetry {
eventCh <- EventResponse{
Message: "Message are still in proccess. Please retry.",
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
IsRetry: true,
}
} else {
eventCh <- EventResponse{
Message: "You've received a message",
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
}
}
}
}
ticker.Stop()
close(eventCh)
}
Client Side
main.go
package main
import (
_ "embed"
"net/http"
)
//go:embed index.html
var indexHTML []byte
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write(indexHTML)
})
http.ListenAndServe(":8000", nil)
}
index.html
<html lang="en">
<head>
<title>Server Sent Events</title>
</head>
<body>
<strong>
<p id="title">Server Sent Events</p>
<p id="message">Loading Message...</p>
<p id="timestamp">Loading Time...
</strong>
<script>
// This is For UI
const messageEl = document.getElementById("message");
const timestampEl = document.getElementById("timestamp");
// URL Params
const urlParams = new URLSearchParams(window.location.search);
const isRetry = urlParams.get('is_retry');
var url = "http://localhost:8888/sse?is_retry=" + isRetry
const esEvent = new EventSource(url);
// We can it send option withCredentials. It will send cookie. The Server also need to allow Credentials.
// Sample Client side:
// const esEvent = new EventSource(url, {withCredentials : true});
// Only support withCredentials. not support other parameter like:
// const esEvent = new EventSource(url, {"user_id": userID});
// when there is error
esEvent.onerror = (err) => {
};
// when a connection successfully to open
esEvent.onopen = (...args) => {
};
// listen to event: test-event
esEvent.addEventListener("test-event", (event) => {
const resp = JSON.parse(event.data);
messageEl.innerText = resp.data.message;
timestampEl.innerText = resp.data.timestamp;
if (resp.data.is_retry){
setTimeout(() => {
messageEl.innerText = "Loading Message...";
timestampEl.innerText = "Loading Time...";
}, 3000);
}else{
// to close the connection. Once this is closed, the Client can not listen to the event anymore and need to reopen connection.
esEvent.close();
}
});
</script>
</body>
</html>
Result
As you can see on network, when Client requested to Server, the type is eventsource
.
No Retry Mechanism
Once Client already got the response, Client will close the connection and won’t listen to the event anymore.
With Retry Mechanism
Client will still listen to the event until Server response to stop retrying.
Based on the experiment, instead of Client requested manually, Client will listen to the event. Therefore, Client will be notified if there is updates/info. SSE is able to support this use case. For simple use case, like push updates, it’s good to give a try for SSE.
References
https://ably.com/topic/server-sent-events
https://ably.com/blog/websockets-vs-sse
https://ably.com/topic/what-are-websockets-used-for
https://javascript.info/server-sent-events
https://blog.logrocket.com/server-sent-events-vs-websockets/
https://github.com/gurleensethi/go-server-sent-event-example
Hope this article is useful for you!