The Server-Sent Events (SSE) API is a standard that proposes a mechanism for servers to push content to the client. We got a taste of its server push capabilities in the Receive Updates from the Server Using the EventSource article. In that same article, we saw how to set the default timeout value from the server. As we’ll see in today’s follow-up article, it’s the key to keeping a connection open so that a series of data transmissions may be communicated to the client without it attempting to reconnect after each.
Keeping the Good Times Rolling
In order to stream chunks of data to the client without it timing out, a loop must be employed. It can either be set to deliver a predetermined number of packets or simply to execute until a given condition occurs such as the client issuing an EventSource.close() command. The following PHP script sends the time to the client every second. Even if the client does not tell the script to close the connection, it will only send ten updates as defined in the for loop.
When using a loop, don’t forget to call ob_end_flush() as well as flush() as both are required to transmit the data before the next loop literation.
<?php for ($i=1; $i<=10; $i++) { // Headers must be processed line by line. header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); // Set data line print "retry: 10000" . PHP_EOL; print "Event: server-time" . PHP_EOL; print "data: server time: " . date( 'G:H:s', time() ) . PHP_EOL; print PHP_EOL; ob_end_flush(); // Strange behaviour, will not work flush(); // Unless both are called ! // Wait one second. sleep(1); }
Nothing needs to change in the client script because the EventSource’s onmessage() event will fire for every server update. The only problem is that after the server ceases to send data, the idle time will eventually exceed the timeout limit and cause the client to attempt to reestablish the connection, ultimately starting the whole process over again, until the The EventSource’s close() method is invoked, as evidenced by the following transcript.
server time: 16:16:54
server time: 16:16:55
server time: 16:16:56
server time: 16:16:57
server time: 16:16:58
server time: 16:16:59
server time: 16:16:00
server time: 16:16:01
Transmission ends. (logging should stop here!)
(the EventSource’s readyState is set to EventSource.CONNECTING and the onerror event is fired)
Reconnecting…
server time: 16:16:06
server time: 16:16:07
server time: 16:16:08
Clock Stopped.
Cancelling an Event Stream from the Server
The client needs to know when then server transmissions have completed so as to not confuse a lack of transmissions with an accidental severing of communication channels. According to the official spec:
Clients will reconnect if the connection is closed; a client can be told to stop reconnecting using the HTTP 204 No Content response code.
In fact, any HTTP status other than “200 OK” (e.g. 404 Not Found) will close the connection. Unfortunately, at the time of this writing, there is no way to capture the HTTP status via the EventSource or Event object so using an HTTP status to signal the end of communications is a moot point.
Unless you don’t mind turning off reconnecting at your end by trapping the EventSource.CONNECTING readyState and stopping your script there no matter what the underlying condition, you’ll have to deviate from the W3’s recommendation here and introduce your own signal to terminate the server push. It just so happens that it’s quite easy to do that on text-based protocols like this one. Here is an updated PHP script that now sends a special message of “data: Transmission ends.” after 10 iterations.
<?php for ($i=1; $i<=11; $i++) { // Headers must be processed line by line. header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); if ($i<=10) { print "retry: 10000" . PHP_EOL; print "Event: server-time" . PHP_EOL; print "data: server time: " . date( 'G:H:s', time() ) . PHP_EOL; } else { print "data: Transmission ends." . PHP_EOL; } print PHP_EOL; ob_end_flush(); // Strange behaviour, will not work flush(); // Unless both are called ! // Wait one second. sleep(1); }
On the client-side, the EventSource.onmessage() event checks for the terminate signal and calls close() to end the session.
es.onmessage = function(event) { result.innerHTML += event.data + '<br>'; if (event.data == 'Transmission ends.') { this.close(); } };
Conclusion
Although there is still some important functionality missing from Server-Sent Events as they are currently implemented in browsers, there is every reason to be hopeful that they will continue to evolve with each browser update.