Html5入门教程系列(5)–WebSocket

33次阅读

WebSocket 允许浏览器和服务器进行双向通信,这在过去是比较难实现的。像聊天、游戏、股票这类实时性较高的应用,特别需要这种技术。从此 web 实时应用可以摆脱 long-polling插件 了。

概述

WebSocket 允许浏览器和服务器进行双向通信,这在过去是比较难实现的。在 WebSocket 出来以前,只能通过一种称为 long-polling 的技术模拟。webqq 微信 web 版 目前仍然使用这种 long-polling 的方式。这种古老的方式当然不是个好的方案,但是在那个年代只能如此处理。

long-polling要求浏览器利用 ajax 发起请求,但是服务器并不立刻返回结果,而是等到特定的事件触发后才返回。接着,浏览器继续发起请求,并持续等待。

WebSocket 的出现,使得浏览器和服务器得以在 TCP 之上构建双工通信机制,特别是聊天、游戏、股票这类实时性较高的应用,特别需要这种技术。

WebSocket 已经正式标准化为rfc6455。意味着,在较新的浏览器上,可以使用 WebSocket。本文就来详细解释一下 WebSocket 及其原理。

什么是全双工

在通信领域全双工是指,在任意时刻,通信双方可以发送数据给另一方。但这在传统的 HTTP 协议中是做不到的。因为 HTTP 是一种请求响应模型,服务端每次只能响应客户端的一次请求,当服务端完成数据发送后,理论上本次通信结束,TCP 链接应当断开。尽管,Keep-Alive允许客户端通知服务端保持 TCP 链接,然而多数场景下,服务端并不会真正保持 TCP 链接,即使服务端保持跟客户端的 TCP 链接,只是为了减少每次通信过程的 TCP 握手时间,通信模式没有本质变化(即服务端无法主动发送数据)。这种模式限制了基于 web 的实时应用,从而诞生了 ajax long polling 解决方案:

Html5 入门教程系列(5)--WebSocket

long polling模式已经在上文和上图中描述过了。尽管是一种解决方案,但是仍然有一些弊端。比如:因为客户端必须先发送请求,服务端才可以发送消息,必然导致实时性不高。另外,数据的交互仍然基于 HTTP,所以 HTTP 头成为了额外的开销,是完全不必要的。

WebSocket 协议使全双工通信成为可能,这是由于 WebSocket 要求通信双方保持 TCP 长连接,从而,服务端可以在任意时间向客户端发送消息,而不需要客户端先发送请求:

Html5 入门教程系列(5)--WebSocket

javascript 客户端

Html5 新增了 WebSocket 相关的 javascript api,这样,我们可以通过 javascript 使浏览器与支持 WebSocket 的服务器进行基于 WebSocket 的通信。作为初学,我们可以先把注意力放在客户端,服务端可以使用公共的Echo Test 服务。javascript 代码如下:

  <!DOCTYPE html>
  <meta charset="utf-8" />
  <title>WebSocket Test</title>
  <script language="javascript" type="text/javascript">

var wsUri = “ws://echo.websocket.org/”;
var output;

function init()
{

<span class="nx">output</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"output"</span><span class="p">);</span>
<span class="nx">testWebSocket</span><span class="p">();</span>

}

function testWebSocket()
{

<span class="nx">websocket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WebSocket</span><span class="p">(</span><span class="nx">wsUri</span><span class="p">);</span>
<span class="nx">websocket</span><span class="p">.</span><span class="nx">onopen</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">evt</span><span class="p">)</span> <span class="p">{</span> <span class="nx">onOpen</span><span class="p">(</span><span class="nx">evt</span><span class="p">)</span> <span class="p">};</span>
<span class="nx">websocket</span><span class="p">.</span><span class="nx">onclose</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">evt</span><span class="p">)</span> <span class="p">{</span> <span class="nx">onClose</span><span class="p">(</span><span class="nx">evt</span><span class="p">)</span> <span class="p">};</span>
<span class="nx">websocket</span><span class="p">.</span><span class="nx">onmessage</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">evt</span><span class="p">)</span> <span class="p">{</span> <span class="nx">onMessage</span><span class="p">(</span><span class="nx">evt</span><span class="p">)</span> <span class="p">};</span>
<span class="nx">websocket</span><span class="p">.</span><span class="nx">onerror</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">evt</span><span class="p">)</span> <span class="p">{</span> <span class="nx">onError</span><span class="p">(</span><span class="nx">evt</span><span class="p">)</span> <span class="p">};</span>

}

function onOpen(evt)
{

<span class="nx">writeToScreen</span><span class="p">(</span><span class="s2">"CONNECTED"</span><span class="p">);</span>
<span class="nx">doSend</span><span class="p">(</span><span class="s2">"WebSocket rocks"</span><span class="p">);</span>

}

function onClose(evt)
{

<span class="nx">writeToScreen</span><span class="p">(</span><span class="s2">"DISCONNECTED"</span><span class="p">);</span>

}

function onMessage(evt)
{

<span class="nx">writeToScreen</span><span class="p">(</span><span class="s1">'&lt;span style="color: blue;"&gt;RESPONSE:'</span> <span class="o">+</span> <span class="nx">evt</span><span class="p">.</span><span class="nx">data</span><span class="o">+</span><span class="s1">'&lt;/span&gt;'</span><span class="p">);</span>
<span class="nx">websocket</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span>

}

function onError(evt)
{

<span class="nx">writeToScreen</span><span class="p">(</span><span class="s1">'&lt;span style="color: red;"&gt;ERROR:&lt;/span&gt;'</span> <span class="o">+</span> <span class="nx">evt</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>

}

function doSend(message)
{

<span class="nx">writeToScreen</span><span class="p">(</span><span class="s2">"SENT:"</span> <span class="o">+</span> <span class="nx">message</span><span class="p">);</span>
<span class="nx">websocket</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span>

}

function writeToScreen(message)
{

<span class="kd">var</span> <span class="nx">pre</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s2">"p"</span><span class="p">);</span>
<span class="nx">pre</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">wordWrap</span> <span class="o">=</span> <span class="s2">"break-word"</span><span class="p">;</span>
<span class="nx">pre</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="nx">message</span><span class="p">;</span>
<span class="nx">output</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">pre</span><span class="p">);</span>

}

window.addEventListener(“load”, init, false);

</script>

<h2>WebSocket Test</h2>

<div id=“output”></div>

Echo Test 的效果如下:

Html5 入门教程系列(5)--WebSocket

可以看到关键在于使用 websocket = new WebSocket(wsUri); 来初始化一个 WebSocket 对象,该对象负责完成所有的通信。开发者无需关心具体的协议细节。

下面这个例子,可以实现一个简单 web 聊天室,服务端采用 python 实现,python的服务端实现包含了 WebSocket 的协议细节。可以从 这里 获取上面这个例子的代码

Html5 入门教程系列(5)--WebSocket

协议细节

协议层次

WebSocket 的协议层次跟 HTTP 相同,都是基于 TCP 的,只是 WebSocket 在握手阶段需要 HTTP 协助

Html5 入门教程系列(5)--WebSocket

地址

与 HTTP 类似,WebSocket 定义了 ws 或者 wss 作为协议,其他部分跟 HTTP 完全相同,例如:

ws://echo.websocket.org/

握手

WebSocket 的握手阶段是比较关键的部分,握手过程存在一个从一个协议转化为另一个协议的问题(从 http 转化为 WebSocket)。下图展示了这个握手的过程:

Html5 入门教程系列(5)--WebSocket

  1. 客户端首先发送一个 HTTP 请求,请求头部中 a) 必须 包含 Upgrade: websocketConnection: Upgrade,这是告诉服务器客户端希望进行协议升级。b) 同时客户端随机生成一个 16 字节的随机值,并用 base64 编码后保存为 Header:Sec-Websocket-Key: <16-byte nonce, base64 encoded>c) Sec-Websocket-Version: 13表明 WebSocket 的协议版本为 13,这是固定值,无需改动。(图中的写法是错误的)

    d) 其他头部为可选

  2. 服务端收到这个请求后,返回状态码 101a) 必须 包含 Upgrade: websocketConnection: Upgradeb) Sec-Websocket-Accept的值是将客户端发来的 Sec-Websocket-Key 进行 hash 后的结果,具体算法如下。有意思的是 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 这个 GUID 是固定的
base64encode(sha1(Sec-Websocket-Key+'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))

数据帧

为了实现高效的数据通信,WebSocket 在具体进行数据收发的时候,采用类似 TCP 封包的模式,将数据使用简单的头部定义封装后直接发送,免去了 HTTP 协议中灵活而复杂的 HTTP Header,这样在通信过程中实际的数据发送量大大减少,提高了传输的效率,节省了带宽。我们来具体看看封包的方式:

Html5 入门教程系列(5)--WebSocket

这里稍作解释:

  1. 首先数据帧分为文本类型和二进制数据类型,这是通过 opcode 来规定的。文本类型为0x01,二进制类型为0x02
  2. 需要将 Payload(实际的数据) 的长度在开始的若干的字节中表示出来,这样如果数据长度比较大,TCP 层分包以后,WebSocket 层的实现得以根据长度来组包。
  3. 使用 MASK 标记位说明数据实体是否需要进行掩码处理
  4. Payload部分如果需要掩码处理,则通过 Masking-key(32 位)来计算

掩码

MASK(掩码)实际上是一种安全措施,Websocket 规定,客户端发送的所有数据帧都需要将数据进行掩码处理,而服务端发送的数据是不一定要经过掩码处理的。

掩码处理就是使用 Masking-key(32 位),对 Payload 数据进行一个异或 (XOR) 计算。经过计算后 Payload 的长度不会发生变化。解码端,可以通过同样的异或运算,反推出原始的 Payload:

Html5 入门教程系列(5)--WebSocket

由于 Masking-key 是 32 位的随机值,所以在进行掩码计算时,是每次将 Payload 的 4 个字节拿出来,跟 Masking-key 进行按位异或运算,得到掩码后的结果,然后取出接下来的 4 个字节,做同样的处理。上图对这个过程进行了描述,不过上图是按字节来异或的,实际是一样的。类似的代码如下:

for (size_t i = 0; i < payloadLength; i++) {
<span class="n">frame_buffer</span><span class="p">[</span><span class="n">frame_buffer_size</span><span class="p">]</span> <span class="o">=</span> <span class="n">unmasked_payload</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">^</span> <span class="n">mask_key</span><span class="p">[</span><span class="n">i</span> <span class="o">%</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">uint32_t</span><span class="p">)];</span>
<span class="n">frame_buffer_size</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>

}

其他协议细节

在协议方面还有其他的细节,比如 ping 包等,可通过 rfc6455 了解详细。

兼容

目前浏览器对 WebSocket 的支持已经相当可观了:

Html5 入门教程系列(5)--WebSocket

然而,虽然我们在讨论浏览器对 Websocket 的支持情况,不过不要忘了,作为一个协议而言,客户端并不局限于浏览器,我们完全可以使用其他语言在其他平台上实现基于 Websocket 的通信。比如,如果一个游戏服务端接口即希望支持移动设备,还希望支持网页游戏,那么采用 Websocket 作为通信协议是一个可以考虑的选项。

正文完