一文探究web实时通信方案并深入websocket原理与应用

2,259 阅读10分钟

背景

在最近的项目中,有一个数据展示的需求,要求是实时展示各组状态与倒计时。在技术层面就是延时要控制到非常低。

对于实时类信息获取,我们一般会有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的长轮询:

Ajax的长轮询

基于Iframe的流:

在页面中嵌入一个隐藏的iframe,然后让这个iframe的src属性指向我们请求的一个服务端地址,并且为了数据更新,我们将页面上数据更新操作封装为一个js函数,将函数名当做参数传递到这个地址当中。

服务端收到请求后解析地址取出参数(客户端js函数调用名),每当有数据更新的时候,返回对客户端函数的调用,并且将要跟新的数据以js函数的参数填入到返回内容当中,例如返回“<script type="text/javascript">update("data")</script>”这样一个字符串,意味着以data为参数调用客户端update函数进行客户端view更新。

基于Iframe的流

文字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优劣比较如下:

SSEWebSocket
通信类型半双工(单向)全双工(双向)
浏览器支持目前在 Microsoft 浏览器中不可用。可用于所有主要浏览器。
开发工作量小:只需发送一条包含特定标头的 HTTP 消息。中等:需要建立并维护 TCP 套接字通信。在服务器端还需要一个监听器套接字。
扩展性较弱较强,支持数据的双向通信

为了后期更好的扩展性,选择了websocket的方案。

深入websocket

简单理解

WebSocket 协议在2008年诞生,2011年成为国际标准。所有现代浏览器都已经支持了。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

http、websocket流程图

特点:

  1. 建立在 TCP 协议之上,服务器端的实现比较容易。

  2. 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

  3. 数据格式比较轻量,性能开销小,通信高效。

  4. 可以发送文本,也可以发送二进制数据。

  5. 没有同源限制,客户端可以与任意服务器通信。

  6. 协议标识符是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协议流程图:

wesocket协议流程图

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传输帧协议:

websocket传输帧协议

参考文档

developer.mozilla.org/zh-CN/docs/…

www.ruanyifeng.com/blog/2017/0…

www.zhihu.com/question/20…

blog.51cto.com/kusorz/2058…

zhuanlan.zhihu.com/p/21595082