Html5入门教程系列(5)--WebSocket霓虹建站 > 后花园 > 网站运营

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

<h2 id="section">概述</h2>

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

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

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

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

<h2 id="section-1">什么是全双工</h2>

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

<p></p>

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

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

<p></p>

<h2 id="javascript">javascript客户端</h2>

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

<figure class="highlight"><pre> <span class="cp"><!DOCTYPE html></span>
<span class="nt"><meta</span> <span class="na">charset=</span><span class="s">"utf-8"</span> <span class="nt">/></span>
<span class="nt"><title></span>WebSocket Test<span class="nt"></title></span>
<span class="nt"><script </span><span class="na">language=</span><span class="s">"javascript"</span> <span class="na">type=</span><span class="s">"text/javascript"</span><span class="nt">></span>

<span class="kd">var</span> <span class="nx">wsUri</span> <span class="o">=</span> <span class="s2">"ws://echo.websocket.org/"</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">output</span><span class="p">;</span>

<span class="kd">function</span> <span class="nx">init</span><span class="p">()</span>
<span class="p">{</span>

<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>

<span class="p">}</span>

<span class="kd">function</span> <span class="nx">testWebSocket</span><span class="p">()</span>
<span class="p">{</span>

<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>

<span class="p">}</span>

<span class="kd">function</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">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>

<span class="p">}</span>

<span class="kd">function</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">writeToScreen</span><span class="p">(</span><span class="s2">"DISCONNECTED"</span><span class="p">);</span>

<span class="p">}</span>

<span class="kd">function</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">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>

<span class="p">}</span>

<span class="kd">function</span> <span class="nx">onError</span><span class="p">(</span><span class="nx">evt</span><span class="p">)</span>
<span class="p">{</span>

<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>

<span class="p">}</span>

<span class="kd">function</span> <span class="nx">doSend</span><span class="p">(</span><span class="nx">message</span><span class="p">)</span>
<span class="p">{</span>

<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>

<span class="p">}</span>

<span class="kd">function</span> <span class="nx">writeToScreen</span><span class="p">(</span><span class="nx">message</span><span class="p">)</span>
<span class="p">{</span>

<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>

<span class="p">}</span>

<span class="nb">window</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s2">"load"</span><span class="p">,</span> <span class="nx">init</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>

<span class="nt"></script></span>

<span class="nt"><h2></span>WebSocket Test<span class="nt"></h2></span>

<span class="nt"><div</span> <span class="na">id=</span><span class="s">"output"</span><span class="nt">></div></span></pre></figure>

<p>Echo Test的效果如下:</p>

<p></p>

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

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

<p></p>

<h2 id="section-2">协议细节</h2>

<h3 id="section-3">协议层次</h3>

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

<p></p>

<h3 id="section-4">地址</h3>

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

<div class="highlighter-rouge"><pre class="highlight">ws://echo.websocket.org/
</pre>
</div>

<h3 id="section-5">握手</h3>

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

<p></p>

  1. <p>客户端首先发送一个HTTP请求,请求头部中</p> <p>a) 必须包含Upgrade: websocketConnection: Upgrade,这是告诉服务器客户端希望进行协议升级。</p> <p>b) 同时客户端随机生成一个16字节的随机值,并用base64编码后保存为Header:Sec-Websocket-Key: <16-byte nonce, base64 encoded></p> <p>c) Sec-Websocket-Version: 13表明WebSocket的协议版本为13,这是固定值,无需改动。(图中的写法是错误的)</p> <p>d) 其他头部为可选</p>
  2. <p>服务端收到这个请求后,返回状态码101</p> <p>a) 必须包含Upgrade: websocketConnection: Upgrade</p> <p>b) Sec-Websocket-Accept的值是将客户端发来的Sec-Websocket-Key进行hash后的结果,具体算法如下。有意思的是258EAFA5-E914-47DA-95CA-C5AB0DC85B11这个GUID是固定的</p>

<figure class="highlight"><pre><span class="nx">base64encode</span><span class="p">(</span><span class="nx">sha1</span><span class="p">(</span><span class="nx">Sec</span><span class="o">-</span><span class="nx">Websocket</span><span class="o">-</span><span class="nx">Key</span><span class="o">+</span><span class="s1">'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'</span><span class="p">))</span></pre></figure>

<h3 id="section-6">数据帧</h3>

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

<p></p>

<p>这里稍作解释:</p>

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

<h3 id="section-7">掩码</h3>

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

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

<p></p>

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

<figure class="highlight"><pre><span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o"><</span> <span class="n">payloadLength</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>

<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>

<span class="p">}</span></pre></figure>

<h3 id="section-8">其他协议细节</h3>

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

<h2 id="section-9">兼容</h2>

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

<p></p>

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

标签: html5入门教程

联系我们期待您的来信!我们会认真诚实的对待每一位客户,有来信必将得到我们的回复!

谢谢您能联系我们!
嘿,我来帮您!