- Html5 入门教程系列(1)– 总览:http://www.ningmop.com/15024.html
- Html5 入门教程系列(2)–javascript 介绍:http://www.ningmop.com/15025.html
- Html5 入门教程系列(3)–CSS 第一部分:http://www.ningmop.com/15026.html
- Html5 入门教程系列(4)–CSS 第二部分:http://www.ningmop.com/15027.html
- Html5 入门教程系列(5)–WebSocket:http://www.ningmop.com/15028.html
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
解决方案:
long polling
模式已经在上文和上图中描述过了。尽管是一种解决方案,但是仍然有一些弊端。比如:因为客户端必须先发送请求,服务端才可以发送消息,必然导致实时性不高。另外,数据的交互仍然基于 HTTP,所以 HTTP 头成为了额外的开销,是完全不必要的。
WebSocket 协议使全双工通信成为可能,这是由于 WebSocket 要求通信双方保持 TCP 长连接,从而,服务端可以在任意时间向客户端发送消息,而不需要客户端先发送请求:
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">'<span style="color: blue;">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">'</span>'</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">'<span style="color: red;">ERROR:</span>'</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 的效果如下:
可以看到关键在于使用 websocket = new WebSocket(wsUri);
来初始化一个 WebSocket 对象,该对象负责完成所有的通信。开发者无需关心具体的协议细节。
下面这个例子,可以实现一个简单 web 聊天室,服务端采用 python
实现,python
的服务端实现包含了 WebSocket
的协议细节。可以从 这里 获取上面这个例子的代码
协议细节
协议层次
WebSocket 的协议层次跟 HTTP 相同,都是基于 TCP 的,只是 WebSocket 在握手阶段需要 HTTP 协助
地址
与 HTTP 类似,WebSocket 定义了 ws
或者 wss
作为协议,其他部分跟 HTTP
完全相同,例如:
ws://echo.websocket.org/
握手
WebSocket 的握手阶段是比较关键的部分,握手过程存在一个从一个协议转化为另一个协议的问题(从 http 转化为 WebSocket)。下图展示了这个握手的过程:
- 客户端首先发送一个 HTTP 请求,请求头部中 a)
必须
包含Upgrade: websocket
和Connection: Upgrade
,这是告诉服务器客户端希望进行协议升级。b) 同时客户端随机生成一个 16 字节的随机值,并用 base64 编码后保存为 Header:Sec-Websocket-Key: <16-byte nonce, base64 encoded>
c)Sec-Websocket-Version: 13
表明 WebSocket 的协议版本为 13,这是固定值,无需改动。(图中的写法是错误的)d) 其他头部为可选
- 服务端收到这个请求后,返回状态码
101
a)必须
包含Upgrade: websocket
和Connection: Upgrade
b)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,这样在通信过程中实际的数据发送量大大减少,提高了传输的效率,节省了带宽。我们来具体看看封包的方式:
这里稍作解释:
- 首先数据帧分为文本类型和二进制数据类型,这是通过
opcode
来规定的。文本类型为0x01
,二进制类型为0x02
。 - 需要将
Payload(实际的数据)
的长度在开始的若干的字节中表示出来,这样如果数据长度比较大,TCP 层分包以后,WebSocket 层的实现得以根据长度来组包。 - 使用 MASK 标记位说明数据实体是否需要进行掩码处理
Payload
部分如果需要掩码处理,则通过 Masking-key(32 位)来计算
掩码
MASK(掩码)实际上是一种安全措施,Websocket 规定,客户端发送的所有数据帧都需要将数据进行掩码处理
,而服务端发送的数据是不一定要经过掩码处理的。
掩码处理就是使用 Masking-key(32 位),对 Payload 数据进行一个异或 (XOR) 计算。经过计算后 Payload 的长度不会发生变化。解码端,可以通过同样的异或运算,反推出原始的 Payload:
由于 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 的支持已经相当可观了:
然而,虽然我们在讨论浏览器对 Websocket 的支持情况,不过不要忘了,作为一个协议而言,客户端并不局限于浏览器,我们完全可以使用其他语言在其他平台上实现基于 Websocket 的通信。比如,如果一个游戏服务端接口即希望支持移动设备,还希望支持网页游戏,那么采用 Websocket 作为通信协议是一个可以考虑的选项。