Server-Sent Events

Server-Sent Events

The server pushes data to the client, there are many solutions. In addition to “polling” and WebSocket, HTML 5 also provides Server-Sent Events (hereafter referred to as SSE).

Generally speaking, the HTTP protocol can only allow the client to initiate a request to the server, and the server cannot actively push to the client. But there is a special case where the server declares to the client that the next thing to send is streaming. That is to say, what is sent is not a one-time packet, but a stream of data that will be sent continuously. At this time, the client will not close the connection and will always wait for a new data stream sent by the server. In essence, this communication is to complete a long download in the form of streaming information.

SSE uses this mechanism to push information to the browser using streaming information. It is based on the HTTP protocol and is currently supported by other browsers except IE/Edge.

Comparison with WebSocket

SSE is similar to WebSocket in that it establishes a communication channel between the browser and the server, and then the server pushes information to the browser.

Overall, WebSocket is more powerful and flexible. Because it is a full-duplex channel, it can communicate in both directions; SSE is a one-way channel, which can only be sent from the server to the browser, because streaming is essentially downloading. If the browser sends information to the server, it becomes another HTTP request.

However, SSE also has its own advantages.

  • SSE uses HTTP protocol, which is supported by existing server software. WebSocket is an independent protocol.
  • SSE is lightweight and easy to use; the WebSocket protocol is relatively complex.
  • SSE supports disconnection and reconnection by default, and WebSocket needs to implement disconnection and reconnection by itself.
  • SSE is generally only used to transmit text. Binary data needs to be encoded and transmitted. WebSocket supports the transmission of binary data by default.
  • SSE supports custom message types sent.

Therefore, both have their own characteristics and are suitable for different occasions.

Client API

EventSource object

SSE’s client-side API is deployed on an EventSource object. The following code can detect whether the browser supports SSE.

if ('EventSource' in window) {
  // ...
}

When using SSE, the browser first generates an EventSource instance to initiate a connection to the server.

var source = new EventSource(url);

The url above can be in the same domain as the current URL, or it can be cross-domain. When cross-domain, you can specify the second parameter to open the withCredentials property, indicating whether to send cookies together.

var source = new EventSource(url, { withCredentials: true });

readyState property

The readyState property of an EventSource instance, indicating the current state of the connection. This property is read-only and can take the following values.

  • 0: Equivalent to the constant EventSource.CONNECTING, indicating that the connection has not been established, or the disconnection is reconnecting.
  • 1: Equivalent to the constant EventSource.OPEN, indicating that the connection has been established and can accept data.
  • 2: Equivalent to the constant EventSource.CLOSED, indicating that the connection has been disconnected and will not be reconnected.
var source = new EventSource(url);
console.log(source.readyState);

url property

The url property of an EventSource instance returns the URL of the connection, which is read-only.

withCredentials property

The withCredentials property of the EventSource instance returns a boolean value indicating whether the current instance has withCredentials of CORS enabled. This property is read-only and defaults to false.

onopen property

Once the connection is established, the open event is fired, and a callback function can be defined in the onopen property.

source.onopen = function (event) {
  // ...
};

// another way of writing
source.addEventListener('open', function (event) {
  // ...
}, false);

onmessage property

When the client receives the data sent by the server, it will trigger the message event. You can define a callback function in the onmessage property.

source.onmessage = function (event) {
  var data = event.data;
  var origin = event.origin;
  var lastEventId = event.lastEventId;
  // handle message
};

// another way of writing
source.addEventListener('message', function (event) {
  var data = event.data;
  var origin = event.origin;
  var lastEventId = event.lastEventId;
  // handle message
}, false);

In the above code, the parameter object event has the following properties.

  • data: The data returned by the server (text format).
  • origin: The domain name part of the server URL, i.e. protocol, domain name and port, indicating the origin of the message.
  • lastEventId: The ID of the data, sent by the server. If there is no number, this property is empty.

onerror property

If a communication error occurs (such as connection interruption), the error event will be fired, and a callback function can be defined in the onerror property.

source.onerror = function (event) {
  // handle error event
};

// another way of writing
source.addEventListener('error', function (event) {
  // handle error event
}, false);

custom events

By default, data sent from the server always triggers the message event of the browser EventSource instance. Developers can also customize SSE events, in this case, the data sent back will not trigger the message event.

source.addEventListener('foo', function (event) {
  var data = event.data;
  var origin = event.origin;
  var lastEventId = event.lastEventId;
  // handle message
}, false);

In the above code, the browser listens to the foo event of SSE. How to implement the server to send the foo event, see below.

close() method

The close method is used to close the SSE connection.

source.close();

server implementation

Data Format

The SSE data sent by the server to the browser must be UTF-8 encoded text with the following HTTP header information.

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

Of the three lines above, the Content-Type on the first line must specify the MIME type event-steam.

The information sent each time consists of several messages, and each message is separated by \n\n. Each message consists of several lines, and each line is in the following format.

[field]: value\n

The field above can take four values.

  • data
  • event
  • id
  • retry

In addition, there can be lines beginning with a colon to indicate a comment. Usually, the server sends a note to the browser every once in a while, keeping the connection uninterrupted.

: This is a comment

Below is an example.

: this is a test stream\n\n

data: some text\n\n

data: another message\n
data: with two lines \n\n

data field

The data content is represented by the data field.

data:  message\n\n

If the data is very long, it can be divided into multiple lines, the last line is terminated with \n\n, and the previous lines are terminated with \n.

data: begin message\n
data: continue message\n\n

Below is an example of sending JSON data.

data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n

id field

The data identifier is represented by the id field, which is equivalent to the number of each piece of data.

id: msg1\n
data: message\n\n

The browser reads this value with the lastEventId property. Once the connection is disconnected, the browser will send an HTTP header containing a special Last-Event-ID header, and send this value back to help the server re-establish the connection. Therefore, this header can be viewed as a synchronization mechanism.

event field

The event field represents a custom event type, the default is the message event. The browser can listen for this event with addEventListener().

event: foo\n
data: a foo event\n\n

data: an unnamed event\n\n

event: bar\n
data: a bar event\n\n

The above code creates three messages. The name of the first line is foo, which triggers the browser’s foo event; the second line is unnamed, indicating the default type, which triggers the browser’s message event; the third line is bar, which triggers the browser’s message event bar event.

Below is another example.

event: userconnect
data: {"username": "bobby", "time": "02:33:48"}

event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}

event: userdisconnect
data: {"username": "bobby", "time": "02:34:23"}

event: usermessage
data: {"username": "sean", "time": "02:34:36", "text": "Bye, bobby."}

retry field

The server can use the retry field to specify the time interval for the browser to re-initiate the connection.

retry: 10000\n

Two situations will cause the browser to re-initiate the connection: one is the expiration of the time interval, and the other is the connection error due to network errors and other reasons.

Node server instance

SSE requires the server to remain connected to the browser. For different server software, the resources consumed are different. Apache server, each connection is a thread, if you want to maintain a large number of connections, it is bound to consume a lot of resources. Node uses the same thread for all connections, so the resource consumption will be much smaller, but this requires that each connection cannot contain very time-consuming operations, such as disk IO read and write.

Below is an SSE server [instance] for Node (http://cjihrig.com/blog/server-sent-events-in-node-js/).

var http = require("http");

http.createServer(function (req, res) {
  var fileName = "." + req.url;

  if (fileName === "./stream") {
    res.writeHead(200, {
      "Content-Type":"text/event-stream",
      "Cache-Control":"no-cache",
      "Connection":"keep-alive",
      "Access-Control-Allow-Origin": '*',
    });
    res.write("retry: 10000\n");
    res.write("event: connecttime\n");
    res.write("data: " + (new Date()) + "\n\n");
    res.write("data: " + (new Date()) + "\n\n");

    interval = setInterval(function () {
      res.write("data: " + (new Date()) + "\n\n");
    }, 1000);

    req.connection.addListener("close", function () {
      clearInterval(interval);
    }, false);
  }
}).listen(8844, "127.0.0.1");

Reference link