workerman websocket 協議實現github
協議由一個開放握手組成,其次是基本的消息成幀,分層的TCP.web
基於瀏覽器的機制,實現客戶端與服務端的雙向通訊.瀏覽器
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
以上,頭順序無所謂.服務器
一旦客戶端和服務器都發送了握手信號,若是握手成功,數據傳輸部分啓動。這是雙方溝通的渠道,獨立於另外一方,可隨意發送數據。websocket
服務器的響應,不是隨意的,須要遵循必定的規則 請參考RFC 文檔 第 6/7頁:cookie
Sec-Weboscket-Key
字段值,去除收尾空白字符258EAFA5-E914-47DA-95CA-C5AB0DC85B11
sha1
加密(短格式)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
鏈接。基於框架而不是基於流/文本或二進制幀.
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
則應正確地按照文檔要求進行解析.Upgrade
頭字段值必須是大小寫不敏感的 websocket
Sec-WebSocket-key
d 解碼時長度爲 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
)若是經過了上述驗證,則服務器表示接受該連接.那麼起響應必須符合如下要求詳情查看 Page 23:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept
頭字段,詳情請查閱 協議概述 部分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 ... | +---------------------------------------------------------------+
4bit,opcode(如下定義在ABNF中)
1bit, mask
7bit、7 + 16bit、7 + 64bit,Payload length 具體範圍請參閱 RFC 文檔(Page 31)
Byte
0/4 byte, masking-key
x + y Byte, Payload Data
x Byte, Extension Data
y Byte, Application Data
圖中表示遵循 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; }
消息分片的主要目的是容許消息開始但沒必要緩衝整個消息時,發送一個未知大小的消息;未分片的消息須要緩衝整個消息,以便獲取消息大小;
接受到一個 ping(0x9)
控制幀,必須返回一個 pong(0xa)
控制幀,表示進程還在!!實際就是心跳檢查
ping(0x9)
控制幀後,做爲響應消息返回。pong
幀,表示發送方進程還在,做爲單向心跳以上我的理解,僅供參考,有錯歡迎糾正,未完待續 ....