WebSocket 協議

參考文章

websocket RFC github 中文翻譯php

Websocket RFC 文檔git

workerman websocket 協議實現github

協議組成

協議由一個開放握手組成,其次是基本的消息成幀,分層的TCP.web

解決的問題

基於瀏覽器的機制,實現客戶端與服務端的雙向通訊.瀏覽器

協議概述

  1. 來自客戶端握手
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
  1. 來自服務端的握手
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
// 可選的頭,表示容許的經過的客戶端
Sec-WebSocket-Protocol: chat

以上,頭順序無所謂.服務器

一旦客戶端和服務器都發送了握手信號,若是握手成功,數據傳輸部分啓動。這是雙方溝通的渠道,獨立於另外一方,可隨意發送數據。websocket

服務器的響應,不是隨意的,須要遵循必定的規則 請參考RFC 文檔 第 6/7頁:cookie

  1. 獲取客戶端請求的 Sec-Weboscket-Key 字段值,去除收尾空白字符
  2. 與全球惟一標識符拼接 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
  3. sha1 加密(短格式)
  4. base64 加密

PHP 程序描述:網絡

$client_key = 'dGhlIHNhbXBsZSBub25jZQ==';
$client_key = trim($client_key);
$guid       = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
$key        = $client_key . $guid;
$key        = sha1($key , true);
$key        = base64_encode($key);

上述結果得出的值便是服務端返回給客戶端握手的 Sec-Websocket-Accept 頭字段值.框架

關閉連接

接收到一個 0x8 控制幀後,連接也許當即斷開,也許在接收完剩下的數據後斷開。

  • 能夠有消息體,指明消息緣由,可做爲日誌進行記錄。
  • 應用發送關閉幀後必須不在發送更多數據幀。
  • 若是一個端點接受到一個關閉幀且先前沒有發送關閉幀,則必須發送一個關閉幀。
  • 端點在接受到關閉幀後,能夠延遲響應關閉幀,繼續發送或接受數據幀,但不保證一個已經發送關閉幀的端點繼續處理數據。
  • 發送並接收了關閉幀的端點,被認爲是關閉了 websocket 鏈接,其必須關閉底層的 TCP 鏈接。

設計理念

基於框架而不是基於流/文本或二進制幀.

連接要求

針對客戶端要求

  • 握手必須是一個有效的 HTTP 請求
  • 請求的方法必須爲 GET,且 HTTP 版本必須是 1.1
  • 請求的 REQUEST-URI 必須符合文檔規定的要求(詳情查看 Page 13)
  • 請求必須包含 Host
  • 請求必須包含 Upgrade: websocket 頭,值必須爲 websocket
  • 請求必須包含 Connection: Upgrade 頭,值必須爲 Upgrade
  • 請求必須包含 Sec-WebSocket-Key
  • 請求必須包含 Sec-WebSocket-Version: 13 頭,值必須爲 13
  • 請求必須包含 Origin
  • 請求可能包含 Sec-WebSocket-Protocol 頭,規定子協議
  • 請求可能包含 Sec-WebSocket-Extensions ,規定協議擴展
  • 請求可能包含其餘字段,如 cookie

不符合上述要求的服務器響應,客戶端都會斷開連接.

  • 若是響應不包含 Sec-WebSocket-Protocol 中指定的子協議,客戶端斷開
  • 若是響應 HTTP/1.1 101 Switching Protocols 狀態碼不是 101,客戶端斷開

針對服務端要求

  • 若是請求是 HTTP/1.1 或更高的 GET 請求,包含 REQUEST-URI 則應正確地按照文檔要求進行解析.
  • 必須驗證 Host 字段
  • Upgrade 頭字段值必須是大小寫不敏感的 websocket
  • Sec-WebSocket-keyd 解碼時長度爲 16Byte
  • Sec-WebSocket-Version 值必須是 13
  • Host 若是沒有被包含,則連接不該該被解釋爲瀏覽器發起的行爲
  • Sec-WebSocket-Protocol 中列出的客戶端請求的子協議,服務端應按照優先順序排列,響應
  • 任選的其餘字段

響應要求:

  • 驗證 Origin 字段,若是不符合要求的請求則返回適當的錯誤代碼(例如:403)
  • Sec-WebSocket-Key 值是一個 base64 加密後的值,服務端不須要對其進行解碼,而僅是用來建立服務器的握手.
  • 驗證 Sec-WebSocket-Version 值,若是不是 13,則返回一個適當的錯誤代碼(例如:HTTP/1.1 426 Upgrade Required)
  • 資源名驗證
  • 子協議驗證
  • extensions 驗證

若是經過了上述驗證,則服務器表示接受該連接.那麼起響應必須符合如下要求詳情查看 Page 23:

  1. 必須,狀態行 HTTP/1.1 101 Switching Protocols
  2. 必須,協議升級頭 Upgrade: websocket
  3. 必須,表示鏈接升級的頭字段 Connection: Upgrade
  4. 必須,Sec-WebSocket-Accept 頭字段,詳情請查閱 協議概述 部分
  5. 可選:Sec-WebSocket-Protocols 頭部

完整的響應代碼以下(嚴格按照以下格式響應!!頭部順序無所謂!關鍵是後面的換行符注意了!嚴格控制數量!):

HTTP/1.1 101 Switching Protocols\r\n
Connection: Upgrade\r\n
Upgrade: websocket\r\n
Sec-WebSocket-Accept: 3nlEzv+LqVBYnTHclAqtk62uOTQ=\r\n
// 下面這個頭字段爲可選字段
Sec-WebSocket-Protocols: chat\r\n\r\n

基本框架協議

數據傳輸部分對 進行了分組!!因爲是在bit層面上進行的數據封裝,因此若是直接取出的話,獲取到的將是處理後的數據,須要解密。下圖是傳輸數據格式

0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+

1. 特殊名詞含義介紹

  1. 1bit,FIN
  2. 每一個 1bit, RSV一、RSV二、RSV3
  3. 4bit,opcode(如下定義在ABNF中)

    • %x0 連續幀
    • %x1 文本幀
    • %x2 二進制幀
    • %x3 - %x7 保留幀
    • %x8 連接關閉
    • %x9 ping
    • %xA pong
    • %xB-F 保留的控制幀
    • 以上表示的都是 16 進制數值
  4. 1bit, mask

    • 客戶端發送給服務端的數據都須要設置爲 1
    • 也就是說數據都是通過掩碼處理過的
  5. 7bit、7 + 16bit、7 + 64bit,Payload length 具體範圍請參閱 RFC 文檔(Page 31)

    • Playload length = Extended Payload length + Application Payload length
    • 有效載荷長度 = 擴展數據長度 + 應用程序數據長度
    • 擴展數據長度有可能爲 0,因此當 擴展數據長度 = 0 的時候,有效載荷長度 = 應用程序長度
    • 有效載荷數據的長度單位爲 Byte
  6. 0/4 byte, masking-key

    • 客戶端發送給服務端的數據都是通過掩碼處理的,長度爲 32bit
    • 服務端發送給客戶端的數據都是未通過掩碼處理的,長度爲 0bit
  7. x + y Byte, Payload Data

    • 有效載荷數據
  8. x Byte, Extension Data

    • 擴展數據
  9. y Byte, Application Data

    • 應用數據

2. 理解

圖中表示遵循 websocket 協議進行傳輸的數據,因爲是通過 websocket 協議處理後的數據,因此沒法直接獲取有效數據。若是想要獲取有效數據,就須要按照 websocket 協議規定進行解讀。

圖中從左往右,按高位到低位進行排列。

什麼是低位、高位??

就像是十進制數字,若是有一個描述是這樣的:3表示個位,2 表示十位,1表示百位,請問這個數字是??答案:123

這就很好理解了,個位、十位、百位 描述了排列順序;一樣的,在程序領域,低位到高位描述的也是排列順序!不過 個位、十位、百位描述的是10進制的排列順序,而 低位、高位描述的是 2進制 的排列順序,具體描述是 位0、位一、位2.... 等(當前舉例中的的排列順序爲低位到高位),如下是圖片描述:

描述

理解了低位、高位,就清楚了上圖描述的數據排列順序。

衆所周知,位(bit)是內存中的最小存儲單位,僅能存 0、1兩個數值。因此要想獲取、設置某位的值,須要進行位操做。因爲是在上進行操做者,因此,圖中描述的內容是在補碼的基礎上進行的。

客戶端發送給服務端的數據是通過掩碼處理的! 須要進行解析,解析數據流程:

// 按照 websocket 規範解析客戶端加密數據
function decode(string $buffer){
    // buffer[0] 獲取第一個字節,8bit
    // 對照那張圖,表示的是 fin + rsv1 + rsv2 + rsv 3 + opcode
    // 之因此要轉換爲 ASCII 碼值
    // 是爲了確保位運算結果正確!
    // php 位運算詳情參考:https://note.youdao.com/share/?id=927bfc2f40a8d62f4c9165de30a41e75&type=note#/
    // 這邊作一點簡單解釋
    // 後面的代碼會有 $first_byte >> 7 這樣的代碼
    // php 中 << >> 都會將操做數當成是整型數(int) 
    // 因此若是不轉換成 ascii 值的話,過程將會是
    // (int) $buffer[0] >> 7
    // 這樣的結果將是錯誤的!!
    // ord((int) $buffer[0]) !== ord($buffer[0]) 就是最好的證實
    // 由於 ascii 值不同,則二進制值(嚴格一點,我認爲應該說成是:補碼)也不同
    // 這違反了 websocket 規定的協議
    // 會致使解析錯誤
    $first_byte  = ord($buffer[0]);
    // buffer[1] 獲取第二個字節,8bit
    // 對照那張圖,表示的是 mask + payload len
    $second_byte = ord($buffer[1]);
    
    // 獲取左邊第一位值
    $fin = $first_byte >> 7;
    // 對照那張圖,要想獲取 payload len 表示的值
    // 須要設置 位 7 爲 0
    // 由於位 7 表示的是掩碼,位 0 - 6 表示的是 paylaod len 的補碼
    // 因此要想獲取 payload len 的值
    // 0111 1111 => 127
    $payload_len = $second_byte & 127;
    
    // 客戶端發送給服務端的數據是通過掩碼處理的
    // 因此要獲取 掩碼鍵 + 掩碼處理事後的客戶端數據
    // 獲取 mask-key + payload data
    if ($payload_len === 127) {
        // 若是 payload len = 127 byte
        // payload len 自己佔據 7bit
        // extended payload lenght 佔據 64bit
        $mask_key       = substr($buffer , 10 , 4);
        $encoded_data   = substr($buffer , 14);
    } else if ($payload_len === 126) {
        // 若是 payload len = 126 byte
        // payload length 自己佔據 7bit
        // extended payload lenght 佔據 16bit
        $mask_key       = substr($buffer , 4 , 4);
        $encoded_data   = substr($buffer , 8);
    } else {
        // 若是 payload len = 126 byte
        // payload length 自己佔據 7bit
        // extended payload lenght 佔據 0bit
        $mask_key       = substr($buffer , 2 , 4);
        $encoded_data   = substr($buffer , 6);
    }
    
    // 對 payload data 進行解碼
    $decoded_data = "";
    
    // 對每個有效載荷數據進行解碼操做
    // 解碼規則在 RFC 文檔中有詳細描述
    for ($index = 0; $index < count($encoded_data); ++$index)
    {
        $k              = $index % 4;
        $valid_data     = $encoded_data[$index] ^ $mask_data[$k];
        $decoded_data  .= $valid_data;
    }
    
    // 這個就是客戶端發送的真實數據!!
    return $decoded_data;
}

相反,若是服務器想要發送數據給 websocket 客戶端,則也要對數據進行相應處理!處理流程:

// 按照 websocket 規範封裝發送給客戶端的消息
function encode($msg){
    if (!is_scalar($msg)) {
        print_r("只容許發送標量數據");
    }
    
    // 數據長度
    $len = strlen($msg);
    
    // 這邊僅實現傳輸文本幀!第一個字節,文本幀 1000 0001 => 129
    // 若是須要例如二進制幀,用於傳輸大文件,請另行實現
    $first_byte = chr(129);
    
    if ($len <= 125) {
        // payload length = 7bit 支持的最大範圍!
        $second_byte = chr($len);
    } else {
        if ($len <= 65535) {
            // payload length = 7 , extended payload length = 16bit,支持的最大範圍 65535
            // 最後16bit 被解釋爲無符號整數,排序爲:大端字節序(網絡字節序)
            $second_byte = chr(126) . pack('n' , $len);
        } else {
            // payload length = 7,extended payload length = 64bit
            // 最後 64 位被解釋爲無符號整數,大端字節序(網絡字節序)
            $second_byte = chr(127) . pack('J' , $len);
        }
    }
    
    // 注意了,發送給客戶端的數據不須要處理
    // 詳情查看 websocket 文檔!!
    $encoded_data = $first_byte . $second_byte . $buffer;
    
    // 這個就是發送給客戶端的數據!   
    return $encoded_data;
}

消息分片

分片目的

消息分片的主要目的是容許消息開始但沒必要緩衝整個消息時,發送一個未知大小的消息;未分片的消息須要緩衝整個消息,以便獲取消息大小;

分片要求:

  • 首個分片 Fin = 0,opcode != 0x0,其後跟隨多個 Fin = 0,opcode = 0x0的分片,終止於 Fin = 1,opcode = 0x0的片斷
  • 擴展數據可能發生在分片中的任意一個分片中
  • 控制幀可能被注入到分片消息的中間,控制幀自己必須不被分割
  • 消息分片必須按照發送者發送順序交付給收件人
  • 片斷中的一個消息必須不能與片斷中的另外一個消息交替,除非已協商了一個能解釋交替的擴展。
  • websocket服務器應可以處理分片消息中間的控制幀
  • 一個發送者能夠爲非控制消息(非控制幀)建立任何大小的片斷
  • 不能處理控制幀
  • 若是使用了任何保留的位值且這些值的意思對中間件是未知的,一箇中間件必須不改變一個消息的分片。
  • 在一個鏈接上下文中,已經協商了擴展且中間件不知道協商的擴展的語義,一箇中間件必須不改變任何消息的分片。一樣,沒有看見WebSocket握手(且沒被通知有關它的內容)、致使一個WebSocket鏈接的一箇中間件,必須不改變這個連接的任何消息的分片。
  • 因爲這些規則,一個消息的全部分片是相同類型,以第一個片斷的操做碼設置。由於控制幀不能被分片,用於一個消息中的全部分片的類型必須或者是文本、或者二進制、或者一個保留的操做碼。

ping

接受到一個 ping(0x9) 控制幀,必須返回一個 pong(0xa) 控制幀,表示進程還在!!實際就是心跳檢查

pong

  1. 能夠在接收到 ping(0x9) 控制幀後,做爲響應消息返回。
  2. 也能夠單向發送 pong 幀,表示發送方進程還在,做爲單向心跳

狀態碼

  1. 1000,正常關閉
  2. 1001,正在離開
  3. 1003,正在關閉鏈接
  4. 1004,保留
  5. 1005,保留
  6. 1006,保留
  7. 1007,端點正在終止鏈接,由於它收到的消息中沒有與消息類型一致。
  8. 1008,端點正在終止連接,由於接收到了違反其規則的消息。
  9. 1009,端點正在終止連接,由於接受到的消息太大
  10. 1010,端點正在終止連接,由於擴展問題
  11. 1011,端點正在終止連接,發生了之外錯誤
  12. 1015,保留
  13. .....省略了部分,詳情參考 rfc 文檔

尾部

以上我的理解,僅供參考,有錯歡迎糾正,未完待續 ....

相關文章
相關標籤/搜索