57

So I understand the concept of server-sent events (EventSource):

  • A client connects to an endpoint via EventSource
  • Client just listens to messages sent from the endpoint

The thing I'm confused about is how it works on the server. I've had a look at different examples, but the one that comes to mind is Mozilla's: http://hacks.mozilla.org/2011/06/a-wall-powered-by-eventsource-and-server-sent-events/

Now this may be just a bad example, but it kinda makes sense how the server side would work, as I understand it:

  • Something changes in a datastore, such as a database
  • A server-side script polls the datastore every Nth second
  • If the polling script notices a change, a server-sent event is fired to the clients

Does that make sense? Is that really how it works from a barebones perspective?

Ahmed Nuaman
  • 12,662
  • 15
  • 55
  • 87

2 Answers2

81

The HTML5 doctor site has a great write-up on server-sent events, but I'll try to provide a (reasonably) short summary here as well.

Server-sent events are, at its core, a long running http connection, a special mime type (text/event-stream) and a user agent that provides the EventSource API. Together, these make the foundation of a unidirectional connection between a server and a client, where messages can be sent from server to client.

On the server side, it's rather simple. All you really need to do is set the following http headers:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

Be sure to respond with the code 200 and not 204 or any other code, as this will cause compliant user agents to disconnect. Also, make sure to not end the connection on the server side. You are now free to start pushing messages down that connection. In nodejs (using express), this might look something like the following:

app.get("/my-stream", function(req, res) {
    res.status(200)
       .set({ "content-type"  : "text/event-stream"
            , "cache-control" : "no-cache"
            , "connection"    : "keep-alive"
            })

    res.write("data: Hello, world!\n\n")
})

On the client, you just use the EventSource API, as you noted:

var source = new EventSource("/my-stream")
source.addEventListener("message", function(message) {
    console.log(message.data)
})

And that's it, basically.

Now, in practice, what actually happens here is that the connection is kept alive by the server and the client by means of a mutual contract. The server will keep the connection alive for as long as it sees fit. Should it want to, it may terminate the connection and respond with a 204 No Content next time the client tries to connect. This will cause the client to stop trying to reconnect. I'm not sure if there's a way to end the connection in a way that the client is told not to reconnect at all, thereby skipping the client trying to reconnect once.

As mentioned client will keep the connection alive as well, and try to reconnect if it is dropped. The algorithm to reconnect is specified in the spec, and is fairly straight forward.

One super important bit that I've so far barely touched on however is the mime type. The mime type defines the format of the message coming down the connecting. Note however that it doesn't dictate the format of the contents of the messages, just the structure of the messages themselves. The mime type is extremely straight forward. Messages are essentially key/value pairs of information. The key must be one of a predefined set:

  • id - the id of the message
  • data - the actual data
  • event - the event type
  • retry - milleseconds the user agent should wait before retrying a failed connection

Any other keys should be ignored. Messages are then delimited by the use of two newline characters: \n\n

The following is a valid message: (last new line characters added for verbosity)

data: Hello, world!
\n

The client will see this as: Hello, world!.

As is this:

data: Hello,
data: world!
\n

The client will see this as: Hello,\nworld!.

That pretty much sums up what server-sent events are: a long running non-cached http connection, a mime type and a simple javascript API.

For more information, I strongly suggest reading the specification. It's small and describes things very well (although the requirements of the server side could possibly be summarized a bit better.) I highly suggest reading it for the expected behavior with certain http status codes, for instance.

Nawaz
  • 353,942
  • 115
  • 666
  • 851
Marcus Stade
  • 4,724
  • 3
  • 33
  • 54
  • 4
    This is great for detail about the client and how the connection works, but if the event needs to be sent for a database change I assume there is no option but to constantly poll the database on the server? I think this is what the op was asking? Is there a more efficient solution server-side - obviously it wouldn't take many connections on a small business server to start slowing things down if there are loops constantly checking for db changes. – benedict_w Sep 17 '12 at 08:10
  • 2
    The client just receives a message whenever the server decides to send one, it never polls anything. The server is free to figure out on its own when and why to send a message. That may be implemented server side using polling or whatever technique fits the bill. Connections on the server could be pooled and a message could be sent down each connection, giving you a sort of broadcast functionality. Thus, the server could be the single connection to a DB, but broadcast to tons of clients connected to the server. I think the question was more general than that though. – Marcus Stade Sep 17 '12 at 08:53
  • 4
    That retry: milliseconds bit - genius! Saved my skin! – rob_james Sep 03 '13 at 10:12
  • Ok great explanation but i still dont understand how you would update the content. are you saying that you have to update the server file each time you want to update the stream? if i want to update a weather page about rain accumulation. every 10mins the rain accumulates. it may rain hard for 20 mins then slow down to a drizzle for another 20mins so if in the first 10mins the rain level is 1 inch, then in the next 10mins its 2.7 inches, then slows down for 10mins and goes to 3 inches. where would the server receive that info from? we wont know the exact amount until it happens – user2585548 Feb 20 '17 at 00:46
  • About closing a connection: If I want to prevent the browser from reconnecting, I just send a specific server event (e.g. event: close) and listen on the client for that, then call the close method. – Simon Jul 07 '21 at 09:01
  • **Update**: If your web server is using HTTP/2, the Connection header is not allowed, and may even prevent some browsers from loading the page containing the request (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection). If you want to keep your SSE connection open, it is best to send an empty data message from the server every 30 seconds or so while no other data is available. e.g. `res.write("data: \n\n")` – Tristan Bailey Jul 08 '21 at 04:59
  • @MarcusStade I have the same question as benedict_w. I would love to see an example of how you could use server-sent events without the server constantly polling the database. – Jar Feb 01 '22 at 01:48
  • @Jar poll a message queue instead, when the message you're looking for becomes available hit the db – sf8193 Sep 18 '22 at 22:52
6

You also need to make sure to call res.flushHeaders(), otherwise Node.js won't send the HTTP headers until you call res.end(). See this tutorial for a complete example.

vkarpov15
  • 3,614
  • 24
  • 21
  • The tutorial talks about _response_ `flushHeaders` not request `flushHeaders`. You then (making the same mistake as the tutorial) link to the Node.js _request_ `flushHeaders`. Here is the correct link: https://nodejs.org/api/http.html#responseflushheaders – icc97 Aug 14 '23 at 05:52