WebSocket原理與實踐(三)--解析數據幀
1-1 理解數據幀的含義:
在WebSocket協議中,數據是通過幀序列來傳輸的。為了數據安全原因,客戶端必須掩碼(mask)它發送到服務器的所有幀,當它收到一個
沒有掩碼的幀時,服務器必須關閉連接。不過服務器端給客戶端發送的所有幀都不是掩碼的,如果客戶端檢測到掩碼的幀時,也一樣必須關閉連接。
當幀被關閉的時候,可能發送狀態碼1002(協議錯誤)。
基本幀協議如下:
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 ... | +---------------------------------------------------------------+
如上是基本幀協議,它帶有操作碼(opcode)的幀類型,負載長度,和用於 "擴展數據" 與 "應用數據" 及 它們一起定義的 "負載數據"的指定位置,
某些字節和操作碼保留用於未來協議的擴展。
FIN(1位): 是否為消息的最后一個數據幀。
RSV1,RSV2,Rsv3(每個占1位),必須是0,除非一個擴展協商為非零值定義的。Opcode
表示幀的類型(4位),例如這個傳輸的幀是文本類型還是二進制類型,二進制類型傳輸的數據可以是圖片或者語音之類的。(這4位轉換成16進制值表示的意思如下):
0x0 表示附加數據幀 0x1 表示文本數據幀 0x2 表示二進制數據幀 0x3-7 暫時無定義,為以后的非控制幀保留 0x8 表示連接關閉 0x9 表示ping 0xA 表示pong 0xB-F 暫時無定義,為以后的控制幀保留
Mask(占1位): 表示是否經過掩碼處理, 1 是經過掩碼的,0是沒有經過掩碼的。
payload length (7位+16位,或者 7位+64位),定義負載數據的長度。
1. 如果數據長度小於等於125的話,那么該7位用來表示實際數據長度。
2. 如果數據長度為126到65535(2的16次方)之間,該7位值固定為126,也就是 1111110,往后擴展2個字節(16為,第三個區塊表示),用於存儲數據的實際長度。
3. 如果數據長度大於65535, 該7位的值固定為127,也就是 1111111 ,往后擴展8個字節(64位),用於存儲數據實際長度。
Masking-key(0或者4個字節),該區塊用於存儲掩碼密鑰,只有在第二個子節中的mask為1,也就是消息進行了掩碼處理時才有,否則沒有,
所以服務器端向客戶端發送消息就沒有這一塊。
Payload data 擴展數據,是0字節,除非已經協商了一個擴展。
1-2 客戶端到服務器掩碼
WebSocket協議要求客戶端所發送的幀必須掩碼,掩碼的密鑰是一個32位的隨機值。所有數據都需要與掩碼做一次異或運算。幀頭在第二個字節的第一位表示該幀是否使用了掩碼。
WebSocket服務器接收的每個載荷在處理之前首先需要處理掩碼,解除掩碼之后,服務器將得到原始消息內容。二進制消息可以直接交付。文本消息將進行UTF-8解碼
並輸出到字符串中。
二進制位運算符知識擴展:
>> 含義是右移運算符,
右移運算符是將一個二進制位的操作數按指定移動的位數向右移動,移出位被丟棄,左邊移出的空位一律補0.
比如 11 >> 2, 意思是說將數字11右移2位。
首先將11轉換為二進制數為 0000 0000 0000 0000 0000 0000 0000 1011 , 然后把低位的最后2個數字移出,因為該數字是正數,
所以在高位補零,則得到的最終結果為:0000 0000 0000 0000 0000 0000 0000 0010,轉換為10進制是2.
<< 含義是左移運算符
左移運算符是將一個二進制位的操作數按指定移動的位數向左移位,移出位被丟棄,右邊的空位一律補0.
比如 3 << 2, 意思是說將數字3左移2位,
首先將3轉換為二進制數為 0000 0000 0000 0000 0000 0000 0000 0011 , 然后把該數字高位(左側)的兩個零移出,其他的數字都朝左平移2位,
最后在右側的兩個空位補0,因此最后的結果是 0000 0000 0000 0000 0000 0000 0000 1100,則轉換為十進制是12(1100 = 1*2的3次方 + 1*2的2字方)
注意1: 在使用補碼作為機器數的機器中,正數的符號位為0,負數的符號位為1(一般情況下).
比如:十進制數13在計算機中表示為00001101,其中第一位0表示的是符號
注意2:負數的二進制位如何計算?
比如二進制的原碼為 10010101,它的補碼怎么計算呢?
首先計算它的反碼是 01101010; 那么補碼 = 反碼 + 1 = 01101011
再來看一個列子:
-7 >> 2 意思是將數字 -7 右移2位。
負數先用它的絕對值正數取它的二進制代碼,7的二進制位為: 0000 0000 0000 0000 0000 0000 0000 0111 ,那么 -7的二進制位就是 取反,
取反后再加1,就變成補碼。
因此-7的二進制位: 1111 1111 1111 1111 1111 1111 1111 1001,
因此 -7右移2位就成 1111 1111 1111 1111 1111 1111 1111 1110 因此轉換成十進制的話 -7 >> 2 ,值就變成 -2了。
數據幀解析的程序如下代碼:(decodeDataFrame.js 代碼如下:)
var crypto = require('crypto'); var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; require('net').createServer(function(o) { var key; o.on('data', function(e) { if (!key) { key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1]; // WS的字符串 加上 key, 變成新的字符串后做一次sha1運算,最后轉換成Base64 key = crypto.createHash('sha1').update(key+WS).digest('base64'); // 輸出字段數據,返回到客戶端, o.write('HTTP/1.1 101 Switching Protocol\r\n'); o.write('Upgrade: websocket\r\n'); o.write('Connection: Upgrade\r\n'); o.write('Sec-WebSocket-Accept:' +key+'\r\n'); // 輸出空行,使HTTP頭結束 o.write('\r\n'); } else { // 數據處理 onmessage(e); } }) }).listen(8000); /* >> 含義是右移運算符, 右移運算符是將一個二進制位的操作數按指定移動的位數向右移動,移出位被丟棄,左邊移出的空位一律補0. 比如 11 >> 2, 意思是說將數字11右移2位。 首先將11轉換為二進制數為 0000 0000 0000 0000 0000 0000 0000 1011 , 然后把低位的最后2個數字移出,因為該數字是正數, 所以在高位補零,則得到的最終結果為:0000 0000 0000 0000 0000 0000 0000 0010,轉換為10進制是2. << 含義是左移運算符 左移運算符是將一個二進制位的操作數按指定移動的位數向左移位,移出位被丟棄,右邊的空位一律補0. 比如 3 << 2, 意思是說將數字3左移2位, 首先將3轉換為二進制數為 0000 0000 0000 0000 0000 0000 0000 0011 , 然后把該數字高位(左側)的兩個零移出,其他的數字都朝左平移2位, 最后在右側的兩個空位補0,因此最后的結果是 0000 0000 0000 0000 0000 0000 0000 1100,則轉換為十進制是12(1100 = 1*2的3次方 + 1*2的2字方) 注意1: 在使用補碼作為機器數的機器中,正數的符號位為0,負數的符號位為1(一般情況下). 比如:十進制數13在計算機中表示為00001101,其中第一位0表示的是符號 注意2:負數的二進制位如何計算? 比如二進制的原碼為 10010101,它的補碼怎么計算呢? 首先計算它的反碼是 01101010; 那么補碼 = 反碼 + 1 = 01101011 再來看一個列子: -7 >> 2 意思是將數字 -7 右移2位。 負數先用它的絕對值正數取它的二進制代碼,7的二進制位為: 0000 0000 0000 0000 0000 0000 0000 0111 ,那么 -7的二進制位就是 取反, 取反后再加1,就變成補碼。 因此-7的二進制位: 1111 1111 1111 1111 1111 1111 1111 1001, 因此 -7右移2位就成 1111 1111 1111 1111 1111 1111 1111 1110 因此轉換成十進制的話 -7 >> 2 ,值就變成 -2了。 */ function decodeDataFrame(e) { var i = 0, j, s, arrs = [], frame = { // 解析前兩個字節的基本數據 FIN: e[i] >> 7, Opcode: e[i++] & 15, Mask: e[i] >> 7, PayloadLength: e[i++] & 0x7F }; // 處理特殊長度126和127 if (frame.PayloadLength === 126) { frame.PayloadLength = (e[i++] << 8) + e[i++]; } if (frame.PayloadLength === 127) { i += 4; // 長度一般用4個字節的整型,前四個字節一般為長整型留空的。 frame.PayloadLength = (e[i++] << 24)+(e[i++] << 16)+(e[i++] << 8) + e[i++]; } // 判斷是否使用掩碼 if (frame.Mask) { // 獲取掩碼實體 frame.MaskingKey = [e[i++], e[i++], e[i++], e[i++]]; // 對數據和掩碼做異或運算 for(j = 0, arrs = []; j < frame.PayloadLength; j++) { arrs.push(e[i+j] ^ frame.MaskingKey[j%4]); } } else { // 否則的話 直接使用數據 arrs = e.slice(i, i + frame.PayloadLength); } // 數組轉換成緩沖區來使用 arrs = new Buffer(arrs); // 如果有必要則把緩沖區轉換成字符串來使用 if (frame.Opcode === 1) { arrs = arrs.toString(); } // 設置上數據部分 frame.PayloadLength = arrs; // 返回數據幀 return frame; } function onmessage(e) { console.log(e) e = decodeDataFrame(e); // 解析數據幀 console.log(e); // 把數據幀輸出到控制台 }
index.html代碼如下:
<html> <head> <title>WebSocket Demo</title> </head> <body> <script type="text/javascript"> var ws = new WebSocket("ws://127.0.0.1:8000"); ws.onerror = function(e) { console.log(e); }; ws.onopen = function(e) { console.log('握手成功'); ws.send('次碳酸鈷'); } </script> </body> </html>
demo還是一樣,decodeDataFrame.js 和 index.html, 先進入項目中對應的目錄后,使用node decodeDataFrame.js, 然后打開index.html后查看效果
如下:
這樣服務器接收客戶端穿過了的數據就沒問題了。