工控课堂

 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,访问微社区

工控课堂 首页 工控文库 工控编程 查看内容

全双工通信的 WebSocket

2019-9-14 20:08| 发布者: 198366809| 查看: 1| 评论: 0|原作者: 198366809

摘要: 全双工通信的 WebSocket
作者:一缕殇流化隐半边冰霜

一. WebSocket 是什么?


WebSocket 是一种网络通信协议。在 2009 年诞生,于 2011 年被 IETF 定为标准 RFC 6455 通信标准。并由 RFC7936 补充规范。WebSocket API 也被 W3C 定为标准。

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工(full-duplex)通讯的协议。没有了 Request 和 Response 的概念,两者地位完全平等,连接一旦建立,就建立了真•持久性连接,双方可以随时向对方发送数据。

(HTML5 是 HTML 最新版本,包含一些新的标签和全新的 API。HTTP 是一种协议,目前最新版本是 HTTP/2 ,所以 WebSocket 和 HTTP 有一些交集,两者相异的地方还是很多。两者交集的地方在 HTTP 握手阶段,握手成功后,数据就直接从 TCP 通道传输。)

二. 为什么要发明 WebSocket ?

在没有 WebSocket 之前,Web 为了实现即时通信,有以下几种方案,最初的 polling ,到之后的 Long polling,最后的基于 streaming 方式,再到最后的 SSE,也是经历了几个不种的演进方式。

(1) 最开始的短轮询 Polling 阶段


这种方式下,是不适合获取实时信息的,客户端和服务器之间会一直进行连接,每隔一段时间就询问一次。客户端会轮询,有没有新消息。这种方式连接数会很多,一个接受,一个发送。而且每次发送请求都会有 HTTP 的 Header,会很耗流量,也会消耗 CPU 的利用率。

这个阶段可以看到,一个 Request 对应一个 Response,一来一回一来一回。

在 Web 端,短轮询用 AJAX JSONP Polling 轮询实现。

由于 HTTP 无法无限时长的保持连接,所以不能在服务器和 Web 浏览器之间频繁的长时间进行数据推送,所以 Web 应用通过通过频繁的异步 JavaScript 和 XML (AJAX) 请求来实现轮循。


  • 优点:短连接,服务器处理简单,支持跨域、浏览器兼容性较好。
  • 缺点:有一定延迟、服务器压力较大,浪费带宽流量、大部分是无效请求。


(2) 改进版的长轮询 Long polling 阶段(Comet Long polling)


长轮询是对轮询的改进版,客户端发送 HTTP 给服务器之后,有没有新消息,如果没有新消息,就一直等待。直到有消息或者超时了,才会返回给客户端。消息返回后,客户端再次建立连接,如此反复。这种做法在某种程度上减小了网络带宽和 CPU 利用率等问题。

这种方式也有一定的弊端,实时性不高。如果是高实时的系统,肯定不会采用这种办法。因为一个 GET 请求来回需要 2个 RTT,很可能在这段时间内,数据变化很大,客户端拿到的数据已经延后很多了。

另外,网络带宽低利用率的问题也没有从根源上解决。每个 Request 都会带相同的 Header。

对应的,Web 也有 AJAX 长轮询,也叫 XHR 长轮询。

客户端打开一个到服务器端的 AJAX 请求,然后等待响应,服务器端需要一些特定的功能来允许请求被挂起,只要一有事件发生,服务器端就会在挂起的请求中送回响应并关闭该请求。客户端在处理完服务器返回的信息后,再次发出请求,重新建立连接,如此循环。


  • 优点:减少轮询次数,低延迟,浏览器兼容性较好。
  • 缺点:服务器需要保持大量连接。


(3) 基于流(Comet Streaming)

1. 基于 Iframe 及 htmlfile 的流(Iframe Streaming)

iframe 流方式是在页面中插入一个隐藏的 iframe,利用其 src 属性在服务器和客户端之间创建一条长链接,服务器向 iframe 传输数据(通常是 HTML,内有负责插入信息的 JavaScript),来实时更新页面。iframe 流方式的优点是浏览器兼容好。


使用 iframe 请求一个长连接有一个很明显的不足之处:IE、Morzilla Firefox 下端的进度栏都会显示加载没有完成,而且 IE 上方的图标会不停的转动,表示加载正在进行。

Google 的天才们使用一个称为 “htmlfile” 的 ActiveX 解决了在 IE 中的加载显示问题,并将这种方法用到了 gmail+gtalk 产品中。Alex Russell 在 “What else is burried down in the depth's of Google's amazing JavaScript?”文章中介绍了这种方法。Zeitoun 网站提供的 comet-iframe.tar.gz,封装了一个基于 iframe 和 htmlfile 的 JavaScript comet 对象,支持 IE、Mozilla Firefox 浏览器,可以作为参考。

  • 优点:实现简单,在所有支持 iframe 的浏览器上都可用、客户端一次连接、服务器多次推送。
  • 缺点:无法准确知道连接状态,IE浏览器在 iframe 请求期间,浏览器 title 一直处于加载状态,底部状态栏也显示正在加载,用户体验不好(htmlfile 通过 ActiveXObject 动态写入内存可以解决此问题)。


2. AJAX multipart streaming(XHR Streaming)

实现思路:浏览器必须支持 multi-part 标志,客户端通过 AJAX 发出请求 Request,服务器保持住这个连接,然后可以通过 HTTP1.1 的 chunked encoding 机制(分块传输编码)不断 push 数据给客户端,直到 timeout 或者手动断开连接。

  • 优点:客户端一次连接,服务器数据可多次推送。
  • 缺点:并非所有的浏览器都支持 multi-part 标志。


3. Flash Socket(Flash Streaming)

实现思路:在页面中内嵌入一个使用了 Socket 类的 Flash 程序,JavaScript 通过调用此 Flash 程序提供的 Socket 接口与服务器端的 Socket 接口进行通信,JavaScript 通过 Flash Socket 接收到服务器端传送的数据。

  • 优点:实现真正的即时通信,而不是伪即时。
  • 缺点:客户端必须安装 Flash 插件;非 HTTP 协议,无法自动穿越防火墙。


4. Server-Sent Events

服务器发送事件(SSE)也是 HTML5 公布的一种服务器向浏览器客户端发起数据传输的技术。一旦创建了初始连接,事件流将保持打开状态,直到客户端关闭。该技术通过传统的 HTTP 发送,并具有 WebSockets 缺乏的各种功能,例如自动重新连接、事件 ID 以及发送任意事件的能力。

SSE 就是利用服务器向客户端声明,接下来要发送的是流信息(streaming),会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,可以类比视频流。SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。

SSE 是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。

服务器向浏览器发送的 SSE 数据,必须是 UTF-8 编码的文本,具有如下的 HTTP 头信息。
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

上面三行之中,第一行的Content-Type必须指定 MIME 类型为event-steam


  • 优点:适用于更新频繁、低延迟并且数据都是从服务端发到客户端。
  • 缺点:浏览器兼容难度高。



以上是常见的四种基于流的做法,Iframe Streaming、XHR Streaming、Flash Streaming、Server-Sent Events。

从浏览器兼容难度看 —— 短轮询/AJAX > 长轮询/Comet > 长连接/SSE

WebSocket 的到来

从上面这几种演进的方式来看,也是不断改进的过程。

短轮询效率低,非常浪费资源(网络带宽和计算资源)。有一定延迟、服务器压力较大,并且大部分是无效请求。

长轮询虽然省去了大量无效请求,减少了服务器压力和一定的网络带宽的占用,但是还是需要保持大量的连接。

最后到了基于流的方式,在服务器往客户端推送,这个方向的流实时性比较好。但是依旧是单向的,客户端请求服务器依然还需要一次 HTTP 请求。

那么人们就在考虑了,有没有这样一个完美的方案,即能双向通信,又可以节约请求的 header 网络开销,并且有更强的扩展性,最好还可以支持二进制帧,压缩等特性呢?

于是人们就发明了这样一个目前看似“完美”的解决方案 —— WebSocket。

在 HTML5 中公布了 WebSocket 标准以后,直接取代了 Comet 成为服务器推送的新方法。

Comet 是一种用于 web 的推送技术,能使服务器实时地将更新的信息传送到客户端,而无须客户端发出请求,目前有两种实现方式,长轮询和 iframe 流。


  • 优点:
  • 较少的控制开销,在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于 HTTP 请求每次都要携带完整的头部,此项开销显著减少了。
  • 更强的实时性,由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
  • 长连接,保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
  • 双向通信、更好的二进制支持。与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易被屏蔽,能通过各种 HTTP 代理服务器。
  • 缺点:部分浏览器不支持(支持的浏览器会越来越多)。
  • 应用场景:较新浏览器支持、不受框架限制、较高扩展性。



一句话总结一下 WebSocket:

WebSocket 是 HTML5 开始提供的一种独立在单个 TCP 连接上进行全双工通讯的有状态的协议(它不同于无状态的 HTTP),并且还能支持二进制帧、扩展协议、部分自定义的子协议、压缩等特性。

目前看来,WebSocket 是可以完美替代 AJAX 轮询和 Comet 。但是某些场景还是不能替代 SSE,WebSocket 和 SSE 各有所长!

三. WebSocket 握手

WebSocket 的 RFC6455 标准中制定了 2 个高级组件,一个是开放性 HTTP 握手用于协商连接参数,另一个是二进制消息分帧机制用于支持低开销的基于消息的文本和二进制数据传输。接下来就好好谈谈这两个高级组件,这一章节详细的谈谈握手的细节,下一个章节再谈谈二进制消息分帧机制。

首先,在 RFC6455 中写了这样一段话:

WebSocket 协议尝试在既有 HTTP 基础设施中实现双向 HTTP 通信,因此 也使用 HTTP 的 80 和 443 端口......不过,这个设计不限于通过 HTTP 实现 WebSocket 通信,未来的实现可以在某个专用端口上使用更简单的握手,而 不必重新定义么一个协议。

——WebSocket Protocol RFC 6455

从这段话中我们可看出制定 WebSocket 协议的人的“野心”或者说对未来的规划有多远,WebSocket 制定之初就已经支持了可以在任意端口上进行握手,而不仅仅是要依靠 HTTP 握手。

不过目前用的对多的还是依靠 HTTP 进行握手。因为 HTTP 的基础设施已经相当完善了。

标准的握手流程

接下来看一个具体的 WebSocket 握手的例子。以笔者自己的网站 https://threes.halfrost.com/ 为例。

打开这个网站,网页一渲染就会开启一个 wss 的握手请求。握手请求如下:

GET wss://threes.halfrost.com/sockjs/689/8x5nnke6/websocket HTTP/1.1
// 请求的方法必须是GET,HTTP版本必须至少是1.1

Host: threes.halfrost.com
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
// 请求升级到 WebSocket 协议

Origin: https://threes.halfrost.com
Sec-WebSocket-Version: 13
// 客户端使用的 WebSocket 协议版本

User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Mobile Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: _ga=GA1.2.00000006.14111111496; _gid=GA1.2.23232376.14343448247; Hm_lvt_d60c126319=1524898423,1525574369,1526206975,1526784803; Hm_lpvt_d606319=1526784803; _gat_53806_2=1
Sec-WebSocket-Key: wZgx0uTOgNUsHGpdWc0T+w==
// 自动生成的键,以验证服务器对协议的支持,其值必须是 nonce 组成的随机选择的 16 字节的被 base64 编码后的值

Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
// 可选的客户端支持的协议扩展列表,指示了客户端希望使用的协议级别的扩展

这里和普通的 HTTP 协议相比,不同的地方有以下几处:

请求的 URL 是 ws:// 或者 wss:// 开头的,而不是 HTTP:// 或者 HTTPS://。由于 websocket 可能会被用在浏览器以外的场景,所以这里就使用了自定义的 URI。类比 HTTP,ws协议:普通请求,占用与 HTTP 相同的 80 端口;wss协议:基于 SSL 的安全传输,占用与 TLS 相同的 443 端口。

Connection: Upgrade
Upgrade: websocket

这两处是普通的 HTTP 报文一般没有的,这里利用 Upgrade 进行了协议升级,指明升级到 websocket 协议。

Sec-WebSocket-Version: 13
Sec-WebSocket-Key: wZgx0uTOgNUsHGpdWc0T+w==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

Sec-WebSocket-Version 表示 WebSocket 的版本,最初 WebSocket 协议太多,不同厂商都有自己的协议版本,不过现在已经定下来了。如果服务端不支持该版本,需要返回一个 Sec-WebSocket-Version,里面包含服务端支持的版本号。

最新版本就是 13,当然有可能存在非常早期的版本 7 ,8(目前基本不会不存在 7,8 的版本了)

注意:尽管本文档的草案版本(09、10、11、和 12)发布了(它们多不是编辑上的修改和澄清而不是改变电报协议 [wire protocol]),值 9、10、11、和 12 不被用作有效的 Sec-WebSocket-Version。这些值被保留在 IANA 注册中心,但并将不会被使用。

+--------+-----------------------------------------+----------+
|Version |                Reference                |  Status  |
| Number |                                         |          |
+--------+-----------------------------------------+----------+
| 0      + draft-ietf-hybi-thewebsocketprotocol-00 | Interim  |
+--------+-----------------------------------------+----------+
|
1      + draft-ietf-hybi-thewebsocketprotocol-01 | Interim  |
+--------+-----------------------------------------+----------+
| 2      + draft-ietf-hybi-thewebsocketprotocol-02 | Interim  |
+--------+-----------------------------------------+----------+
|
3      + draft-ietf-hybi-thewebsocketprotocol-03 | Interim  |
+--------+-----------------------------------------+----------+
| 4      + draft-ietf-hybi-thewebsocketprotocol-04 | Interim  |
+--------+-----------------------------------------+----------+
|
5      + draft-ietf-hybi-thewebsocketprotocol-05 | Interim  |
+--------+-----------------------------------------+----------+
| 6      + draft-ietf-hybi-thewebsocketprotocol-06 | Interim  |
+--------+-----------------------------------------+----------+
|
7      + draft-ietf-hybi-thewebsocketprotocol-07 | Interim  |
+--------+-----------------------------------------+----------+
| 8      + draft-ietf-hybi-thewebsocketprotocol-08 | Interim  |
+--------+-----------------------------------------+----------+
|
9      +                Reserved                 |          |
+--------+-----------------------------------------+----------+
| 10     +                Reserved                 |          |
+--------+-----------------------------------------+----------+
|
11     +                Reserved                 |          |
+--------+-----------------------------------------+----------+
| 12     +                Reserved                 |          |
+--------+-----------------------------------------+----------+
|
13     +                RFC 6455                 | Standard |
+--------+-----------------------------------------+----------+

[RFC 6455]

The|SecWebSocketKey| header field is used in the WebSocket opening handshake. It is sent from the client to the server to provide part of the information used by the server to prove that it received a valid WebSocket opening handshake.
This helps ensure that the server does not accept connections from non-WebSocket clients (e.g., HTTP clients) that are being abused to send data to unsuspecting WebSocket servers.
Sec-WebSocket-Key 字段用于握手阶段。它从客户端发送到服务器以提供部分内容,服务器用来证明它收到的信息,并且能有效的完成 WebSocket 握手。这有助于确保服务器不会接受来自非 WebSocket 客户端的连接(例如 HTTP 客户端)被滥用发送数据到毫无防备的 WebSocket 服务器。

Sec-WebSocket-Key 是由浏览器随机生成的,提供基本的防护,防止恶意或者无意的连接。

Sec-WebSocket-Extensions 是属于升级协商的部分,这里放在下一章节进行详细讲解。

接着来看看 Response:
HTTP/1.1 101 Switching Protocols
// 101 HTTP 响应码确认升级到 WebSocket 协议
Server: nginx/1.12.1
Date: Sun, 20 May 2018 09:06:28 GMT
Connection: upgrade
Upgrade: websocket
Sec-WebSocket-Accept: 375guuMrnCICpulKbj7+JGkOhok=
// 签名的键值验证协议支持
Sec-WebSocket-Extensions: permessage-deflate
// 服务器选择的WebSocket 扩展
在 Response 中,用 HTTP 101 响应码回应,确认升级到 WebSocket 协议。

同样也有两个 WebSocket 的 header:
Sec-WebSocket-Accept: 375guuMrnCICpulKbj7+JGkOhok=
// 签名的键值验证协议支持
Sec-WebSocket-Extensions: permessage-deflate
// 服务器选择的 WebSocket 扩展

Sec-WebSocket-Accept 是经过服务器确认后,并且加密之后的 Sec-WebSocket-Key。

Sec-WebSocket-Accept 的计算方法如下:

  • 先将客户端请求头里面的 Sec-WebSocket-Key 取出来跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;(258EAFA5-E914-47DA-95CA-C5AB0DC85B11 这个 Globally Unique Identifier (GUID, [RFC4122]) 是唯一固定不变的)
  • 然后进行 SHA-1 哈希,最后进行 base64-encoded 得到的结果就是 Sec-WebSocket-Accept。


伪代码:

> toBase64(sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ))

同样,Sec-WebSocket-Key/Sec-WebSocket-Accept 只是在握手的时候保证握手成功,但是对数据安全并不保证,用 wss:// 会稍微安全一点。

握手中的子协议

WebSocket 握手有可能会涉及到子协议的问题。

先来看看 WebSocket 的对象初始化函数:

WebSocket WebSocket(
in DOMString url,
// 表示要连接的URL。这个URL应该为响应WebSocket的地址。
in optional DOMString protocols
// 可以是一个单个的协议名字字符串或者包含多个协议名字字符串的数组。默认设为一个空字符串。
);
这里有一个 optional ,是一个可以协商协议的数组。

var ws = new WebSocket('wss://example.com/socket', ['appProtocol', 'appProtocol-v2']);

ws.onopen = function () {
if (ws.protocol == 'appProtocol-v2') {
  ...
  } else {
  ...
  }
}

在创建 WebSocket 对象的时候,可以传递一个可选的子协议数组,告诉服务器,客户端可以理解哪些协议或者希望服务器接收哪些协议。服务器可以从数据里面选择几个支持的协议进行返回,如果一个都不支持,那么会直接导致握手失败。触发 onerror 回调,并断开连接。

这里的子协议可以是自定义的协议。

多版本的 websocket 握手

使用 WebSocket 版本通知能力( Sec-WebSocket-Version 头字段),客户端可以初始请求它选择的 WebSocket 协议的版本(这并不一定必须是客户端支持的最新的)。如果服务器支持请求的版本且握手消息是本来有效的,服务器将接受该版本。如果服务器不支持请求的版本,它必须以一个包含所有它将使用的版本的 Sec-WebSocket-Version 头字段(或多个 Sec-WebSocket-Version 头字段)来响应。此时,如果客户端支持一个通知的版本,它可以使用新的版本值重做 WebSocket 握手。

举个例子:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
...
Sec-WebSocket-Version: 25
服务器不支持 25 的版本,则会返回:

HTTP/1.1 400 Bad Request
...
Sec-WebSocket-Version: 13, 8, 7
客户端支持 13 版本的,则需要重新握手:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
...
Sec-WebSocket-Version: 13

四. WebSocket 升级协商

在 WebSocket 握手阶段,会 5 个带 WebSocket 的 header。这 5 个 header 都是和升级协商相关的。

  • Sec-WebSocket-Version
    客户端表明自己想要使用的版本号(一般都是 13 号版本),如果服务器不支持这个版本,则需要返回自己支持的版本。客户端拿到 Response 以后,需要对自己支持的版本号重新握手。这个 header 客户端必须要发送。
  • Sec-WebSocket-Key
    客户端请求自动生成的一个 key。这个 header 客户端必须要发送。
  • Sec-WebSocket-Accept
    服务器针对客户端的 Sec-WebSocket-Key 计算的响应值。这个 header 服务端必须要发送。
  • Sec-WebSocket-Protocol
    用于协商应用子协议:客户端发送支持的协议列表,服务器必须只回应一个协议名。如果服务器一个协议都不能支持,直接握手失败。客户端可以不发送子协议,但是一旦发送,服务器无法支持其中任意一个都会导致握手失败。这个 header 客户端可选发送。
  • Sec-WebSocket-Extensions
    用于协商本次连接要使用的 WebSocket 扩展:客户端发送支持的扩展,服务器通过返回相同的首部确认自己支持一或多个扩展。这个 head

路过

雷人

握手

鲜花

鸡蛋

相关阅读

最新评论

热门文章

QQ|免责声明|本站介绍|工控课堂 ( 沪ICP备14007696号-3 )|网站地图

GMT+8, 2019-9-14 20:08 , Processed in 0.061772 second(s), 41 queries .

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

返回顶部