If you need to deal with situations similar to social media status updates, such as friend logins, subscription updates, status updates between users and subscribers, or alerts, then Server-Sent Events may help you
introduce
The EventSource object
The client API of SSE (Server-Sent Events) is deployed on the EventSource object, which is a network event interface pushed by the Server. An EventSource instance opens a persistent connection to the HTTP service, sends events in text/event-stream format, and remains open until asked to close.
Unlike WebSockets, server-side push is one-way. Data information is distributed one-way from server to client. This makes them an excellent choice when there is no need to send data from the client to the server in the form of a message.
Once the connection is opened, incoming messages from the server are distributed to your code as events. If there is an event field in the received message, the event is fired with the same value as the event field. If no event field exists, a generic event is emitted.
View EventSource details
practice
Let’s start with a simple service that allows you to subscribe to live updates
//server.js
const express = require("express");
const app = express();
app.use(express.static(__dirname));
app.listen(3000, () => {
console.log("server open in 3000");
});
Copy the code
Create index.html in the current folder and act as the client (i.e. the user). We set a button to turn the service on and off (in real application development, it can be turned on by default).
<button onclick="closeServerPush()"> </button> </h3> <ul id="box"></ul>Copy the code
Define the binding event, create the instance using the EventSource() constructor, which takes the interface address, and define the related functions. Onmessage is a generic event acceptance function. If the stream returned by the back end does not specify an event, it will be accepted through this function
//index.html let es const openServerPush = () => { es = new EventSource("/sse"); Es.onopen = () => {console.log(" enabled... ") ); }; Es. Onmessage = (e, me) => {console.log(" default push :" + e.data); }; es.onerror = (err) => { console.log(err); }; }; const closeServerPush = () => { if (es) { es.close(); }};Copy the code
The backend implements the get interface and sets the response header “content-Type “: “text/event-stream; charset=utf-8”
//server.js
app.get("/sse", (req, res) => {});
Copy the code
Other HTTP methods do not work; the EventSource is initiated as a GET request
The principle of
The EventSource transmits data as a stream. We all know that in Node, although the req and RES callback functions are inherited streams, they are not the same stream. Generally speaking, we need to create a stream between the server and the client, and then write data to the server. The stream.Transform class in Node implements Readable and Writable interfaces, and has a process function _transform() in the middle. The data can be modified to suit our needs
//server.js
const stream = require("stream");
let sse
app.get("/sse", (req, res) => {
res.writeHead(200, {
"Content-Type": "text/event-stream; charset=utf-8"
});
sse = new stream.Transform({ objectMode: true });
sse.pipe(res);
});
Copy the code
If the res state is not 200, or the res content-Type is not text/event-stream, the connection fails
Instances of a stream switch to objectMode using the objectMode option when creating the stream.
All streams created by the Node.js API operate only on strings and Buffer (or Uint8Array) objects. However, implementations of streams can use other types of JavaScript values (besides NULL, which has special use in streams). Such flows are said to operate in “object mode”.
Create an instance of the Transform flow globally so that other interfaces can call it, but you don’t have to.
In this case, Sse.pipe (res) is to set up the pipe to pass data and wait for SSE.write to write data to pass to the client
Before writing data, we should first understand the format of text/event-stream. It is recommended to take a look at the HTML Standard. If you don’t understand it, you can read this article.
Each event consists of a type and data, and each event can have an optional identifier. The contents of different events are separated by empty lines (” \r\n “) containing only carriage returns and newlines. The data for each event may consist of multiple lines.
First clear the incoming data, we need all of the fields are not than preach, because is the flow, the client read each paragraph flow, break up the corresponding field is returned to the event functions, there are things processing, not empty, but when we were in the use of or into basic fields as well, guarantee the function integrity, below is the pseudo data flow
Id: data flow ID Event: indicates the event to be triggered. Retry: indicates the number of milliseconds after the connection is disconnected. Data: indicates the id of the data flow. Data flow ID Event: indicates the event to be triggered. Retry: Indicates the number of milliseconds to be reconnected after disconnection. Data: indicates dataCopy the code
This is where _transform() is used to process the data so that the stream written to it is rendered in the format above and can be recognized by the client
function dataString(data) {
if (typeof data === 'object')
return dataString(JSON.stringify(data));
return data.split(/\r\n|\r|\n/).map(line => `data: ${line}\n`).join('');
}
sse._transform = (message, encoding, callback) => {
if (message.comment) sse.push(`: ${message.comment}\n`);
if (message.event) sse.push(`event: ${message.event}\n`);
if (message.id) sse.push(`id: ${message.id}\n`);
if (message.retry) sse.push(`retry: ${message.retry}\n`);
if (message.data) sse.push(dataString(message.data));
sse.push("\n");
callback();
};
sse.write(':ok\n\n');
Copy the code
In the code above, message is the object we passed in by calling sse.write(). We can pass data in object format because we turned on object mode when we created the instance
Message.com ment is used to add comments. If a message has only comments, the event will not be triggered
The purpose of sse.push(“\n”) is to add blank lines at the end of each stream, otherwise the client will not be able to determine whether the stream unit is terminated and will not fire the corresponding event.
Finally, let’s execute sse.write({comment: ‘OK ‘}) at the end of the request; In some browsers the onOpen event is not triggered after the creation of the EventSource instance, so you can do some compatibility processing on the back end to ensure consistency.
When we click on the Open button, we can see the console output, indicating that the connection has been successfully established,
Let’s go back to the implementation of the subscription dynamic real-time update function, now we need a function to trigger the update dynamic, simple implementation, in the current folder new up. HTML, the main content is as follows
HTML <input type="text" id="content" /> <button onclick="releaseDynamics()"> </button> <script> const content = document.getElementById("content"); const releaseDynamics = () => { fetch("http://localhost:3000/pushDynamics", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ content: Content.value }), }) .then((res) => { return res.json(); }) .then((res) => { console.log(res); }); }; </script>Copy the code
The event processing
Add corresponding interfaces on the back-end
const bodyParser = require("body-parser"); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); . let contentId = 0 app.post("/pushDynamics", (req, res) => { const { content } = req.body; Const message = {data: {name: "blanca", content}, event: "dynamicUpdate", // ++contentId, retry: 10000, // Tell the client to retry the connection after 10 seconds if disconnected}; sse? .write(message); Res.json ({code: 0, data: "publish successfully"}).end(); });Copy the code
You can see that we defined message.event as dynamicUpdate so we need to bind the corresponding event on the client side,
//index.html const box = document.querySelector("#box"); // ul element const openServerPush = () => {es = new EventSource("/sse"); es.addEventListener("dynamicUpdate", (e) => { const li = document.createElement("li"); let user = JSON.parse(e.data) li.innerText = `${user.name}: ${user.content}` box.appendChild(li); }); Es.onopen = () => {console.log(" enabled... ") ); }; Es. Onmessage = (e, me) => {console.log(" default push :" + e.data); }; Es. Onerror = (e) => {console.log(" server push error ", e); }; }Copy the code
Now you can try to start the service node server.js. Click on the index.html page to start the function, enter the content in the up. HTML page, and click on the Publish button
If no event is specified, the default universal es. Onmessage will be enabled. Let’s try modifying the code.
//server.js app.post("/pushDynamics", (req, res) => { const { content } = req.body; sse? .write({ data: content, }); Res.json ({code: 0, data: "publish successfully"}).end(); }Copy the code
Reconnection is
If the connection is disconnected for some reason, the client will re-initiate the connection after a certain period of time. Now let’s simulate that by disconnecting the interface and restoring the previous code
//index.html <button onclick="cutServerPush()"> </button> <script> const cutServerPush = () => { fetch("/cutLink", { method: "POST" }) .then((res) => { return res.json(); }) .then((res) => { console.log(res); }); }; </script>Copy the code
/ / server. Js app. Post ("/cutLink ", (the req, res) = > {sse. End (res). Json ({code: 0, data: "cut off successful}"). The end (); }); app.post("/pushDynamics", (req, res) => { const { content } = req.body; Const message = {data: {name: "blanca", content}, event: "dynamicUpdate", // ++contentId, retry: 10000, // Tell the client to retry the connection after 10 seconds if disconnected}; sse? .write(message); Res.json ({code: 0, data: "publish successfully"}).end(); });Copy the code
Let’s refresh the page, click the Open button, and publish the update in the up. HTML page. Then click the disconnect button in the index.html page to open the console and view the network changes. And the ID of the Last data in the request header last-event-id
In order to ensure data integrity and correct ID, it is necessary to process the refield at the back end. The reconnected data ID can be connected to the previous ID
app.get("/sse",() => {
contentId = req.headers["last-event-id"] || 0
...
})
Copy the code
Keep the long connection?
Some of the referenced articles say that in order to maintain a long connection, you need to add the following code
/ / send comments keep long connection setInterval () = > {res. Write (' : \ n \ n '); }, 15000);Copy the code
However, in my tests, even without the above code, the browser still received the data after a long time, and finally found the reason in the specification: the compatible old agent will disconnect after timeout
The source code
In fact, there is an existing NPM package that implements this function, SseStream, and this article is written according to its source code. Let’s take a look at the advantages of this NPM package source code
Click here to view the source code
req.socket.setKeepAlive(true); // Enable the keepalive function req.socket.setnodelay (true); // When a TCP connection is created, it enables the Nagle algorithm. Nagle's algorithm delays data before it is sent across the network. It tries to optimize throughput at the expense of latency. req.socket.setTimeout(0); // Disable existing idle timeoutsCopy the code
Click here to view the source code
destination.writeHead(200, { 'Content-Type': 'text/event-stream; Charset = UTF-8 ', 'transfer-encoding ': 'identity', // used to refer to itself (e.g. uncompressed and modified). This flag is always acceptable unless specified otherwise. Chunk 'cache-control ': 'no-cache', Connection: 'keep-alive',}); destination.flushHeaders(); // Refresh the response headers. In some cases, history headers may be used, causing the connection to failCopy the code
The specification for cache-control caching actually recommends setting no-store
SSE has some problems and deficiencies
- SSE only supports one-way real-time communication from server to client
- Server-sent Events is not supported in IE
3. SSE will remain connection status, if the server not push data within a long time, the SSE is equal to the waste of the resources, we can set up an event, when the service side think a period of time in the future will not push information, push a suspension of service events, and the next reconnection, close the connection after client receives, Enable the scheduled reconnection time
//index.html <button onclick="pauseServerPush()"> </button> <script> const pauseServerPush = () => { fetch("/pauseLink", { method: "POST" }) .then((res) => { return res.json(); }) .then((res) => { console.log(res); }); }; Let timeout const openServerPush = () => {clearTimeout(timeout) es.addEventListener("pause", (e) => { es.close(); timeout = setTimeout(() => { openServerPush(); }, +e.data - Date.now()); }); } </script>Copy the code
//server.js app.post("/pauseLink", (req, res) => { sse? .write({event: "pause", ID: ++contentId, data: date.now () + 10*1000 + ", // specify the retransmission time,10s}); Res.json ({code: 0, data: "pause successful"}).end(); });Copy the code
Write in the last
The practice of the code or logic to have a lot of errors, such as the back-end sse global leads to only one connection in a service, because the second connection came in will replace before, so it needs to be done to deal with, can create different instances of sse for user authentication, and stored in an object or an array, such implementation can use multiple users, The actual business requirements certainly have a lot to change as well.
If there are cross-domains, you need to perform other cross-domain processing, and you can specify the second parameter, the withCredentials attribute, to indicate whether cookies are sent together.
new EventSource(url, { withCredentials: true });
Copy the code
Here’s a repository of sample code
reference
- HTML Standard
- EventSource
- JS Real-time communication three axes series: eventSource
- Understand buffers and streams in nodes
- Server – Sent Events tutorial
- Protocol details and implementation of Server-sent Events
- SSE technical details: the use of HTTP server data push application technology
- nodejs.cn/api/