The data is not real-time enough: try a long connection?
The data is not real-time enough: try a long connection?
background
In specific scenarios, we often need to obtain the latest data in real time, such as obtaining news push or announcements, chat messages, real-time logs and academic information, etc., all of which have high requirements for real-time data. Faced with such scenarios, The most commonly used one may be polling, but in addition to polling, there are also persistent connection (Websocket) and server push (SSE) solutions to choose from.
polling
Polling is to use the method of circular http request to obtain the latest data through repeated interface requests.
Short polling (polling)
Short polling may be the most commonly used method for refreshing data in real time. When we talk about polling schemes, most of them refer to short polling. Just add a timer or useRequest to configure the polling parameters. The principle is very simple, as shown in the figure below. If it is http1.1 and above, TCP connections can be reused. Of course, http1.0 and below can also be used, but the consumption will be more . The characteristic of short polling is that the interface request will return immediately, and each request can be understood as a new request.
Pros and cons of short polling
The biggest advantage of short polling is that it is simple. The front end sets the time interval and requests data regularly, while the server only needs to query the data synchronously and return it, but the disadvantages are also obvious:
- Too many useless requests: As can be seen from the figure below, there must be a request sent every fixed time, and each time the interface may return the same data or return an empty result, the server will repeatedly query the database, and the front end will repeatedly re-render
- The real-time performance is uncontrollable. If the data is updated, but the polling request has just finished a round, the data will not be updated within the polling interval.
long polling
After reading the above introduction about short polling, we know that polling has two main defects: one is too many useless requests, and the other is uncontrollable real-time data. In order to solve these two problems, there is a further long polling scheme.
In the above figure, after the client initiates the request, the server finds that there is no new data. At this time, the server does not return the request immediately, but suspends the request. After waiting for a period of time (usually 30s or 60s, set A timeout return is mainly to consider that the long-term no-data connection occupation will be disconnected by the gateway or a certain layer of middleware or even by the operator), if it is found that there is still no data update, an empty result will be returned to the client. After the client receives the reply from the server, it immediately sends a new request to the server again. This time the server also waited for a period of time after receiving the request from the client. Fortunately, the data on the server was updated this time, and the server returned the latest data to the client. After getting the result, the client sends the next request again, and so on.
Pros and cons of long polling
Long polling perfectly solves the problem of short polling. First of all, the server does not return data to the client without data update, so it avoids a large number of repeated requests from the client. In addition, the client sends the next request immediately after receiving the return from the server, which ensures better data real-time performance. However, long polling also has disadvantages:
- 服务端资源大量消耗: 服务端会一直hold住客户端的请求,这部分请求会占用服务器的资源。对于某些语言来说,每一个HTTP连接都是一个独立的线程,过多的HTTP连接会消耗掉服务端的内存资源。
- 难以处理数据更新频繁的情况: 如果数据更新频繁,会有大量的连接创建和重建过程,这部分消耗是很大的。虽然HTTP有TCP连接复用,但每次拿到数据后客户端都需要重新请求,因此相对于WebSocket和SSE它多了一个发送新请求的阶段,对实时性和性能还是有影响的。
从上面的描述来看,长轮询的次数和时延似乎可以更少,那是不是长轮询更好呢?其实不是的,这个两种轮询方式都有优劣势和适合的场景。
短轮询 ,长轮询怎么选?
长 轮询多用于操作频繁,点对点的通讯,而且连接数不能太多情况,每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
而像WEB网站的http服务一般都用短 轮询,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。
长连接
WebSocket
上面说到长轮询不适用于服务端资源频繁更新的场景,而解决这类问题的一个方案就是WebSocket。用最简单的话来介绍WebSocket就是:客户端和服务器之间建立一个持久的长连接,这个连接是双工的,客户端和服务端都可以实时地给对方发送消息。下面是WebSocket的图示:
WebSocket对于前端的同学来说是非常常见了,因为无论是webpack还是vite,用来HMR的reload就是通过WebSocket来进行的,有代码改动,工程重新编译,新变更的模块通知到浏览器加载新的模块,这里的通知浏览器加载新模块就是通过WebSocket的进行的。如上图,通过握手(协议转换)建立连接后,双方就保持持久连接,由于历史的关系,WebSocket建立连接是依赖HTTP的,但是其建连请求有明显的特征,目的是客户端和服务端都能识别并保持连接。
请求特征
请求头特征
- HTTP 必须是 1.1 GET 请求
- HTTP Header 中 Connection 字段的值必须为 Upgrade
- HTTP Header 中 Upgrade 字段必须为 websocket
- Sec-WebSocket-Key 字段的值是采用 base64 编码的随机 16 字节字符串
- Sec-WebSocket-Protocol 字段的值记录使用的子协议,比如 binary base64
- Origin 表示请求来源
响应头特征
- 状态码是 101 表示 Switching Protocols
- Upgrade / Connection / Sec-WebSocket-Protocol 和请求头一致
- Sec-WebSocket-Accept 是通过请求头的 Sec-WebSocket-Key 生成
兼容性
WebSocket 协议在2008年诞生,2011年成为国际标准。现在所有浏览器都已经支持了。
实现一个简单的 WebSocket
基于原生WebSocket我们实现一个简单的长连。
连接
// 连接只需实例一个WebSocket
const ws = new WebSocket(`wss://${url}`);
- 1.
- 2.
发送消息
ws.send("这是一条消息:" + count);
- 1.
监听消息
ws.onmessage = function (event) {
console.log(event.data);
}
- 1.
- 2.
- 3.
关闭连接
ws.close();
- 1.
在工程上使用WebSocket
在工程上,很少直接基于原生WebSocket实现业务需求,使用WebSocket需要完成下面几个问题:
- 鉴权:防止恶意连接连接进来接收消息
- 心跳:客户端意外断开,导致死链占用服务端资源,长时间无消息的连接可能会被中间网关或运营商断开
- 登录:通过建连需要识别出该连接是哪个用户,有无权限,需要推送哪些消息
- 日志:监控连接,错误上报
- 后台:能方便的查看在线连接的客户端数量,消息传输量
服务端推送(SSE)
SSE全称Server-sent Events,是HTML 5 规范的一个组成部分,该规范十分简单,主要由两个部分组成:第一个部分是服务器端与浏览器端之间的通讯协议,第二部分则是在浏览器端可供 JavaScript 使用的 EventSource 对象。通讯协议是基于纯文本的简单协议。服务器端的响应的内容类型是“text/event-stream”。响应文本的内容可以看成是一个事件流,由不同的事件所组成。每个事件由类型和数据两部分组成,同时每个事件可以有一个可选的标识符。不同事件的内容之间通过仅包含回车符和换行符的空行(“rn”)来分隔。每个事件的数据可能由多行组成。
和 Websocket对比
SSE | WebSocket |
单向:仅服务端能发送消息 | 双向:客户端、服务端双向发送 |
仅文本数据 | 二进制、文本都可 |
常规HTTP协议 | WebSocket协议 |
兼容性
数据格式
服务器向浏览器发送的 SSE 数据,必须是 UTF-8 编码的文本,
响应头
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
- 1.
- 2.
- 3.
数据传输
服务端每次发送消息,由若干message组成,使用\n\n分隔,如果单个messag过长,可以用\n分隔。
- 1.
field取值
data
event
id
retry
- 1.
- 2.
- 3.
- 4.
例子
// 注释,用于心跳包
: this is a test stream\n\n
// 设置断链1000ms重试一次
retry:1000 \n\n
event: 自定义消息\n\n
data: some text\n\n
data: another message\n
data: with two lines \n\n
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
实现一个简单的SSE
web端
实例化EventSource,监听open、message、error
const source = new EventSource(url, { withCredentials: true });
// 监听消息
source.onmessage = function (event) {
// handle message
};
source.addEventListener('message', function (event) {
// handle message
}, false);
// 监听错误
source.onerror = function (event) {
// handle error
};
source.addEventListener('error', function (event) {
// handle error
}, false);
// 关闭连接
source.close()
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
服务端
以nodejs为例,服务端代码和普通请求无异,并没有新的处理类库。
res.writeHead(200, {
"Content-Type":"text/event-stream",
"Cache-Control":"no-cache",
"Connection":"keep-alive",
"Access-Control-Allow-Origin": '*',
});
res.write("retry: 10000\n\n");
res.write("event: connecttime\n\n");
res.write("data: " + (new Date()) + "\n");
res.write("data: " + (new Date()) + "\n\n");
// 模拟收到消息推送给客户端
interval = setInterval(function () {
res.write("data: " + (new Date()) + "\n\n");
}, 1000);
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
和WebSocket不同,SSE并不是新的通信协议,其本质是在普通HTTP请求的基础上定义一个Content-Type,保持上连接,通过普通的接口也能模拟出SSE的效果,以XMLHttpRequest为例
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:8844/long", true);
xhr.onload = (e) => {
console.log("onload", xhr.responseText);
};
xhr.onprogress = (e) => {
// 每次服务端写入response的数据,都会传输过来,并产生一次onprogress事件
console.log("onprogress", xhr.responseText);
};
xhr.send();
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
参考文献
rfc6455.pdf[1]
WebSocket协议中文版(rfc6455)[2]
深入剖析WebSocket的原理 - 知乎[3]
HTTP长连接实现原理 - 掘金[4]
WebSocket() - Web API 接口参考 | MDN[5]
EventSource - Web API 接口参考 | MDN[6]