WebSocket原理與實踐(四)--生成數據幀
從服務器發往客戶端的數據也是同樣的數據幀,但是從服務器發送到客戶端的數據幀不需要掩碼的。我們自己需要去生成數據幀,解析數據幀的時候我們需要分片。
消息分片:
有時候數據需要分成多個數據包發送,需要使用到分片,也就是說多個數據幀來傳輸一個數據。比如將大數據分成多個數據包傳輸,分片的目的是允許發送未知長度的消息。
這樣做的好處是:
1. 大數據的傳輸可以分片傳輸,不用考慮到數據大小導致的長度標志位不夠的情況。
2. 和http的chunk一樣,可以邊生成數據邊傳遞消息,可以提高傳輸效率。
如果大數據不能被碎片化,那么一端就必須將消息整個載入內存緩沖之中,然后需要計算長度等操作並發送,但是有了碎片化機制,服務器端或者中間件就可以選取適用的內存緩沖長度,然后當緩沖滿了之后就發送一個消息碎片。
分片規則:
1. 如果一個消息不分片的話,那么該消息只有一幀(FIN為1,opcode非0);
2. 如果一個消息分片的話,它的構成是由起始幀(FIN為0,opcode非0),然后若干(0個或多個)幀(FIN為0,opcode為0),然后結束幀(FIN為1,opcode為0)。
注意:
1. 當前已經定義了控制幀包括 0x8(close), 0x9(Ping), 0xA(Pong). 控制幀可以出現在分片消息中間,但是控制幀不允許分片,控制幀是通過它的opcode
的最高有效位是1去確定的。
2. 組成消息的所有幀都是相同的數據類型,在第一幀中的opcode中指明。組成消息的碎片類型必須是文本,二進制,或者其他的保留類型。
下面我們來理解下上面分片規則2中的話的含義:
1. 開始幀(1個)---消息分片起始幀的構成是 (FIN為0,opcode非0);即:FIN=0, Opcode > 0;
2. 傳輸幀(0個或多個)---是由若干個(0個或多個)幀組成; 即 FIN = 0, Opcode = 0;
3. 終止幀(1個)--- FIN = 1, Opcode = 0;
還是看基本幀協議如下:
1 2 3 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 ... | +---------------------------------------------------------------+
demo解析:
比如我們現在第三節我們講到的 "解析數據幀" 里面的代碼,我們發送的消息123456789后,返回的數據部分是:
<Buffer 81 89 b0 23 52 5a 81 11 61 6e 85 15 65 62 89> { FIN: 1, Opcode: 1, Mask: 1, PayloadLength: '123456789', MaskingKey: [ 176, 35, 82, 90 ] }
上面返回的數據部分是16進制,因此我們需要他們轉換成二進制,有關16進制,10進制,2進制的轉換表如下:
16進制-->10進制-->2進制轉換查看
我們現在需要把 81 89 b0 23 52 5a 81 11 61 6e 85 15 65 62 89 這些16進制先轉換成10進制,然后轉換成二進制,分析代碼如下:
16進制(a=10, b=11, ... 依次類推)
16進制 10進制 2進制 81 8*16的1次方 + 1*16的0次方 = 129 10000001 89 8*16的1次方 + 9*16的0次方 = 137 10001001 b0 11*16的1次方 + 0*16的0次方 = 176 10110000 23 2*16的1次方 + 3*16的0次方 = 35 00100011 52 5*16的1次方 + 2*16的0次方 = 82 01010010 5a 5*16的1次方 + 10*16的0次方 = 90 01011010 81 8*16的1次方 + 1*16的0次方 = 129 10000001 11 1*16的1次方 + 1*16的0次方 = 17 00010001 61 6*16的1次方 + 1*16的0次方 = 97 00111101 6e 6*16的1次方 + 14*16的0次方 = 110 01101110 85 8*16的1次方 + 5*16的0次方 = 133 10000101 15 1*16的1次方 + 5*16的0次方 = 21 00010101 65 6*16的1次方 + 5*16的0次方 = 101 01100101 62 6*16的1次方 + 2*16的0次方 = 98 01100010 89 8*16的1次方 + 9*16的0次方 = 137 10001001
我們把上面的轉換后的二進制 對照上面的 基本幀協議表看下:
1. 先看 FIN 的含義是: 第一位是否為消息的最后一個數據幀,如果為1的話,說明是,否則為0的話就不是,那說明是最后一個數據幀。
2. 第2~4位都為0,對應的RSV(1~3), 5~8為 0001,是屬於opcode的部分了,opcode是代表是幀的類型;它有如下類型:
0x0 表示附加數據幀
0x1 表示文本數據幀
0x2 表示二進制數據幀
0x3-7 暫時無定義,為以后的非控制幀保留
0x8 表示連接關閉
0x9 表示ping
0xA 表示pong
0xB-F 暫時無定義,為以后的控制幀保留
注意:其中8進制是以0開頭的,16進制是以0x開頭的。
0001,是文本數據幀了。
3. 第九位是1,那么對應的幀協議表就是MASK部分了,Mask(占1位): 表示是否經過掩碼處理, 1 是經過掩碼的,0是沒有經過掩碼的。說明是經過掩碼處理的,
也就是說可以理解為是客戶端向服務器端發送數據的。(因為服務器端給客戶端是不需要掩碼的,否則連接中斷)。
4. 第10~16位是 0001001 = 9 < 125, 對應幀協議中的 payload length的部分了,數據長度為9,因此小於125位,因此使用7位來表示實際數據長度。
5. b0, 23, 52, 5a 對應的部分是 屬於Masking-key(0或者4個字節),該區塊用於存儲掩碼密鑰,只有在第二個子節中的mask為1,也就是消息進行了掩碼處理時才有。
6. 81 11 61 6e 85 15 65 62 89 這些就是對應表中的數據部分了。
下面我們再來理解下 消息 123456789 怎么通過掩碼加密成 81 11 61 6e 85 15 65 62 89 這些數據了。
數字字符1的ASCLL碼的16進制為31,轉換成10進制就是49了。其他的數字依次類推+1;
數字 10進制 二進制
1 49 00110001
2 50 00110010
3 51 00110011
4 52 00110100
5 53 00110101
6 54 00110110
7 55 00110111
8 56 00111000
9 57 00111001
6-1: 其中字符1的二進制位 00110001,掩碼b0的二進制位 10110000, 因此:
00110001
10110000
進行交配的話,二進制就變成:10000001,轉換成10進制為 129了,那么轉換成16進制就是 81了。
6-2:字符2的二進制位 00110010,掩碼23的二進制位 00100011,因此:
00110010
00100011
進行交配的話,二進制就變成 00010001,轉換10進制為17,那么轉換成16進制就是 11了。
6-3: 字符3的二進制位 00110011,掩碼52的二進制位 01010010,因此:
00110011
01010010
進行交配的話,二進制就變成:01100001,轉換成10進制為 97,那么轉換成16進制就是 61了。
6-4: 字符4的二進制位 00110100,掩碼 5a 的二進制位 01011010,因此:
00110100
01011010
進行交配的話,二進制就變成 01101110,轉換成10進制為 110,那么轉換成16進制為 6e.
6-5: 字符5的二進制位 00110101,掩碼b0的二進制位 10110000, 因此:
00110101
10110000
進行交配的話,二進制就變成:10000101,轉換成10進制為 133,那么轉換成16進制就是 85了。
6-6: 字符6的二進制位 00110110,掩碼23的二進制位 00100011,因此:
00110110
00100011
進行交配的話,二進制就變成:00010101,轉換成10進制為 21,那么轉換成16進制就是 15了。
6-7: 字符7的二進制位 00110111,掩碼52的二進制位 01010010,因此:
00110111
01010010
進行交配的話,二進制就變成:01100101,轉換成10進制為 101,那么轉換成16進制就是 65了。
6-8: 字符8的二進制位 00111000,掩碼 5a 的二進制位 01011010,因此:
00111000
01011010
進行交配的話,二進制就變成:01100010,轉換成10進制為 98,那么轉換成16進制就是 62了。
6-9: 字符9的二進制位 00111001,掩碼b0的二進制位 10110000, 因此:
00111001
10110000
進行交配的話,二進制就變成:10001001,轉換成10進制為 137,那么轉換成16進制就是 89了。
字符123456789與掩碼加密的整個過程如上面分析,可以看到,字符分別依次與掩碼交配,如果掩碼不夠的話,依次從頭循環即可。
因此我們可以編寫如下encodeDataFrame.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'); // 握手成功后給客戶端發送數據 o.write(encodeDataFrame({ FIN: 1, Opcode: 1, PayloadData: "123456789" })) } else { } }) }).listen(8001); /* >> 含義是右移運算符, 右移運算符是將一個二進制位的操作數按指定移動的位數向右移動,移出位被丟棄,左邊移出的空位一律補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 encodeDataFrame(e) { var arrs = [], o = new Buffer(e.PayloadData), l = o.length; // 處理第一個字節 arrs.push((e.FIN << 7)+e.Opcode); // 處理第二個字節,判斷它的長度並放入相應的后溪長度 if (l < 126) { arrs.push(l); } else if(l < 0x0000) { arrs.push(126, (1&0xFF00) >> 8, 1&0xFF); } else { arrs.push(127, 0, 0, 0, 0, (l&0xFF000000)>>24,(l&0xFF0000)>>16,(l&0xFF00)>>8,l&0xFF ); } // 返回頭部分和數據部分的合並緩沖區 return Buffer.concat([new Buffer(arrs), o]); }
然后index.html代碼如下:
<html> <head> <title>WebSocket Demo</title> </head> <body> <script type="text/javascript"> var ws = new WebSocket("ws://127.0.0.1:8001"); ws.onerror = function(e) { console.log(e); }; ws.onopen = function(e) { console.log('握手成功'); ws.send('123456789'); } ws.onmessage = function(e) { console.log(e); } </script> </body> </html>
進入目錄后,運行node encodeDataFrame.js后,打開index.html頁面,在控制台看待效果圖如下:
使用分片的方式重新修改代碼:
上面是基本的使用方法,但是有時候我們需要將一個大的數據包需要分成多個數據幀來傳輸,因此分片它分為3個部分:
1個開始幀:FIN=0, Opcode > 0;
零個或多個傳輸幀: FIN=0, Opcode=0;
1個終止幀:FIN=1, Opcode=0;
因此之前的握手成功后發送的數據代碼:
o.write(encodeDataFrame({ FIN: 1, Opcode: 1, PayloadData: "123456789" }))
需要分成三部分來發送了;
改成如下代碼:
// 握手成功后給客戶端發送數據 o.write(encodeDataFrame({ FIN: 0, Opcode: 1, PayloadData: "123" })); o.write(encodeDataFrame({ FIN: 0, Opcode: 0, PayloadData: "456" })); o.write(encodeDataFrame({ FIN: 1, Opcode: 0, PayloadData: "789" }));