
The Use Case
For one of the recent projects I had the necessity to send data from the server. However I did not know if the data is available yet, so the client may have to wait; while an image comparison and identification workflow is happening in the background.
This is one of the classic use cases that you encounter all the time. Even though WebSockets is the word of the day today, however some age old techniques may still be the more preferred approach for some of these applications. WebSockets is great when we need bi-directional communication between your browser (application) and server – but what about a simple use case where you will need to only send data from the server?
Recipes
For messaging from server to client, we can resort to the following two options:
- Long/ Short Poll
- Server Sent Events (SSE)
The first option above is a client side pull and the second one is basically a server side push. If we opt for polling, we from the client side will have to keep on calling the server at a pre-determined rate (when timer expires). This is normally done by an ajax call – that is set to trigger every time a javascript timer expires. However, what this means is that we are opening up a connection to the server every time we call it. That is where SSE steps in. SSE is managed by the browser which handles connectivity; and after a connection is opened to the server, a call is initiated approximately every three seconds by default.
For my use case, I felt that SSE is better than polling as it makes for a simpler implementation and I can avoid managing my own server connections. SSE also gives me the option to configure from the server when I want the server to call me back again.
Sample Implementation
Even though my project backend services were built using Java, Spring Boot to be more precise, as a micro-service, for convenience here, I am going to build a dummy event source and will use Python with Flask for my service. Just to provide a full disclosure, I am using Python version 3.7.2 and Flask version 1.1.1. My server has a method that returns the current system time, that is the one we will use for our test.
from flask import Flask from flask import Response from datetime import datetime app = Flask(__name__) curr_id = 0 # Home @app.route("/") def home_page(): return "Event Source Test!", 200, {"Content-Type": "text/plain; charset=utf-8"}
Here we defined a default route. Next we define our server time method that will return current server time.
# Event Source feeder @app.route("/servertime") def server_time(): now = datetime.now() dt_fmt = now.strftime("%d/%m/%Y %H:%M:%S") # Event Response global curr_id curr_id = evt_id = curr_id + 1 evt_nm = "TestEvent" evt_dt = "Server Time now is: " + dt_fmt rtr_fr = 10000 # 10 second retry # Format str_fmt = "id: {0}\nevent: {1}\nretry: {2}\ndata: {3}\n\n".format(evt_id, evt_nm, rtr_fr, evt_dt) resp = Response(str_fmt) resp.headers["Content-Type"] = "text/event-stream" return resp
There are quite a few things of interest here. id which is returning a sequential value to the client plays a very important role in case of pre-mature connection termination between server and client. If the connection terminates, browser will automatically reestablish the connection and will also send the last received ID from the server. In this case server can send all data that has been generated since the ID sent. This provides a way for the client to receive all messages even if there is an issue with client connection.
Event, which is a marker that can be used to segregate message types. We can pass any value to this and this will only be received by the corresponding subscriber from client perspective. An example where event can be used is where we are sending few different types of messages, like ‘record added’, ‘record updated’ or ‘record deleted’. This field is optional and defaults to ‘message’ as an event type.
The next thing of interest is retry that is being sent from the server. This is also optional and defaults to about 3 seconds. This number defines in milli-seconds how much time client should wait before calling the server again.
SSE only supports UTF-8 messages, and the mime type for request has to be text/event-stream. This means if we want to send non UTF-8 messages we would be better with implementing a WebSocket.
We also want to prevent caching. So, we add flask code to set headers for not caching as provided below.
app.after_request def add_nocache(resp): resp.headers["Last-Modified"] = datetime.now() resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0" resp.headers["Pragma"] = "no-cache" resp.headers["Expires"] = "-1" resp.headers["Access-Control-Allow-Origin"] = "*" return resp if __name__ == "__main__": app.run(debug=True)
Now that we are done with our server side code, let us work on client side. We will write a small snippet that has two buttons; one to start a SSE channel and the other to stop this channel. We can also terminate the connection from server, but in this case we will let the client handle it. Without much ado, here is the code for client side SSE.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Event Consumer</title> </head> <body> <input type="button" name="startwait" value="Start Event Capture" onclick="startWait();"> <input type="button" name="stopwait" value="Stop Event Capture" onclick="stopWait();"> <script language="javascript"> var evtSource = new EventSource('http://127.0.0.1:5000/servertime'); function startWait() { console.log(evtSource); evtSource.addEventListener('message', function(e) { console.log(e.data); }, false); evtSource.addEventListener('TestEvent', function(e) { console.log(e.data); }, false); evtSource.addEventListener('open', function(e) { console.log('Connection Opened'); }, false); evtSource.addEventListener('error', function(e) { if (e.readyState == EventSource.CLOSED) { console.log('Connection Closed'); } }, false); }; function stopWait() { evtSource.close(); }; </script> </body> </html>
The first thing to note about the code above is EventSource. Ideally we should check the availability of EventSource support before we invoke it, but in this case we are not checking for errors.
We are also subscribing to TestEvent on line 21, this is what is triggered for server responses. We also have a subscriber for default messages, but in this case it would not be triggered as we are not sending any default messages.
That’s it for today. Hope you find this post useful.