背景
在最近的项目中,有一个数据展示的需求,要求是实时展示各组状态与倒计时。在技术层面就是延时要控制到非常低。
对于实时类信息获取,我们一般会有4种方案:
- 轮询,浏览器的定时器发起http请求
- 长轮询(Comet),http1.1支持的由浏览器发起的长轮询
- websocket,浏览器与后端服务器建立websocket连接,双工(双向)通信
- SSE(Server-Sent Events),基于HTTP的html5新特性,服务器推送,半双工通信模型
ps
:http2.0中有一个服务器推送不是实时需求的方案,这个特性是服务端根据客户端的请求,提前返回多个响应,推送额外的资源给客户端。如果一个请求是由你的主页发送的,服务器可能会响应主页内容、logo以及样式表,因为他知道客户端会用到这些东西。这样不但减轻了数据传送冗余步骤,也加快了页面响应的速度,提高了用户体验。
基于 Flash的socket实现逐渐淘汰,不在考虑范围内。
以下文字版本demo是参考知乎用户@Ovear的回答,推荐大家看下原文,顺便看下该问题的其他回答:www.zhihu.com/question/20…
轮询
轮询是指客户端定时向服务器发送ajax请求,服务器接到请求后马上返回响应信息并关闭连接。
这个是基于“分布式、无状态、基于TCP的请求/响应式”的http协议的。
文字demo
客户端:啦啦啦,有没有新信息(Request)
服务端:没有(Response)
客户端:啦啦啦,有没有新信息(Request)
服务端:没有。。(Response)
客户端:啦啦啦,有没有新信息(Request)
服务端:你好烦啊,没有啊。。(Response)
客户端:啦啦啦,有没有新消息(Request)
服务端:好啦好啦,有啦给你。(Response)
客户端:啦啦啦,有没有新消息(Request)
服务端:。。。。。没。。。。没。。。没有(Response) ---- loop
代码demo
<script type="text/javascript">
//前端Ajax持续调用服务端,称为Ajax轮询技术
var getting = {
url:'server.php',
dataType:'json',
success:function(res) {
console.log(res);
$.ajax(getting); //关键在这里,回调函数内再次请求Ajax
}
//当请求时间过长(默认为60秒),就再次调用ajax长轮询
error:function(res){
$.ajax($getting);
}
};
$.ajax(getting);
</script>
Comet长轮询,一种hack技术
客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。
Comet的实现主要有两种方式,基于Ajax的长轮询(long-polling)方式和基于 Iframe 及 htmlfile 的流(http streaming)方式。
Ajax的长轮询:
基于Iframe的流:
在页面中嵌入一个隐藏的iframe,然后让这个iframe的src属性指向我们请求的一个服务端地址,并且为了数据更新,我们将页面上数据更新操作封装为一个js函数,将函数名当做参数传递到这个地址当中。
服务端收到请求后解析地址取出参数(客户端js函数调用名),每当有数据更新的时候,返回对客户端函数的调用,并且将要跟新的数据以js函数的参数填入到返回内容当中,例如返回“<script type="text/javascript">update("data")</script>
”这样一个字符串,意味着以data为参数调用客户端update函数进行客户端view更新。
文字demo
客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request)
服务端:额。。 等待到有消息的时候。。来 给你(Response)
客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) -loop
代码demo
<script type="text/javascript">
//前端Ajax持续调用服务端,称为Ajax轮询技术
var getting = {
url:'server.php',
dataType:'json',
success:function(res) {
console.log(res);
$.ajax(getting); //关键在这里,回调函数内再次请求Ajax
}
//当请求时间过长(默认为60秒),就再次调用ajax长轮询
error:function(res){
$.ajax($getting);
}
};
$.ajax(getting);
</script>
websocket
文字demo
客户端:啦啦啦,我要建立Websocket协议,需要的服务:chat,Websocket协议版本:17(HTTP Request)
服务端:ok,确认,已升级为Websocket协议(HTTP Protocols Switched)
客户端:麻烦你有信息的时候推送给我噢。。
服务端:ok,有的时候会告诉你的。
服务端:balabalabalabala
服务端:balabalabalabala
服务端:哈哈哈哈哈啊哈哈哈哈
客户端:麻烦你有信息的时候推送给我噢。。
服务端:笑死我了哈哈哈哈哈哈哈
代码demo
var ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = function (evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function (evt) {
console.log("Received Message: " + evt.data);
ws.close();
};
ws.onclose = function (evt) {
console.log("Connection closed.");
};
SSE(Server-Sent Event)
所谓SSE,就是浏览器向服务器发送一个HTTP请求,然后服务器不断单向地向浏览器推送“信息”(message)。这种信息在格式上很简单、固定,就是“信息”加上前缀“data: ”,然后以“\n\n”结尾。
SSE 是一种仅使用 HTTP 传送异步消息的 HTML5 标准。不同于 WebSocket,SSE 不需要在后端创建服务器套接字。
后端响应需加入头信息:response.headers["Content-Type"] = "text/event-stream"。
支持的事件有:
onopen 当通往服务器的连接被打开
onmessage 当接收到消息
onerror 当发生错误
EventSource.close()来关闭连接。
兼容性:developer.mozilla.org/zh-CN/docs/… IE全系不支持。
文字demo
SSE是单向通道, 只能服务端向浏览器发送数据。特别适用于客户端只需接收从服务器传入的更新的应用程序。
客户端:啦啦啦,我要建立SSE
服务端:ok,有的时候会告诉你的。
服务端:来了来了,有消息了
服务端:balabalabalabala
服务端:哈哈哈哈哈啊哈哈哈哈
服务端:笑死我了哈哈哈哈哈哈哈
代码demo
if (typeof (EventSource) !== "undefined") {
var source = new EventSource("server.php");
source.onopen = function () {
console.log("Connection to server opened.");
};
source.onmessage = function (event) {
document.getElementById("result").innerHTML += event.data + "<br>";
};
source.onerror = function () {
console.log("EventSource failed.");
};
} else {
document.getElementById("result").innerHTML = "抱歉,你的浏览器不支持 server-sent 事件...";
}
选择
目前,我们已经积累了较为丰富轮询请求经验。但是,轮询、长轮询已经无法满足这次需求。主要原因是:灯态数据是500ms上报一次,频次非常高,轮询不适合,有请求丢失和异步跳秒的风险。而且,一般而言轮询都有无谓请求、浪费带宽、效率低下的问题。所以需要从SSE、WebSocket方案中选择。SSE、WebSocket优劣比较如下:
SSE | WebSocket | |
---|---|---|
通信类型 | 半双工(单向) | 全双工(双向) |
浏览器支持 | 目前在 Microsoft 浏览器中不可用。 | 可用于所有主要浏览器。 |
开发工作量 | 小:只需发送一条包含特定标头的 HTTP 消息。 | 中等:需要建立并维护 TCP 套接字通信。在服务器端还需要一个监听器套接字。 |
扩展性 | 较弱 | 较强,支持数据的双向通信 |
为了后期更好的扩展性,选择了websocket的方案。
深入websocket
简单理解
WebSocket 协议在2008年诞生,2011年成为国际标准。所有现代浏览器都已经支持了。
它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
特点:
-
建立在 TCP 协议之上,服务器端的实现比较容易。
-
与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
-
数据格式比较轻量,性能开销小,通信高效。
-
可以发送文本,也可以发送二进制数据。
-
没有同源限制,客户端可以与任意服务器通信。
-
协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
ws://example.com:80/some/path
客户端实现与API简介
包括ie在内的所有主流浏览器都支持websocket。
- 构造函数 WebSocket(url[, protocols]) 返回一个 WebSocket 对象
- 属性
- WebSocket.binaryType 使用二进制的数据类型连接 blob(Blob 对象表示一个不可变、原始数据的类文件对象。)、arrayBuffer
- WebSocket.bufferedAmount 只读 未发送至服务器的字节数
- WebSocket.extensions 只读 服务器选择的扩展
- WebSocket.onclose 用于指定连接关闭后的回调函数
- WebSocket.onerror 用于指定连接失败后的回调函数
- WebSocket.onmessage 用于指定当从服务器接受到信息时的回调函数
- WebSocket.onopen 用于指定连接成功后的回调函数
- WebSocket.protocol 只读 服务器选择的下属协议
- WebSocket.readyState 只读 当前的链接状态
- WebSocket.url 只读 WebSocket 的绝对路径
- 方法
- WebSocket.close([code[, reason]]) 关闭当前链接
- WebSocket.send(data) 向服务器发送数据
浏览器客户端示例代码:
var ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = function (evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function (evt) {
console.log("Received Message: " + evt.data);
ws.close();
};
ws.onclose = function (evt) {
console.log("Connection closed.");
};
服务端的实现
几乎各种后端语言都有对应的实现方法,支持度较好。
常用的 Node 实现有以下三种。
代码略过,直接到以上项目的GitHub中查看即可。
nginx的支持
在配置 HTTP、HTTPS 域名位置加入如下配置:
location /websocket {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
Nginx 自从 1.3
版本就开始支持 WebSocket 了,并且可以为 WebSocket 应用程序做反向代理和负载均衡。
WebSockets 受到 Nginx 缺省为60秒的 proxy_read_timeout 的影响。这意味着,如果你有一个程序使用了 WebSocket,但又可能超过60秒不发送任何数据的话,那你要么需要增加超时时间,要么实现一个 ping 的消息(心跳报文)以保持联系。使用 ping 的解决方法有额外的好处,可以发现连接是否被意外关闭。
深入理解
websocket到底是什么?
概念:
HTTP是运行在TCP协议传输层上的应用协议,而WebSocket是通过HTTP协议协商如何连接,然后独立运行在TCP协议传输层上的应用协议。
WebSocket仅仅是利用了HTTP协议做连接请求。WebSocket相当于一个简化版的TCP传输子层(实际上WebSocket也是应用层协议)。
WebSocket之所以能持久连接原因是它运行在TCP协议上,TCP协议自身是长连接协议,所以WebSocket当然可以长连接。为什么HTTP不是长连接,原因是早期的HTTP在发起每个请求,响应完成后就会关闭Socket。但是后来加了多路复用KeepAlive协议后HTTP协议已经可以实现长连接了,可以处理长连接事务了。
所以,Websocket是一个持久化的协议。
特别地:
WebSocket 不是 HTML5 的东西。
WebSocket 是一个协议,归属于 IETF。WebSocket API 是一个 Web API,归属于 W3C。两个规范是独立发布的。
广义上的 HTML5 是一个很宽广的概念,是对大量新 API 的总称, 里面包含的是 WebSocket API,并不是 WebSocket。简单的说,可以把 WebSocket 当成 HTTP,WebSocket API 当成 Ajax。
原理及运行机制
wesocket协议流程图:
Websocket借用HTTP的协议来完成一部分握手。
典型的Websocket的http握手部分:
1.请求部分
GET ws://xxx.xx.xx.xx:8000/v2x-omp/websocket HTTP/1.1
Host: xxx.xx.xx.xx:8000
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://xxx.xx.xx.xx:8000
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: _uid=333.33333.3333.0847; x_xx=_QP5elb46q2pqak9IgsfscW3xDh9Qm
Sec-WebSocket-Key: Uk07fY3CxNYoq2N5Fl9l1A==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
和一般http协议不同的主要有:
(1)
Upgrade: websocket
Connection: Upgrade
这个是Websocket的核心,告诉Apache、Nginx等服务器:这边发起的是Websocket协议,请用相应的后端来处理。
(2)
Sec-WebSocket-Key: Uk07fY3CxNYoq2N5Fl9l1A==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Version: 13
Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,用于验证交互的服务器。
Sec-WebSocket-Version 是告诉服务器所使用的Websocket Draft(协议版本),避免因版本不同出现兼容性问题。
2.响应部分
服务器会响应如下,成功建立Websocket。
HTTP/1.1 101 Switching Protocols
Server: nginx
Date: Tue, 02 Apr 2019 08:11:57 GMT
Connection: upgrade
Upgrade: websocket
Sec-WebSocket-Accept: khI5KCJzpRnpR8H2sOx+nnGCDAY=
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
至此,HTTP已经完成它所有工作了--连接握手成功,接下来就是完全按照Websocket协议进行了。
websocket传输帧协议:
参考文档
developer.mozilla.org/zh-CN/docs/…