Introduction

In web application space, there are a few strategies that can be used to retrieve real-time data from the server. Some of the most popular ones are:

  • Continuous polling.
  • Web sockets.
  • Server Sent Events (SSE).
  • Managed Cloud services like PubNub, Firebase realtime database/function etc.
  • Push notification services like OneSignal, Firebase push notifications etc.

In this post, we will explore the basics of Server Sent Events. I will use Go to write the server logic, but SSE support is available in all other programming languages and frameworks.

All the code referenced in this blog post can be found here. Interested users can clone the repository and follow the instructions in readme.md. If you find any mistakes or have any questions or queries, please create an issue here and I will be more than happy to clarify them.

Server sent events (SSE)

From Mozilla website:

Traditionally, a web page has to send a request to the server to receive new data; that is, the page requests data from the server. With server-sent events, it’s possible for a server to send new data to a web page at any time, by pushing messages to the web page. These incoming messages can be treated as Events + data inside the web page.

SSE is a powerful tool to retrieve real-time updates with low latency over a single persistent HTTP connection. This protocol is widely supported by all browsers and is integrated with the HTML standard. What makes SSE truly stand out is its unique ability to allow the server to take the lead and push data to the client, rather than waiting for the client to initiate a request. This approach eliminates the need for clients to constantly poll the server, resulting in reduced network traffic and increased efficiency.

server-sent-events-overview

It is important to note that SSE are one way message transmission technology. The data flow happens from Server to the client. Client requests events by triggering an http endpoint over http, the server then performs the backend logic and returns the data as and when it is available. When the server has sent all the data, the connection is closed.

Example

To demonstrate the SSE mechanism, we will create a dummy application that gets the list of random cat breeds from server and dynamically displays them on the browser. For simplicity of implementation, we will use Go to serve both backend logic as well as the html. Additionally, all the error handling is skipped to keep the demonstration simple and easy to understand.

Server component

First we create our server component. In the setup below, we first wire the html file present in the static dir to serve the frontend on the root path /. On path /events, we write the logic to generate the server sent events.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func main() {
	// serve html content from static directory
    // request to http://localhost:8080/ will serve the html page
	http.Handle("/", http.FileServer(http.Dir("static")))   // ---> 1

	// request to http://localhost:8080/events will return the dynamically 
    // generated events
	http.HandleFunc("/events", eventsHandler)               // ---> 2

	// serve application on port 8080
	http.ListenAndServe(":8080", nil)
}

func eventsHandler(w http.ResponseWriter, r *http.Request) {// ---> 3
	w.Header().Set("Access-Control-Allow-Origin", "*")  // cors config
	w.Header().Set("Content-Type", "text/event-stream") // return an event stream
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")

	count := 1
	gofaker.Seed(int64(rand.Int()))

    // generate 10 random cat breeds
	for {
        if count > 10 {
            // exit after 10 records
			break                                        // ---> 6
		}
        breed := gofaker.Cat()
                                                         // ---> 4
		fmt.Fprintf(w, "data: %s \n\n", fmt.Sprintf("%d : %s", count, breed))
		w.(http.Flusher).Flush()                         // ---> 5
		time.Sleep(1 * time.Second)
		count++
	}
}

Summary: Server

  1. Bind the content under static directory on the server to serve its contents on root path /.
  2. Serve the logic to return server-sent-events on /events path.
  3. On the eventsHandler(), set the correct response headers.
  4. For each loop iteration, append server event to the response output stream.
  5. Flush each event back to the client.
  6. After 10 iterations, break the loop. This will cause the connection to drop. The client can detect the connection drop and perform any cleanups if required.

Client component.

The client component, on click of the button calls the function fetchData(), which initiates an EventSource request. It appends the server-sent data to the table as long as the connection is alive. On drop of the connection, the onerror() function is triggered where the eventsource is closed and cleanup can be done, if required.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html>
<body>
    <main class="container">
        <h1>Cat breeds</h1>
        <button onclick="fetchData();">Click to get cat breeds</button>
        <table>
            <tbody id="root"></tbody>
        </table>
        <div></div>
    </main>
</body>
<script >
function fetchData() {
  // on click of the button, trigger an EventSource request
  const eventSource = new EventSource('http://localhost:8080/events'); // --> 1

  // keep consuming the events as long as they come
  eventSource.onmessage = function (event) {                           // --> 2
      const dataElement = document.getElementById('root');
      dataElement.innerHTML += '<th scope="row">' + event.data + '</th>';
  };

  // on error (when encountering \n\n), stop the event source
  eventSource.onerror = function (event) {                            // --> 3
      console.log(event);
      eventSource.close();
      console.log("do any cleanups, if required")
  }
}
</script>
</html>

Summary: Client

  1. Initiate request to server to send events in response
  2. As long as events are available, transform the data and append it to the table.
  3. Capture error, when the connection is dropped by server and close the event source.

Here is a demo of the application. If you clone the repository, go to localhost:8080, you would be greeted with the landing page. On click of the button, the server will start streaming the cat breeds and the UI will be updated with cat breeds in realtime.

Conclusion

SSE is a great toolkit for the use cases where unidirectional data flow is required from server to client at low latency. It is a useful approach for handling situations like social media notifications, news feeds, or delivering data into a client-side storage, live graphs or progress updates. It is a good alternative to trivial techniques like client side polling using ajax and can use multiple data formats like json, xml or html fragments.

Reference