服務端的開發離不開協議,swoole的出現對於學習通訊來講,無疑是很是好的教材。很是推薦你們下載 Swoole Framework,其中包含了多種協議的php實現,例如FTP,HTTP,Websocket等。本文大部分代碼都是受這個項目的啓發,固然學習的同時別忘了star一下這個項目。筆者自己計算機基礎較弱,寫這篇文章的同時也查了很多資料,若是有錯誤歡迎提出批評。php
協議分爲兩部分:握手,數據傳輸html
客戶端發出的握手信息相似:git
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
服務器返回的握手信息相似:github
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat Sec-WebSocket-Version: 13
兩段信息的第一行你們應該都比較熟悉,是HTTP協議中的Request-Line和Status-Line,RFC2616。下面接着出現的是無序的頭信息,這和HTTP協議相同。 一旦握手成功,一個雙向鏈接通道就創建了。
鏈接用於傳輸message
, message
由一個或多個frame
組成。每一個frame有一個類型,屬於同一個message的frame的類型都相同。類型包括:文本,二進制,control frame(協議層信號)等。目前一共有6種類型,10種保留類型。web
根據上面的客戶端頭信息能夠看出,握手和HTTP是兼容的。WS的握手是HTTP的"升級版本"。瀏覽器
客戶端發送的握手請求必須
1. 是一個合法的HTTP請求
2. 方法是GET
3. 頭必須包含HOST字段
4. 頭必須包含Upgrade字段,值爲websocket,能夠看做是判斷請求爲ws的標誌。
5. 頭必須包含Connection字段,值爲Upgrade。
6. 頭必須包含Sec-WebSocket-Key字段,用於驗證。
7. 若是請求來自瀏覽器,頭必須包含 Origin字段。
8. 頭必須包含Sec-WebSocket-Version字段,值爲13安全
取Sec-WebSocket-Key
字段的值,鏈接一個GUID字符串,"258EAFA5-E914-47DA-95CA-C5AB0DC85B11", sha1 hash一下,再base64_encode,獲得的值做爲字段Sec-WebSocket-Accept
的值返回給客戶端。用php代碼表示:服務器
'Sec-WebSocket-Accept' => base64_encode(sha1($key . static::GUID, true))
同時,返回的狀態設置爲101,其餘狀態都表示握手沒有成功。 Connection
,Upgrade
字段做爲HTTP升級版必須存在。一個握手返回以下:websocket
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
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 ... | +---------------------------------------------------------------+
FIN
: 1 bit, 標記是不是最後一個message的最後一個片斷RSV1
, RSV2
, RSV3
: 各 1 bit, 保留標記,都爲0Opcode
:4 bits, 是對payload data的說明,指明這個幀的類型。- Mask
: 1 bit, 指明Payload data是否被mask,若是爲1,那麼數據須要根據masking-key來unmask。客戶端發送的幀都是mask的。
- Payload length
: 7 bits 或 7+16 bits 或 7+64 bits. 若是值爲0-125,那麼該值就是payload的長度;若是爲126,那麼接下來的2個byte表示payload長度(16bit, unsigned); 若是爲127,那麼接下來的8個bytes表示payload的長度(64bit, unsigned)。
- Masking-key
: 0 或 4 bytes, 用於unmask payload data。
- Payload data
: 長度爲 Payload length, 能夠分爲 extension data + application data, 擴展數據的長度計算方法是是事先商議好的,剩餘的就是應用數據。swoole
masking-key
是客戶端隨意指定的32bit長度值。從原始數據到masked數據的方式爲:原始數據第i個字節的值 XOR masking-key的第(i%4)個字節的值
。XOR表示異或,%表示取模。
當傳遞一個未知長度的數據時,能夠不用一會兒buffer所有的數據。尤爲當數據很是大時,能夠分屢次buffer,包裝爲frame來發送。
看到這裏,咱們已經瞭解了frame的結構,是否想嘗試解析一個frame,官方文檔提供了幾段二進制數據,咱們能夠用來練習一下。我挑選了其中兩段, 代碼以下:
php<?php //A single-frame unmasked text message $data = array(0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f); //A single-frame masked text message $data2 = array(0x81, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58); handleData(toString($data)); handleData(toString($data2)); function toString(array $data) { return array_reduce($data, function($carry, $item){ return $carry .= chr($item); }); } function handleData($data){ $offset = 0; $temp = ord($data[$offset++]); $FIN = ($temp >> 7) & 0x1; $RSV1 = ($temp >> 6) & 0x1; $RSV2 = ($temp >> 5) & 0x1; $RSV3 = ($temp >> 4) & 0x1; $opcode = $temp & 0xf; echo "First byte: FIN is $FIN, RSV1-3 are $RSV1, $RSV2, $RSV3; Opcode is $opcode \n"; $temp = ord($data[$offset++]); $mask = ($temp >> 7) & 0x1; $payload_length = $temp & 0x7f; if($payload_length == 126){ $temp = substr($data, $offset, 2); $offset += 2; $temp = unpack('nl', $temp); $payload_length = $temp['l']; }elseif($payload_length == 127){ $temp = substr($data, $offset, 8); $offset += 8; $temp = unpack('nl', $temp); $payload_length = $temp['l']; } echo "mask is $mask, payload_length is $payload_length \n"; if($mask ==0){ $temp = substr($data, $offset); $content = ''; for ($i=0; $i < $payload_length; $i++) { $content .= $temp[$i]; } }else{ $masking_key = substr($data, $offset, 4); $offset += 4; $temp = substr($data, $offset); $content = ''; for ($i=0; $i < $payload_length; $i++) { $content .= chr(ord($temp[$i]) ^ ord($masking_key[$i%4])); } } echo "content is $content \n"; }
結果輸出以下圖:
到這裏其實並不算完,ws協議還有不少不少規則,RFC文檔實在是太長了。好比,如何應對每一種control frame,有詳細的說明;如何關閉鏈接;協議擴展;錯誤處理;安全相關;一些基本的內容都能在swoole framework中找到對應的代碼。