Server-Sent Events

Introduction

There are many solutions for the server to push data to the client. In addition to "polling" and WebSocket, HTML 5 also provides Server-Sent Events (hereinafter referred to as SSE).

Generally speaking, the HTTP protocol can only initiate requests from the client to the server, and the server cannot actively push to the client. But there is a special case in which the server declares to the client that the next thing to be sent is streaming. In other words, what is sent is not a one-time data packet, but a data stream, which will be sent continuously. At this time, the client will not close the connection, and will always wait for the new data stream sent by the server. Essentially, this kind of 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 currently supports all browsers except IE/Edge.

Comparison with WebSocket

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

In general, WebSocket is more powerful and flexible. Because it is a full-duplex channel, two-way communication is possible; 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 simple to use; the WebSocket protocol is relatively complex.
  • SSE supports disconnected reconnection by default, and WebSocket needs to implement disconnected reconnect 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

The client API of SSE is deployed on the 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 above url 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 and turn on the withCredentials property to indicate whether to send cookies together.

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

readyState property

The readyState property of the EventSource instance indicates the current state of the connection. This attribute 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 connection is disconnected and reconnecting.
  • 1: Equivalent to the constant EventSource.OPEN, indicating that the connection has been established and data can be received.
  • 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 attribute

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

withCredentials attribute

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

onopen attribute

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

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

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

onmessage attribute

When the client receives the data from the server, it will trigger the message event, and the callback function can be defined 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 attributes.

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

onerror attribute

If a communication error occurs (for example, the connection is interrupted), the error event will be triggered, and the 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 Event

By default, the data sent by the server always triggers the message event of the browser's 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, please 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

Among the above three lines, the first line of Content-Type must specify the MIME type as event-steam.

Each message sent is composed of several message, and each message is separated by \n\n. Each message internally consists of several lines, and each line has the following format.

[field]: value\n

The above field can take four values.

  • data
  • event
  • id
  • retry

In addition, there can be a line starting with a colon to indicate a comment. Usually, the server will send a note to the browser at regular intervals to keep 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 ends with \n\n, and the previous lines all end with \n.

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

The following 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, which contains a special Last-Event-ID header information, and send this value back to help the server re-establish the connection. Therefore, this header information can be regarded as a synchronization mechanism.

event field

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

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 pieces of information. The name of the first item is foo, which triggers the browser's foo event; the second item is unnamed, which indicates the default type, and triggers the browser's message event; the third item is bar, which triggers the browser The bar event.

Here 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 maintain a connection with 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 time-consuming operations, such as disk IO read and write.

The following is Node's SSE server example.

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");