摘要:可以有消息體,指明消息原因,可作為日志進(jìn)行記錄。端點(diǎn)在接受到關(guān)閉幀后,可以延遲響應(yīng)關(guān)閉幀,繼續(xù)發(fā)送或接受數(shù)據(jù)幀,但不保證一個已經(jīng)發(fā)送關(guān)閉幀的端點(diǎn)繼續(xù)處理數(shù)據(jù)。發(fā)送并接收了關(guān)閉幀的端點(diǎn),被認(rèn)為是關(guān)閉了連接,其必須關(guān)閉底層的連接。
參考文章
websocket RFC github 中文翻譯
Websocket RFC 文檔
workerman websocket 協(xié)議實(shí)現(xiàn)
協(xié)議組成協(xié)議由一個開放握手組成,其次是基本的消息成幀,分層的TCP.
解決的問題基于瀏覽器的機(jī)制,實(shí)現(xiàn)客戶端與服務(wù)端的雙向通信.
協(xié)議概述來自客戶端握手
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
來自服務(wù)端的握手
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= // 可選的頭,表示允許的通過的客戶端 Sec-WebSocket-Protocol: chat
以上,頭順序無所謂.
一旦客戶端和服務(wù)器都發(fā)送了握手信號,如果握手成功,數(shù)據(jù)傳輸部分啟動。這是雙方溝通的渠道,獨(dú)立于另一方,可隨意發(fā)送數(shù)據(jù)。
服務(wù)器的響應(yīng),不是隨意的,需要遵循一定的規(guī)則 請參考RFC 文檔 第 6/7頁:
獲取客戶端請求的 Sec-Weboscket-Key 字段值,去除收尾空白字符
與全球唯一標(biāo)識符拼接 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
sha1 加密(短格式)
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);
上述結(jié)果得出的值即是服務(wù)端返回給客戶端握手的 Sec-Websocket-Accept 頭字段值.
關(guān)閉鏈接接收到一個 0x8 控制幀后,鏈接也許立即斷開,也許在接收完剩下的數(shù)據(jù)后斷開。
可以有消息體,指明消息原因,可作為日志進(jìn)行記錄。
應(yīng)用發(fā)送關(guān)閉幀后必須不在發(fā)送更多數(shù)據(jù)幀。
如果一個端點(diǎn)接受到一個關(guān)閉幀且先前沒有發(fā)送關(guān)閉幀,則必須發(fā)送一個關(guān)閉幀。
端點(diǎn)在接受到關(guān)閉幀后,可以延遲響應(yīng)關(guān)閉幀,繼續(xù)發(fā)送或接受數(shù)據(jù)幀,但不保證一個已經(jīng)發(fā)送關(guān)閉幀的端點(diǎn)繼續(xù)處理數(shù)據(jù)。
發(fā)送并接收了關(guān)閉幀的端點(diǎn),被認(rèn)為是關(guān)閉了 websocket 連接,其必須關(guān)閉底層的 TCP 連接。
設(shè)計理念基于框架而不是基于流/文本或二進(jìn)制幀.
鏈接要求 針對客戶端要求握手必須是一個有效的 HTTP 請求
請求的方法必須為 GET,且 HTTP 版本必須是 1.1
請求的 REQUEST-URI 必須符合文檔規(guī)定的要求(詳情查看 Page 13)
請求必須包含 Host 頭
請求必須包含 Upgrade: websocket 頭,值必須為 websocket
請求必須包含 Connection: Upgrade 頭,值必須為 Upgrade
請求必須包含 Sec-WebSocket-Key 頭
請求必須包含 Sec-WebSocket-Version: 13 頭,值必須為 13
請求必須包含 Origin 頭
請求可能包含 Sec-WebSocket-Protocol 頭,規(guī)定子協(xié)議
請求可能包含 Sec-WebSocket-Extensions ,規(guī)定協(xié)議擴(kuò)展
請求可能包含其他字段,如 cookie 等
不符合上述要求的服務(wù)器響應(yīng),客戶端都會斷開鏈接.
如果響應(yīng)不包含 Sec-WebSocket-Protocol 中指定的子協(xié)議,客戶端斷開
如果響應(yīng) HTTP/1.1 101 Switching Protocols 狀態(tài)碼不是 101,客戶端斷開
針對服務(wù)端要求如果請求是 HTTP/1.1 或更高的 GET 請求,包含 REQUEST-URI 則應(yīng)正確地按照文檔要求進(jìn)行解析.
必須驗(yàn)證 Host 字段
Upgrade 頭字段值必須是大小寫不敏感的 websocket
Sec-WebSocket-keyd 解碼時長度為 16Byte
Sec-WebSocket-Version 值必須是 13
Host 如果沒有被包含,則鏈接不應(yīng)該被解釋為瀏覽器發(fā)起的行為
Sec-WebSocket-Protocol 中列出的客戶端請求的子協(xié)議,服務(wù)端應(yīng)按照優(yōu)先順序排列,響應(yīng)
任選的其他字段
響應(yīng)要求:
驗(yàn)證 Origin 字段,如果不符合要求的請求則返回適當(dāng)?shù)腻e誤代碼(例如:403)
Sec-WebSocket-Key 值是一個 base64 加密后的值,服務(wù)端不需要對其進(jìn)行解碼,而僅是用來創(chuàng)建服務(wù)器的握手.
驗(yàn)證 Sec-WebSocket-Version 值,如果不是 13,則返回一個適當(dāng)?shù)腻e誤代碼(例如:HTTP/1.1 426 Upgrade Required)
資源名驗(yàn)證
子協(xié)議驗(yàn)證
extensions 驗(yàn)證
如果通過了上述驗(yàn)證,則服務(wù)器表示接受該鏈接.那么起響應(yīng)必須符合以下要求詳情查看 Page 23:
必須,狀態(tài)行 HTTP/1.1 101 Switching Protocols
必須,協(xié)議升級頭 Upgrade: websocket
必須,表示連接升級的頭字段 Connection: Upgrade
必須,Sec-WebSocket-Accept 頭字段,詳情請查閱 協(xié)議概述 部分
可選:Sec-WebSocket-Protocols 頭部
完整的響應(yīng)代碼如下(嚴(yán)格按照如下格式響應(yīng)!!頭部順序無所謂!關(guān)鍵是后面的換行符注意了!嚴(yán)格控制數(shù)量!):
HTTP/1.1 101 Switching Protocols Connection: Upgrade Upgrade: websocket Sec-WebSocket-Accept: 3nlEzv+LqVBYnTHclAqtk62uOTQ= // 下面這個頭字段為可選字段 Sec-WebSocket-Protocols: chat基本框架協(xié)議
數(shù)據(jù)傳輸部分對 位 進(jìn)行了分組!!由于是在bit層面上進(jìn)行的數(shù)據(jù)封裝,所以如果直接取出的話,獲取到的將是處理后的數(shù)據(jù),需要解密。下圖是傳輸數(shù)據(jù)格式:
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. 特殊名詞含義介紹
1bit,F(xiàn)IN
每個 1bit, RSV1、RSV2、RSV3
4bit,opcode(以下定義在ABNF中)
%x0 連續(xù)幀
%x1 文本幀
%x2 二進(jìn)制幀
%x3 - %x7 保留幀
%x8 鏈接關(guān)閉
%x9 ping
%xA pong
%xB-F 保留的控制幀
以上表示的都是 16 進(jìn)制數(shù)值
1bit, mask
客戶端發(fā)送給服務(wù)端的數(shù)據(jù)都需要設(shè)置為 1
也就是說數(shù)據(jù)都是經(jīng)過掩碼處理過的
7bit、7 + 16bit、7 + 64bit,Payload length 具體范圍請參閱 RFC 文檔(Page 31)
Playload length = Extended Payload length + Application Payload length
有效載荷長度 = 擴(kuò)展數(shù)據(jù)長度 + 應(yīng)用程序數(shù)據(jù)長度
擴(kuò)展數(shù)據(jù)長度有可能為 0,所以當(dāng) 擴(kuò)展數(shù)據(jù)長度 = 0 的時候,有效載荷長度 = 應(yīng)用程序長度
有效載荷數(shù)據(jù)的長度單位為 Byte
0/4 byte, masking-key
客戶端發(fā)送給服務(wù)端的數(shù)據(jù)都是經(jīng)過掩碼處理的,長度為 32bit
服務(wù)端發(fā)送給客戶端的數(shù)據(jù)都是未經(jīng)過掩碼處理的,長度為 0bit
x + y Byte, Payload Data
有效載荷數(shù)據(jù)
x Byte, Extension Data
擴(kuò)展數(shù)據(jù)
y Byte, Application Data
應(yīng)用數(shù)據(jù)
2. 理解圖中表示遵循 websocket 協(xié)議進(jìn)行傳輸?shù)臄?shù)據(jù),由于是經(jīng)過 websocket 協(xié)議處理后的數(shù)據(jù),所以無法直接獲取有效數(shù)據(jù)。如果想要獲取有效數(shù)據(jù),就需要按照 websocket 協(xié)議規(guī)定進(jìn)行解讀。
圖中從左往右,按高位到低位進(jìn)行排列。
什么是低位、高位??
就像是十進(jìn)制數(shù)字,如果有一個描述是這樣的:3表示個位,2 表示十位,1表示百位,請問這個數(shù)字是??答案:123。
這就很好理解了,個位、十位、百位 描述了排列順序;同樣的,在程序領(lǐng)域,低位到高位描述的也是排列順序!不過 個位、十位、百位描述的是10進(jìn)制的排列順序,而 低位、高位描述的是 2進(jìn)制 的排列順序,具體描述是 位0、位1、位2.... 等(當(dāng)前舉例中的的排列順序?yàn)榈臀坏礁呶?/strong>),以下是圖片描述:
理解了低位、高位,就清楚了上圖描述的數(shù)據(jù)排列順序。
眾所周知,位(bit)是內(nèi)存中的最小存儲單位,僅能存 0、1兩個數(shù)值。所以要想獲取、設(shè)置某位的值,需要進(jìn)行位操作。由于是在位上進(jìn)行操作者,所以,圖中描述的內(nèi)容是在補(bǔ)碼的基礎(chǔ)上進(jìn)行的。
客戶端發(fā)送給服務(wù)端的數(shù)據(jù)是經(jīng)過掩碼處理的! 需要進(jìn)行解析,解析數(shù)據(jù)流程:
// 按照 websocket 規(guī)范解析客戶端加密數(shù)據(jù) function decode(string $buffer){ // buffer[0] 獲取第一個字節(jié),8bit // 對照那張圖,表示的是 fin + rsv1 + rsv2 + rsv 3 + opcode // 之所以要轉(zhuǎn)換為 ASCII 碼值 // 是為了確保位運(yùn)算結(jié)果正確! // php 位運(yùn)算詳情參考:https://note.youdao.com/share/?id=927bfc2f40a8d62f4c9165de30a41e75&type=note#/ // 這邊做一點(diǎn)簡單解釋 // 后面的代碼會有 $first_byte >> 7 這樣的代碼 // php 中 << >> 都會將操作數(shù)當(dāng)成是整型數(shù)(int) // 所以如果不轉(zhuǎn)換成 ascii 值的話,過程將會是 // (int) $buffer[0] >> 7 // 這樣的結(jié)果將是錯誤的!! // ord((int) $buffer[0]) !== ord($buffer[0]) 就是最好的證明 // 因?yàn)?ascii 值不一樣,則二進(jìn)制值(嚴(yán)格一點(diǎn),我認(rèn)為應(yīng)該說成是:補(bǔ)碼)也不一樣 // 這違反了 websocket 規(guī)定的協(xié)議 // 會導(dǎo)致解析錯誤 $first_byte = ord($buffer[0]); // buffer[1] 獲取第二個字節(jié),8bit // 對照那張圖,表示的是 mask + payload len $second_byte = ord($buffer[1]); // 獲取左邊第一位值 $fin = $first_byte >> 7; // 對照那張圖,要想獲取 payload len 表示的值 // 需要設(shè)置 位 7 為 0 // 因?yàn)槲?7 表示的是掩碼,位 0 - 6 表示的是 paylaod len 的補(bǔ)碼 // 所以要想獲取 payload len 的值 // 0111 1111 => 127 $payload_len = $second_byte & 127; // 客戶端發(fā)送給服務(wù)端的數(shù)據(jù)是經(jīng)過掩碼處理的 // 所以要獲取 掩碼鍵 + 掩碼處理過后的客戶端數(shù)據(jù) // 獲取 mask-key + payload data if ($payload_len === 127) { // 如果 payload len = 127 byte // payload len 本身占據(jù) 7bit // extended payload lenght 占據(jù) 64bit $mask_key = substr($buffer , 10 , 4); $encoded_data = substr($buffer , 14); } else if ($payload_len === 126) { // 如果 payload len = 126 byte // payload length 本身占據(jù) 7bit // extended payload lenght 占據(jù) 16bit $mask_key = substr($buffer , 4 , 4); $encoded_data = substr($buffer , 8); } else { // 如果 payload len = 126 byte // payload length 本身占據(jù) 7bit // extended payload lenght 占據(jù) 0bit $mask_key = substr($buffer , 2 , 4); $encoded_data = substr($buffer , 6); } // 對 payload data 進(jìn)行解碼 $decoded_data = ""; // 對每一個有效載荷數(shù)據(jù)進(jìn)行解碼操作 // 解碼規(guī)則在 RFC 文檔中有詳細(xì)描述 for ($index = 0; $index < count($encoded_data); ++$index) { $k = $index % 4; $valid_data = $encoded_data[$index] ^ $mask_data[$k]; $decoded_data .= $valid_data; } // 這個就是客戶端發(fā)送的真實(shí)數(shù)據(jù)!! return $decoded_data; }
相反,如果服務(wù)器想要發(fā)送數(shù)據(jù)給 websocket 客戶端,則也要對數(shù)據(jù)進(jìn)行相應(yīng)處理!處理流程:
// 按照 websocket 規(guī)范封裝發(fā)送給客戶端的消息 function encode($msg){ if (!is_scalar($msg)) { print_r("只允許發(fā)送標(biāo)量數(shù)據(jù)"); } // 數(shù)據(jù)長度 $len = strlen($msg); // 這邊僅實(shí)現(xiàn)傳輸文本幀!第一個字節(jié),文本幀 1000 0001 => 129 // 如果需要例如二進(jìn)制幀,用于傳輸大文件,請另行實(shí)現(xiàn) $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 被解釋為無符號整數(shù),排序?yàn)椋捍蠖俗止?jié)序(網(wǎng)絡(luò)字節(jié)序) $second_byte = chr(126) . pack("n" , $len); } else { // payload length = 7,extended payload length = 64bit // 最后 64 位被解釋為無符號整數(shù),大端字節(jié)序(網(wǎng)絡(luò)字節(jié)序) $second_byte = chr(127) . pack("J" , $len); } } // 注意了,發(fā)送給客戶端的數(shù)據(jù)不需要處理 // 詳情查看 websocket 文檔!! $encoded_data = $first_byte . $second_byte . $buffer; // 這個就是發(fā)送給客戶端的數(shù)據(jù)! return $encoded_data; }消息分片 分片目的
消息分片的主要目的是允許消息開始但不必緩沖整個消息時,發(fā)送一個未知大小的消息;未分片的消息需要緩沖整個消息,以便獲取消息大小;
分片要求:首個分片 Fin = 0,opcode != 0x0,其后跟隨多個 Fin = 0,opcode = 0x0的分片,終止于 Fin = 1,opcode = 0x0的片段
擴(kuò)展數(shù)據(jù)可能發(fā)生在分片中的任意一個分片中
控制幀可能被注入到分片消息的中間,控制幀本身必須不被分割
消息分片必須按照發(fā)送者發(fā)送順序交付給收件人
片段中的一個消息必須不能與片段中的另一個消息交替,除非已協(xié)商了一個能解釋交替的擴(kuò)展。
websocket服務(wù)器應(yīng)能夠處理分片消息中間的控制幀
一個發(fā)送者可以為非控制消息(非控制幀)創(chuàng)建任何大小的片段
不能處理控制幀
如果使用了任何保留的位值且這些值的意思對中間件是未知的,一個中間件必須不改變一個消息的分片。
在一個連接上下文中,已經(jīng)協(xié)商了擴(kuò)展且中間件不知道協(xié)商的擴(kuò)展的語義,一個中間件必須不改變?nèi)魏蜗⒌姆制M瑯樱瑳]有看見WebSocket握手(且沒被通知有關(guān)它的內(nèi)容)、導(dǎo)致一個WebSocket連接的一個中間件,必須不改變這個鏈接的任何消息的分片。
由于這些規(guī)則,一個消息的所有分片是相同類型,以第一個片段的操作碼設(shè)置。因?yàn)榭刂茙荒鼙环制糜谝粋€消息中的所有分片的類型必須或者是文本、或者二進(jìn)制、或者一個保留的操作碼。
ping接受到一個 ping(0x9) 控制幀,必須返回一個 pong(0xa) 控制幀,表示進(jìn)程還在!!實(shí)際就是心跳檢查
pong可以在接收到 ping(0x9) 控制幀后,作為響應(yīng)消息返回。
也可以單向發(fā)送 pong 幀,表示發(fā)送方進(jìn)程還在,作為單向心跳
狀態(tài)碼1000,正常關(guān)閉
1001,正在離開
1003,正在關(guān)閉連接
1004,保留
1005,保留
1006,保留
1007,端點(diǎn)正在終止連接,因?yàn)樗盏降南⒅袥]有與消息類型一致。
1008,端點(diǎn)正在終止鏈接,因?yàn)榻邮盏搅诉`反其規(guī)則的消息。
1009,端點(diǎn)正在終止鏈接,因?yàn)榻邮艿降南⑻?/p>
1010,端點(diǎn)正在終止鏈接,因?yàn)閿U(kuò)展問題
1011,端點(diǎn)正在終止鏈接,發(fā)生了以外錯誤
1015,保留
.....省略了部分,詳情參考 rfc 文檔
尾部以上個人理解,僅供參考,有錯歡迎糾正,未完待續(xù) ....
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/107519.html
摘要:概述本文為協(xié)議的第十一章,本文翻譯的主要內(nèi)容為的相關(guān)注意事項(xiàng)。應(yīng)用協(xié)議使用這個協(xié)議規(guī)范互操作性注意事項(xiàng)使用時需要使用或者更高版本的協(xié)議。安全性注意事項(xiàng)見安全性注意事項(xiàng)一節(jié)。 概述 本文為 WebSocket 協(xié)議的第十一章,本文翻譯的主要內(nèi)容為 WebSocket 的 IANA 相關(guān)注意事項(xiàng)。 IANA 注意事項(xiàng)(協(xié)議正文) 11.1 注冊新 URI 協(xié)議 11.1.1 注冊 ws 協(xié)...
摘要:本文作為系列的第四篇內(nèi)容,將會用一個簡單的聊天應(yīng)用把整個傳輸二進(jìn)制數(shù)據(jù)類型的內(nèi)容連接起來,讓用戶對整個傳輸二進(jìn)制數(shù)據(jù)的方法有個了解。如何發(fā)送二進(jìn)制數(shù)據(jù)通過如何設(shè)計一個二進(jìn)制協(xié)議一章,我們知道了如何定義傳輸?shù)亩M(jìn)制數(shù)據(jù)格式。 概述 通過前三篇博客,我們能夠了解在通過WebSocket發(fā)送數(shù)據(jù)之前,我們需要傳遞的數(shù)據(jù)是如何變成ArrayBuffer二進(jìn)制數(shù)據(jù)的;在我們收到二進(jìn)制數(shù)據(jù)之后,我...
摘要:幀是發(fā)送數(shù)據(jù)的基本單位,下邊是它的報文格式報文內(nèi)容中規(guī)定了數(shù)據(jù)標(biāo)示操作代碼掩碼數(shù)據(jù)數(shù)據(jù)長度等格式。首先我們明白了客戶端和服務(wù)端進(jìn)行消息傳遞是這樣的客戶端將消息切割成多個幀,并發(fā)送給服務(wù)端。服務(wù)端接收消息幀,并將關(guān)聯(lián)的幀重新組裝成完整的消息。 本文概述 Web Sockets的目標(biāo)是在一個單獨(dú)的持久連接上提供全雙工、雙向通信。在Javascript創(chuàng)建了Web Socket之后,會有一個...
摘要:幀是發(fā)送數(shù)據(jù)的基本單位,下邊是它的報文格式報文內(nèi)容中規(guī)定了數(shù)據(jù)標(biāo)示操作代碼掩碼數(shù)據(jù)數(shù)據(jù)長度等格式。首先我們明白了客戶端和服務(wù)端進(jìn)行消息傳遞是這樣的客戶端將消息切割成多個幀,并發(fā)送給服務(wù)端。服務(wù)端接收消息幀,并將關(guān)聯(lián)的幀重新組裝成完整的消息。 本文概述 Web Sockets的目標(biāo)是在一個單獨(dú)的持久連接上提供全雙工、雙向通信。在Javascript創(chuàng)建了Web Socket之后,會有一個...
閱讀 3551·2021-11-08 13:15
閱讀 2107·2019-08-30 14:20
閱讀 1386·2019-08-28 18:08
閱讀 977·2019-08-28 17:51
閱讀 1484·2019-08-26 18:26
閱讀 2988·2019-08-26 13:56
閱讀 1484·2019-08-26 11:46
閱讀 2586·2019-08-23 14:22