Good morning everyone! Did I wake you up? Sorry, I didn’t mean to. Or maybe, just this time, I did! Because we’re talking about push notifications and how they’re done right – I present to you: Server-sent events!
What’s the point?
You know when you get these little red icons in your social media, telling you someone sent you a message? They just pop up there. Without you doing anything. Very creepy isn’t it? How does the site actually know where you are, to send you this message?
If you know some web development, or more precisely – a bit about how the Internet works, you’d know, that the server can’t simply send you messages like that. It needs you to ask for it first. If it tries, a bunch of firewalls would block it way before it reaches your computer.
But the Web has developed quite fast in this particular direction. Perhaps in the early 2000s, or even before that, people were already trying to do real-time communication which needed the server to initiate the communication from time to time. Not only the user.
So we, the developers, needed a way to push information in the client’s throat, even if they scream like a little child who is made to eat broccoli (Mmmm, broccoli).
Heartbeat
The poor programmer of the early 2000s had no other choice but write some javascript, which would poll the server for updates once in a while. Every half a second, let’s say.
Can you imagine what happens to the network, when you do this? Do you reckon how many useless requests the server needed to process? Pure madness.
But there was no other way to do it, so there’s that. Until some people decided to invent a better way. So the awesomely named WHATWG came out with a draft of Server-sent Events (SSE) in 2006! Yay!
What is a Server-sent Event anyway?
From a technological point of view, the event part of this technology is the most boring one. It’s just what follows the magic. A simple event, which processes the information sent from the server on the client side.
The magic comes from the ability of the server to randomly send data to the client. This is very different from normal client-server conversation, which is always initiated by the client, which sends a request and finished by the server, by sending a response to this request.
To achieve this magic, both the client and the server need to work together. If they don’t, we’re having the same problems described in the beginning. Therefore, the client initiates this magic.
Client’s contribution
Once the client has been loaded, it needs to create a request to the server. But this is not a simple, normal, boring request. It doesn’t even ask for new information. It is hardly a request, but a permission, actually.
The client tells the server: “Hey, you know what, I don’t need to remind you I need new info. Just send it directly like this to me, ok?” It does that by sending a request to a specific end point on the server and sets an event listener, which is going to process all the received responses to this request.
That’s right, all of them.
What the server does
The server notes which client initiated this process and what information it needs. Then, when it becomes available, it sends a response to the client. Usually this is going to be the end of the conversation with the client for a while, until the client asks for something new.
This time, the server just keeps the conversation open. So does the client. This way, when new info needs to be sent, the server just prepares a second response to the same request. And then another one, and another one… (Insert meme here, oh wait, copyright and stuff).
The connection simply never ends.
How do you do this?
This is a matter of requesting such connection from the client and having support for SSE on the server. Let’s start with the server, shall we?
How to stop the SSE connection from ending?
Well the most important part is, to set the response mimetype to 'text/event-stream'
. Since we’re talking about Python (that’s what the article’s title says), I’ll show you how this is done in Flask. You just need to create a Response and return it like this:
@app.route('sse-stream') def stream(): return Response(dataProvider(), mimetype="text/event-stream")
That’s easy. It makes a REST like route and creates a response for the caller of this route. Now let’s find out what the dataProvider
is.
The response we created here will wait for all the information that needs to be sent, before closing the connection. But it will also take the returned value from the dataProvider and send it immediately. How does it do that? Well it expects the dataProvider to be a generator.
What’s a Generator?
It’s basically an iterator. But functional style. It spits out a value then waits to get the control over the flow and does it again. In Python, it is simply a syntactic sugar for creating an entire iterator class.
And it makes it easier to use! You don’t need to create an object, check if there’s more items to come and then get the next one. You just call the generator function. It automatically spits the next value, until there’s no more. Then it finishes.
Let’s see how to define one.
Creating a generator
Many languages nowadays have support for creating a generator. Anywhere you see the yield
keyword, you might be dealing with a generator. This is how you define one in python. Put yield
anywhere in your function and it is already a generator. Here’s one we can use for our server-sent events:
def dataProvider(): rep = 0 while rep < 10: yield "Hello Dolly!" rep += 1
If we use this one for our SSEs, the server would send 10 responses with the string "Hello Dolly!"
to our client.
The client wouldn’t really be able to use these responses though. Or even ask for them yet. Unwritten code doesn’t work. So the next thing I need to tell you is, how to initiate such a magical request.
How to start a Server-Sent Event connection?
All you need to do, to initiate a server-sent event connection, is to send a request to the server, which would trigger the /sse-stream
resource we defined earlier, and then wait for events from it.
This is pretty simple, since some good people have provided us with a JavaScript API for it. We simply need to create an EventStream
object, and attach some event handlers on it.
function sse() { const eventSource = new EventSource('/sse-stream'); eventSource.addEventListener('onmessage', function(event) { console.log(event.data); }); return eventSource; } const eventSource = sse();
As you can see, the event source gets the name of the server resource, which creates the server-sent event loop as a parameter to its constructor. And that’s all it needs, actually. Once created, you can attach any event to it. Just make sure, the server actually sends such events.
How to send different events?
This is one very simple task, actually. All the magic happens in the EventSource
on the client. It reads the data of the message, parses parts of it, and creates an event with a given name, depending on the data sent from the server.
Therefore you and I need only to send the data in the correct format.
But what’s this format?
It isn’t really sophisticated. It is a string representation of key-value pairs. There’s the data
key, which is followed by whatever useful payload we decide to send. Then there’s the event
key, which is used, to specify the name of the event. And that’s how you send multiple different events.
It is good to know, that all the payload, including the metadata we just discussed, should be a single string. Now we can modify the dataProvider, to create a decent message payload:
def dataProvider(): rep = 0 while rep < 10: yield "event: ping\ndata: 'Hello Dolly!'\n\n" rep += 1
Notice how each of the keys is on a new line? And also how the message ends in a double new line? Pay attention to these. They define the structure of the response.
And there you have it!
This is it! It’s not really a lot. I just needed to explain a few things.
Next time you want to build a notification system, you can use this technology. It offers you lightweight one-way communication channel from the server to the client. No need for websockets or heartbeat. Just create an EventSource
on the client and an endpoint on the server, with an always ongoing response. The rest is taken care of by the infrastructure.
I hope this was helpful to you. If you like web programming, you can have a look at some of these articles (which are part of a series):
- React Router and Creating a User Space
- State Management and Bugfixing – Learning React
- Modern CSS Techniques | Entering the 21-st Century by Learning React
Or something about system administration of your project:
- The Harbour, the Stevedores, and Docker – a story about Containers – this one doesn’t even have a single line of code, but it is fun, I promise.
- Git Submodules Fun – Software Project Structuring
- I don’t like dual boot machines anymore, but I still have one – this one is about compromises
Have fun!