Server-Sent Events with Go (Server And Client Sides)
Server-Sent Events (SSE) is a unidirectional data transmission technology to send events from server to client through HTTP. This technology can help solve some real-time data fetching problems to optimise processes (for example: receive latest feed, send connected users a notification & etc.) or even to create a simple chat type application.
For this article, I chose to implement a simple server, which accepts clients, stores their connections and sends events to them.
Furthermore, I have also written an example of a simple SSE clients, which is written with Go and HTML to receive events from same server. You can take a closer look at my short example here.
Also it’s worth of mentioning that for implementation I chose to store each client connection to avoid channels usage (a bit different approach). With this kind of approach, server will reuse connections instead of creating additional channels and their management logic.
Server side
To begin with, we need to declare a simpleServer
and connection
structures:
So what is for in each structure? Let’s go with connection:
- writer is a received connection response writer, from which server send events to client.
- flusher is a interface, which be used to send buffered data to client.
- requestCtx is a context of clients requests, which be used to track, when user disconnects.
And a Server:
- connections is a map of connection type items, which be used to store all connections.
- connectionsMutex is a Read-Write type mutex to prevent race conditions.
From first glance it might look a bit complicated, but let’s move to a requests acceptance part!
So, what do we see here? A simple logic, which will be called after new connection appears:
line: 3
parsing flusher from ResponseWriter, which will be set in connection item and later used for sending events to client. Also, it worth mentioning thatline: 5
I pre-check if type parsing was successful.line: 10
selecting requests context andline: 11-17
adding new connection to list. For distinguish all connections, I chose to use requests remote address.line: 19
defining defer function in which removeConnection is called (this function is mentioned a bit further), to remove respective connection.line: 23-26
declaring respective headers: setting content type asstream-event
, disabling cache, declaring connection type askeep-alive
(long lasting).line: 28
waiting for request to be terminated. Also, in this case clients requests will not finish.
Nice! Now with this logic server can accept connections. But what about sending messages to make service functional? Let’s take a closer look!
I implemented a very simple Send
function which loops through all currently connected connections and sends given message:
line: 3-4
calling read lock to start reading connections list without race conditions.line: 6
formatting event message (read more about event fields here) and parsing string data to byte slice, which will be buffered.line: 8–12
trying to send message to connection. In case of error, it tells that there is some problem with it and removes it.line: 14
flushing buffered response.
And of course, forgot to mention removing connection from list logic:
Full implementation of Server can be found here.
Client Side
Moving on to client side — it’s a simpler implementation:
line: 4
creating a simple http request and atline: 5
pre-checking if no error was occurred.line: 9-11
setting up similar headers as we used on Server-side implementation.line: 13-17
creating a simple http client, executing request and again pre-checking if no error has occurred.line: 19
finally, we wait for any data to appear from created request. Atline: 21
I’m usingresp.Body.Read
instead ofresp.Body.ReadAll
function, because ReadAll function will wait until it receives some request error or EOF (end-of-file), which from requests point-of-view will tell us about received response. And in our case it’s not the case, because we want to constantly receive events from server.
Conclusion
Finishing up, I won’t mention pros and cons of SSE itself. So, pushing those things aside, what I showed is a simple example of SSE implementation with a little twist of managing connections instead of using channels.
From this point, it can be easier if some additional logic per connection is required (for example.: sending specific events to certain connected clients & etc.).
Also, as mentioned before, you can take a closer look at implementation on my Github.