Websocket協議的學習、調研和實現

zorro47 8年前發布 | 54K 次閱讀 WebSocket WebSocket 開發

來自: http://www.cnblogs.com/lizhenghn/p/5155933.html

本文章同時發在 cpper.info

1. websocket是什么

Websocket是html5提出的一個協議規范,參考rfc6455。

websocket約定了一個通信的規范,通過一個握手的機制,客戶端(瀏覽器)和服務器(webserver)之間能建立一個類似tcp的連接,從而方便c-s之間的通信。在websocket出現之前,web交互一般是基于http協議的短連接或者長連接。

WebSocket是為解決客戶端與服務端實時通信而產生的技術。websocket協議本質上是一個基于tcp的協議,是先通過HTTP/HTTPS協議發起一條特殊的http請求進行握手后創建一個用于交換數據的TCP連接,此后服務端與客戶端通過此TCP連接進行實時通信。

注意:此時不再需要原HTTP協議的參與了。

2. websocket的優點

以前web server實現推送技術或者即時通訊,用的都是輪詢(polling),在特點的時間間隔(比如1秒鐘)由瀏覽器自動發出請求,將服務器的消息主動的拉回來,在這種情況下,我們需要不斷的向服務器發送請求,然而HTTP request 的header是非常長的,里面包含的數據可能只是一個很小的值,這樣會占用很多的帶寬和服務器資源。

而最比較新的技術去做輪詢的效果是Comet – 用了AJAX。但這種技術雖然可達到全雙工通信,但依然需要發出請求(reuqest)。

WebSocket API最偉大之處在于服務器和客戶端可以在給定的時間范圍內的任意時刻,相互推送信息。 瀏覽器和服務器只需要要做一個握手的動作,在建立連接之后,服務器可以主動傳送數據給客戶端,客戶端也可以隨時向服務器發送數據。 此外,服務器與客戶端之間交換的標頭信息很小。

WebSocket并不限于以Ajax(或XHR)方式通信,因為Ajax技術需要客戶端發起請求,而WebSocket服務器和客戶端可以彼此相互推送信息;

因此從服務器角度來說,websocket有以下好處:

  1. 節省每次請求的header
    http的header一般有幾十字節
  2. Server Push
    服務器可以主動傳送數據給客戶端

3. 歷史沿革

3.1 http協議

1996年IETF HTTP工作組發布了HTTP協議的1.0版本,到現在普遍使用的版本1.1,HTTP協議經歷了17年的發展。 這種分布式、無狀態、基于TCP的請求/響應式、在互聯網盛行的今天得到廣泛應用的協議。互聯網從興起到現在,經歷了門戶網站盛行的web1.0時代,而后隨著ajax技術的出現,發展為web應用盛行的web2.0時代,如今又朝著web3.0的方向邁進。反觀http協議,從版本1.0發展到1.1,除了默認長連接之外就是緩存處理、帶寬優化和安全性等方面的不痛不癢的改進。它一直保留著無狀態、請求/響應模式,似乎從來沒意識到這應該有所改變。

3.2 通過腳本發送的http請求(Ajax)

傳統的web應用要想與服務器交互,必須提交一個表單(form),服務器接收并處理傳來的表單,然后返回全新的頁面,因為前后兩個頁面的數據大部分都是相同的,這個過程傳輸了很多冗余的數據、浪費了帶寬。于是Ajax技術便應運而生。

Ajax是Asynchronous JavaScript and 的簡稱,由Jesse James Garrett 首先提出。這種技術開創性地允許瀏覽器腳本(JS)發送http請求。Outlook Web Access小組于98年使用,并很快成為IE4.0的一部分,但是這個技術一直很小眾,直到2005年初,google在他的goole groups、gmail等交互式應用中廣泛使用此種技術,才使得Ajax迅速被大家所接受。

Ajax的出現使客戶端與服務器端傳輸數據少了很多,也快了很多,也滿足了以豐富用戶體驗為特點的web2.0時代 初期發展的需要,但是慢慢地也暴露了他的弊端。比如無法滿足即時通信等富交互式應用的實時更新數據的要求。這種瀏覽器端的小技術畢竟還是基于http協議,http協議要求的請求/響應的模式也是無法改變的,除非http協議本身有所改變。

3.3 一種hack技術(Comet)

以即時通信為代表的web應用程序對數據的Low Latency要求,傳統的基于輪詢的方式已經無法滿足,而且也會帶來不好的用戶體驗。于是一種基于http長連接的“服務器推”技術便被hack出來。這種技術被命名為Comet,這個術語由Dojo Toolkit 的項目主管Alex Russell在博文Comet: Low Latency Data for the Browser首次提出,并沿用下來。

其實,服務器推很早就存在了,在經典的client/server模型中有廣泛使用,只是瀏覽器太懶了,并沒有對這種技術提供很好的支持。但是Ajax的出現使這種技術在瀏覽器上實現成為可能, google的gmail和gtalk的整合首先使用了這種技術。隨著一些關鍵問題的解決(比如IE的加載顯示問題),很快這種技術得到了認可,目前已經有很多成熟的開源Comet框架。

以下是典型的Ajax和Comet數據傳輸方式的對比,區別簡單明了。典型的Ajax通信方式也是http協議的經典使用方式,要想取得數據,必須首先發送請求。在Low Latency要求比較高的web應用中,只能增加服務器請求的頻率。Comet則不同,客戶端與服務器端保持一個長連接,只有客戶端需要的數據更新時,服務器才主動將數據推送給客戶端。

Comet的實現主要有兩種方式:

  • 基于Ajax的長輪詢(long-polling)方式

  • 基于 Iframe 及 htmlfile 的流(http streaming)方式

Iframe是html標記,這個標記的src屬性會保持對指定服務器的長連接請求,服務器端則可以不停地返回數據,相對于第一種方式,這種方式跟傳統的服務器推則更接近。在第一種方式中,瀏覽器在收到數據后會直接調用JS回調函數,但是這種方式該如何響應數據呢?可以通過在返回數據中嵌入JS腳本的方式,如“

”,服務器端將返回的數據作為回調函數的參數,瀏覽器在收到數據后就會執行這段JS腳本。

3.4 Websocket---未來的解決方案

如果說Ajax的出現是互聯網發展的必然,那么Comet技術的出現則更多透露出一種無奈,僅僅作為一種hack技術,因為沒有更好的解決方案。Comet解決的問題應該由誰來解決才是合理的呢?瀏覽器,html標準,還是http標準?主角應該是誰呢?本質上講,這涉及到數據傳輸方式,http協議應首當其沖,是時候改變一下這個懶惰的協議的請求/響應模式了。

W3C給出了答案,在新一代html標準html5中提供了一種瀏覽器和服務器間進行全雙工通訊的網絡技術Websocket。從Websocket草案得知,Websocket是一個全新的、獨立的協議,基于TCP協議,與http協議兼容、卻不會融入http協議,僅僅作為html5的一部分。于是乎腳本又被賦予了另一種能力:發起websocket請求。這種方式我們應該很熟悉,因為Ajax就是這么做的,所不同的是,Ajax發起的是http請求而已。

4. websocket邏輯

與http協議不同的請求/響應模式不同,Websocket在建立連接之前有一個Handshake(Opening Handshake)過程,在關閉連接前也有一個Handshake(Closing Handshake)過程,建立連接之后,雙方即可雙向通信。在websocket協議發展過程中前前后后就出現了多個版本的握手協議,這里分情況說明一下:

  • 基于flash的握手協議

    使用場景是IE的多數版本,因為IE的多數版本不都不支持WebSocket協議,以及FF、CHROME等瀏覽器的低版本,還沒有原生的支持WebSocket。此處,server唯一要做的,就是準備一個WebSocket-Location域給client,沒有加密,可靠性很差。

客戶端請求:

GET /ls HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: www.qixing318.com
Origin: http://www.qixing318.com

服務器返回:

HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://www.qixing318.com
WebSocket-Location: ws://www.qixing318.com/ls
  • 基于md5加密方式的握手協議客戶端請求:

    GET /demo HTTP/1.1

    Host: example.com

    Connection: Upgrade

    Sec-WebSocket-Key2:

    Upgrade: WebSocket

    <p> Origin: <a href="/misc/goto?guid=4959659912411222758" rel="nofollow,noindex" target="_blank">http://www.qixing318.com</a> </p>
    
    <p>[8-byte security key]</p>
    
    

    </div> </li>

    </ul>

    服務端返回:

    HTTP/1.1 101 WebSocket Protocol Handshake
    Upgrade: WebSocket
    Connection: Upgrade
    WebSocket-Origin: http://www.qixing318.com
    WebSocket-Location: ws://example.com/demo
    [16-byte hash response]

    其中 Sec-WebSocket-Key1,Sec-WebSocket-Key2 和 [8-byte security key] 這幾個頭信息是web server用來生成應答信息的來源,依據 draft-hixie-thewebsocketprotocol-76 草案的定義。web server基于以下的算法來產生正確的應答信息:

    1. 逐個字符讀取 Sec-WebSocket-Key1 頭信息中的值,將數值型字符連接到一起放到一個臨時字符串里,同時統計所有空格的數量;

    1. 將在第(1)步里生成的數字字符串轉換成一個整型數字,然后除以第(1)步里統計出來的空格數量,將得到的浮點數轉換成整數型;
    2. 將第(2)步里生成的整型值轉換為符合網絡傳輸的網絡字節數組;
    3. 對 Sec-WebSocket-Key2 頭信息同樣進行第(1)到第(3)步的操作,得到另外一個網絡字節數組;
    4. 將 [8-byte security key] 和在第(3)、(4)步里生成的網絡字節數組合并成一個16字節的數組;
    5. 對第(5)步生成的字節數組使用MD5算法生成一個哈希值,這個哈希值就作為安全密鑰返回給客戶端,以表明服務器端獲取了客戶端的請求,同意創建websocket連接</pre>

      • 基于sha加密方式的握手協議

        也是目前見的最多的一種方式,這里的版本號目前是需要13以上的版本。

        客戶端請求:

        GET /ls HTTP/1.1

        Upgrade: websocket

        Connection: Upgrade

        Host: www.qixing318.com

        Sec-WebSocket-Origin: http://www.qixing318.com

        Sec-WebSocket-Key: 2SCVXUeP9cTjV+0mWB8J6A==

        Sec-WebSocket-Version: 13

      服務器返回:

      HTTP/1.1 101 Switching Protocols
      Upgrade: websocket
      Connection: Upgrade
      Sec-WebSocket-Accept: mLDKNeBNWz6T9SxU+o0Fy/HgeSw=

      其中 server就是把客戶端上報的key拼上一段GUID( “258EAFA5-E914-47DA-95CA-C5AB0DC85B11″),拿這個字符串做SHA-1 hash計算,然后再把得到的結果通過base64加密,最后再返回給客戶端。

      4.1 Opening Handshake:

      客戶端發起連接Handshake請求

      GET /chat HTTP/1.1
      Host: server.example.com
      Upgrade: websocket
      Connection: Upgrade
      Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
      Origin: http://example.com
      Sec-WebSocket-Protocol: chat, superchat
      Sec-WebSocket-Version: 13

      服務器端響應:

      HTTP/1.1 101 Switching Protocols
      Upgrade: websocket
      Connection: Upgrade
      Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
      Sec-WebSocket-Protocol: chat

      • Upgrade:WebSocket
        表示這是一個特殊的 HTTP 請求,請求的目的就是要將客戶端和服務器端的通訊協議從 HTTP 協議升級到 WebSocket 協議。
      • Sec-WebSocket-Key
        是一段瀏覽器base64加密的密鑰,server端收到后需要提取Sec-WebSocket-Key 信息,然后加密。
      • Sec-WebSocket-Accept服務器端在接收到的Sec-WebSocket-Key密鑰后追加一段神奇字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,并將結果進行sha-1哈希,然后再進行base64加密返回給客戶端(就是Sec-WebSocket-Key)。 比如:

        function encry($req)
        {
         $key = $this->getKey($req);
         $mask = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 
         # 將 SHA-1 加密后的字符串再進行一次 base64 加密
         return base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
        }

        如果加密算法錯誤,客戶端在進行校檢的時候會直接報錯。如果握手成功,則客戶端側會出發onopen事件。

      • Sec-WebSocket-Protocol
        表示客戶端請求提供的可供選擇的子協議,及服務器端選中的支持的子協議,“Origin”服務器端用于區分未授權的websocket瀏覽器
      • Sec-WebSocket-Version: 13
        客戶端在握手時的請求中攜帶,這樣的版本標識,表示這個是一個升級版本,現在的瀏覽器都是使用的這個版本。
      • HTTP/1.1 101 Switching Protocols101為服務器返回的狀態碼,所有非101的狀態碼都表示handshake并未完成。

      4.2 Data Framing

      Websocket協議通過序列化的數據幀傳輸數據。數據封包協議中定義了opcode、payload length、Payload data等字段。其中要求:

      1. 客戶端向服務器傳輸的數據幀必須進行掩碼處理:服務器若接收到未經過掩碼處理的數據幀,則必須主動關閉連接。
      2. 服務器向客戶端傳輸的數據幀一定不能進行掩碼處理。客戶端若接收到經過掩碼處理的數據幀,則必須主動關閉連接。

      針對上情況,發現錯誤的一方可向對方發送close幀(狀態碼是1002,表示協議錯誤),以關閉連接。具體數據幀格式如下圖所示:

      • FIN
        標識是否為此消息的最后一個數據包,占 1 bit
      • RSV1, RSV2, RSV3: 用于擴展協議,一般為0,各占1bit
      • Opcode
        數據包類型(frame type),占4bits
        0x0:標識一個中間數據包
        0x1:標識一個text類型數據包
        0x2:標識一個binary類型數據包
        0x3-7:保留
        0x8:標識一個斷開連接類型數據包
        0x9:標識一個ping類型數據包
        0xA:表示一個pong類型數據包
        0xB-F:保留
      • MASK:占1bits
        用于標識PayloadData是否經過掩碼處理。如果是1,Masking-key域的數據即是掩碼密鑰,用于解碼PayloadData。客戶端發出的數據幀需要進行掩碼處理,所以此位是1。
      • Payload length
        Payload data的長度,占7bits,7+16bits,7+64bits:
        • 如果其值在0-125,則是payload的真實長度。
        • 如果值是126,則后面2個字節形成的16bits無符號整型數的值是payload的真實長度。注意,網絡字節序,需要轉換。
        • 如果值是127,則后面8個字節形成的64bits無符號整型數的值是payload的真實長度。注意,網絡字節序,需要轉換。

      這里的長度表示遵循一個原則,用最少的字節表示長度(盡量減少不必要的傳輸)。舉例說,payload真實長度是124,在0-125之間,必須用前7位表示;不允許長度1是126或127,然后長度2是124,這樣違反原則。

      • Payload data應用層數據

        server解析client端的數據

        接收到客戶端數據后的解析規則如下:

      • 1byte
        • 1bit: frame-fin,x0表示該message后續還有frame;x1表示是message的最后一個frame
        • 3bit: 分別是frame-rsv1、frame-rsv2和frame-rsv3,通常都是x0
        • 4bit: frame-opcode,x0表示是延續frame;x1表示文本frame;x2表示二進制frame;x3-7保留給非控制frame;x8表示關 閉連接;x9表示ping;xA表示pong;xB-F保留給控制frame
      • 2byte
        • 1bit: Mask,1表示該frame包含掩碼;0表示無掩碼
        • 7bit、7bit+2byte、7bit+8byte: 7bit取整數值,若在0-125之間,則是負載數據長度;若是126表示,后兩個byte取無符號16位整數值,是負載長度;127表示后8個 byte,取64位無符號整數值,是負載長度
        • 3-6byte: 這里假定負載長度在0-125之間,并且Mask為1,則這4個byte是掩碼
        • 7-end byte: 長度是上面取出的負載長度,包括擴展數據和應用數據兩部分,通常沒有擴展數據;若Mask為1,則此數據需要解碼,解碼規則為- 1-4byte掩碼循環和數據byte做異或操作。

      示例代碼:

      /// 解析客戶端數據包
      /// <param name="recBytes">服務器接收的數據包</param>
      /// <param name="recByteLength">有效數據長度</param> 
      private static string AnalyticData(byte[] recBytes, int recByteLength)
      {
       if(recByteLength < 2)
       {

       return string.Empty;
      

      }

      bool fin = (recBytes[0] & 0x80) == 0x80; // 1bit,1表示最后一幀 if(!fin) {

       return string.Empty;// 超過一幀暫不處理
      

      }

      bool mask_flag = (recBytes[1] & 0x80) == 0x80; // 是否包含掩碼 if(!mask_flag) {

       return string.Empty;// 不包含掩碼的暫不處理
      

      }

      int payload_len = recBytes[1] & 0x7F; // 數據長度

      byte[] masks = new byte[4]; byte[] payload_data;

      if(payload_len == 126) {

       Array.Copy(recBytes, 4, masks, 0, 4);
       payload_len = (UInt16)(recBytes[2] << 8 | recBytes[3]);
       payload_data = new byte[payload_len];
       Array.Copy(recBytes, 8, payload_data, 0, payload_len);
      
      

      } else if(payload_len == 127) {

       Array.Copy(recBytes, 10, masks, 0, 4);
       byte[] uInt64Bytes = new byte[8];
       for(int i = 0; i < 8; i++)
       {
           uInt64Bytes[i] = recBytes[9 - i];
       }
       UInt64 len = BitConverter.ToUInt64(uInt64Bytes, 0);
      
       payload_data = new byte[len];
       for(UInt64 i = 0; i < len; i++)
       {
           payload_data[i] = recBytes[i + 14];
       }
      

      } else {

       Array.Copy(recBytes, 2, masks, 0, 4);
       payload_data = new byte[payload_len];
       Array.Copy(recBytes, 6, payload_data, 0, payload_len);
      
      

      }

      for(var i = 0; i < payload_len; i++) {

       payload_data[i] = (byte)(payload_data[i] ^ masks[i % 4]);
      

      } return Encoding.UTF8.GetString(payload_data); }</pre>

      server發送數據至client

      服務器發送的數據以0x81開頭,緊接發送內容的長度(若長度在0-125,則1個byte表示長度;若長度不超過0xFFFF,則后2個byte 作為無符號16位整數表示長度;若超過0xFFFF,則后8個byte作為無符號64位整數表示長度),最后是內容的byte數組。示例代碼:

      /// 打包服務器數據
      /// <param name="message">數據</param>
      /// <returns>數據包</returns>
      private static byte[] PackData(string message)
      {
       byte[] contentBytes = null;
       byte[] temp = Encoding.UTF8.GetBytes(message);

      if(temp.Length < 126) {

       contentBytes = new byte[temp.Length + 2];
       contentBytes[0] = 0x81;
       contentBytes[1] = (byte)temp.Length;
       Array.Copy(temp, 0, contentBytes, 2, temp.Length);
      

      } else if(temp.Length < 0xFFFF) {

       contentBytes = new byte[temp.Length + 4];
       contentBytes[0] = 0x81;
       contentBytes[1] = 126;
       contentBytes[2] = (byte)(temp.Length & 0xFF);
       contentBytes[3] = (byte)(temp.Length >> 8 & 0xFF);
       Array.Copy(temp, 0, contentBytes, 4, temp.Length);
      

      } else {

       // 暫不處理超長內容
      

      }

      return contentBytes; }</pre>

      4.3 Closing Handshake

      相對于Opening Handshake,Closing Handshake則簡單得多,主動關閉的一方向另一方發送一個關閉類型的數據包,對方收到此數據包之后,再回復一個相同類型的數據包,關閉完成。

      關閉類型數據包遵守封包協議,Opcode為0x8,Payload data可以用于攜帶關閉原因或消息。

      4.4 websocket的事件響應

      以上的Opening Handshake、Data Framing、Closing Handshake三個步驟其實分別對應了websocket的三個事件:

      • onopen 當接口打開時響應
      • onmessage 當收到信息時響應
      • onclose 當接口關閉時響應

      任何程序語言的websocket api都至少要提供上面三個事件的api接口, 有的可能還提供的有onerror事件的處理機制。

      websocket 在任何時候都會處于下面4種狀態中的其中一種:

      • CONNECTING (0):表示還沒建立連接;
      • OPEN (1): 已經建立連接,可以進行通訊;
      • CLOSING (2):通過關閉握手,正在關閉連接;
      • CLOSED (3):連接已經關閉或無法打開;

      5. 如何使用websocket

      客戶端

      在支持WebSocket的瀏覽器中,在創建socket之后。可以通過onopen,onmessage,onclose即onerror四個事件實現對socket進行響應

      一個簡單是示例:

      var ws = new WebSocket(“ws://localhost:8080”);
      ws.onopen = function()
      {
      console.log(“open”);
      ws.send(“hello”);
      };
      ws.onmessage = function(evt)  {  console.log(evt.data); };
      ws.onclose   = function(evt)  {  console.log(“WebSocketClosed!”); };
      ws.onerror   = function(evt)  {  console.log(“WebSocketError!”); };

      首先申請一個WebSocket對象,參數是需要連接的服務器端的地址,同http協議使用http://開頭一樣,WebSocket協議的URL使用ws://開頭,另外安全的WebSocket協議使用wss://開頭。

      client先發起握手請求:

      GET /echobot HTTP/1.1
      Host: 192.168.14.215:9000
      Connection: Upgrade
      Pragma: no-cache
      Cache-Control: no-cache
      Upgrade: websocket
      Origin: http://192.168.14.215
      Sec-WebSocket-Version: 13
      User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36
      Accept-Encoding: gzip, deflate, sdch
      Accept-Language: zh-CN,zh;q=0.8
      Sec-WebSocket-Key: mh3xLXeRuIWNPwq7ATG9jA==
      Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

      服務端響應:

      HTTP/1.1 101 Switching Protocols
      Upgrade: websocket
      Connection: Upgrade
      Sec-WebSocket-Accept: SIEylb7zRYJAEgiqJXaOW3V+ZWQ=

      交互數據:

      ws.send(“hello”);   # 用于將消息發送到服務端
      ws.recv($buffer);   # 用于接收服務端的消息

      6. 自己如何實現websocket server和client

      我分別用C++、PHP、Python語言實現了websocket server和client, 只支持基本功能,也是為了加深理解websocket協議內容。

      所有源代碼放在github上,點此查看: websocket server & client 分別用C++/PHP/Python實現 , 如何使用、測試及集成自己的邏輯也在文檔中進行了說明,這里不再列出了。

      7. reference

      Ajax、Comet與Websocket

      WebSocket使用教程

      分析HTML5中WebSocket的原理

      WebScoket 規范 + WebSocket 協議

      websocket規范 RFC6455 中文版

      </div>

     本文由用戶 zorro47 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
     轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
     本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!