(一) 基本原理1. 了解現(xiàn)有的HTTP的架構(gòu)模式: 1-1. HTTP的特點(diǎn)是: 當(dāng)初這么設(shè)計(jì)也是有原因的,假如服務(wù)器能主動(dòng)推送數(shù)據(jù)給瀏覽器的話,那么瀏覽器很容易受到攻擊,比如一些廣告商會(huì)主動(dòng)把一些廣告信息強(qiáng)行的傳輸給客戶端。 2. 了解HTTP輪詢,長(zhǎng)輪詢和流化。 輪詢: 輪詢是通過(guò)瀏覽器定時(shí)的向web服務(wù)器發(fā)送http的Get請(qǐng)求,服務(wù)器收到請(qǐng)求后,就把最新的數(shù)據(jù)發(fā)回給客戶端,客戶端得到數(shù)據(jù)后,將其顯示出來(lái),然后再定期的重復(fù)這一過(guò)程,雖然可以滿足需求,但是存在一些缺點(diǎn),比如某一段時(shí)間內(nèi)web服務(wù)器沒(méi)有更新的數(shù)據(jù),但是瀏覽器仍然需要定時(shí)的發(fā)送Get請(qǐng)求過(guò)來(lái)詢問(wèn),那么即浪費(fèi)了帶寬,又浪費(fèi)了cpu的利用率。 長(zhǎng)輪詢: 客戶端向服務(wù)器請(qǐng)求信息,并在設(shè)定的時(shí)間段內(nèi)打開(kāi)一個(gè)連接,服務(wù)器如果沒(méi)有任何信息,會(huì)保持請(qǐng)求打開(kāi),直到有客戶端可用的信息,或者直到 缺點(diǎn)是: 如下圖: 流化: 在流化技術(shù)中,客戶端發(fā)送一個(gè)請(qǐng)求,服務(wù)器發(fā)送并維護(hù)一個(gè)持續(xù)更新和保持打開(kāi)的開(kāi)放響應(yīng)。每當(dāng)服務(wù)器有需要交付給客戶端信息時(shí),它就更新響應(yīng) 3. 了解WebSocket WebSocket減少了延遲,因?yàn)橐坏┙⑵餡ebsocket連接,服務(wù)器可以在消息可用時(shí)發(fā)送他們。和輪詢不同的是:WebSocket只發(fā)出一個(gè)請(qǐng)求,服務(wù)器 優(yōu)點(diǎn)有如下: WebSocket的應(yīng)用場(chǎng)景? (二) WebSocket協(xié)議WebSocket協(xié)議是為了解決web即時(shí)應(yīng)用中服務(wù)器與客戶端瀏覽器全雙工通信問(wèn)題而設(shè)計(jì)的。協(xié)議定義ws和wss協(xié)議,分別為普通請(qǐng)求和基于SSL的安全傳輸, ws端口是80,wss的端口為443. WebSocket協(xié)議由兩部分組成,握手和數(shù)據(jù)傳輸。 2-1 握手 <!DOCTYPE html><html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>websocket</title> <meta name="viewport" content="width=device-width, initial-scale=1"/> </head> <body> <script type="text/javascript"> var wsUrl = "wss://echo.websocket.org"; var ws = new WebSocket(wsUrl); ws.onopen = function() { console.log('open'); }; ws.onmessage = function(msg) { console.log(msg.data); } ws.onclose = function() { console.log('已經(jīng)被關(guān)閉了'); } </script> </body></html> 頁(yè)面運(yùn)行后,我們可以看到鏈接到 wss://echo.websocket.org 期間記錄的一個(gè)握手協(xié)議。先來(lái)看看客戶端發(fā)送http的請(qǐng)求頭: GET /chat HTTP/1.1Host:echo.websocket.org Upgrade:websocket Connection:Upgrade Sec-WebSocket-Key:ALS2AoBJtUup67heKDgzFg==Origin:file://Sec-WebSocket-Version:13 服務(wù)器響應(yīng)的頭字段 Connection:Upgrade Sec-WebSocket-Accept:qyzx/EgbRK15QNmr5PhpMQrPZMM=Server: Kaazing Gateway Upgrade:websocket 下面是請(qǐng)求和響應(yīng)頭字段的含義: Sec-WebSocket-Key: ALS2AoBJtUup67heKDgzFg== Sec-WebSocket-Key 的值是一串長(zhǎng)度為24的字符串是客戶端隨機(jī)生成的base64編碼的字符串,它發(fā)送給服務(wù)器,服務(wù)器需要使用它經(jīng)過(guò)一定的運(yùn)算規(guī)則生成服務(wù)器的key,然后把服務(wù)器的key發(fā)到客戶端去,客戶端驗(yàn)證正確后,握手成功。 握手的具體原理:當(dāng)我們客戶端執(zhí)行 new WebSocket(''wss://echo.websocket.org')的時(shí)候,客戶端就會(huì)發(fā)起請(qǐng)求報(bào)文進(jìn)行握手申請(qǐng),報(bào)文中有一個(gè)key就是 下面是實(shí)現(xiàn)一個(gè)簡(jiǎn)單的握手協(xié)議的demo,代碼如下: ### 目錄結(jié)構(gòu)如下: demo hands.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() { console.log('握手成功'); } </script></body></html> hands.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) { console.log(e); key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1]; console.log(key); // WS的字符串 加上 key, 變成新的字符串后做一次sha1運(yùn)算,最后轉(zhuǎn)換成Base64 key = crypto.createHash('sha1').update(key+WS).digest('base64'); console.log(key); // 輸出字段數(shù)據(jù),返回到客戶端, 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頭結(jié)束 o.write('\r\n'); } else { // 數(shù)據(jù)處理 } }) }).listen(8000); 首先在命令行中 進(jìn)入相對(duì)應(yīng)項(xiàng)目目錄后,運(yùn)行 node hands.js, 然后打開(kāi) hands.html 運(yùn)行一下即可看到 命令行中打印出來(lái)如下信息: $ node hands.js<Buffer 47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a 48 6f 73 74 3a 20 31 32 37 2e 30 2e 30 2e 31 3a 38 30 30 30 0d 0a 43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 ... > +iHlfGTolBaWYpnyTIw22g==W7IEsdQtwv8EP2204kssK/6pg+c= 然后在瀏覽器中查看請(qǐng)求頭如下信息: Request Headers: Connection:Upgrade Host:127.0.0.1:8000Origin:file://Sec-WebSocket-Extensions:permessage-deflate; client_max_window_bits Sec-WebSocket-Key:+iHlfGTolBaWYpnyTIw22g==Sec-WebSocket-Version:13Upgrade:websocket 響應(yīng)頭如下信息: Response Headers: Connection:Upgrade Sec-WebSocket-Accept:W7IEsdQtwv8EP2204kssK/6pg+c= Upgrade:websocket 如上信息可以看到,獲取報(bào)文中的key代碼: 和 Request Headers:中的 Sec-WebSocket-Key 值是一樣的,該值是瀏覽器自動(dòng)生成的,然后獲取該值后,與
'258EAFA5-E914-47DA-95CA-C5AB0DC85B11',相連,對(duì)新的字符串通過(guò)sha1安全散列算法計(jì)算出結(jié)果后,再進(jìn)行Base64編碼, (三) 解析數(shù)據(jù)幀1-1 理解數(shù)據(jù)幀的含義: 基本幀協(xié)議如下: 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 ... | +---------------------------------------------------------------+ 如上是基本幀協(xié)議,它帶有操作碼(opcode)的幀類型,負(fù)載長(zhǎng)度,和用于 "擴(kuò)展數(shù)據(jù)" 與 "應(yīng)用數(shù)據(jù)" 及 它們一起定義的 "負(fù)載數(shù)據(jù)"的指定位置, FIN(1位): 是否為消息的最后一個(gè)數(shù)據(jù)幀。 0x0 表示附加數(shù)據(jù)幀0x1 表示文本數(shù)據(jù)幀0x2 表示二進(jìn)制數(shù)據(jù)幀0x3-7 暫時(shí)無(wú)定義,為以后的非控制幀保留0x8 表示連接關(guān)閉0x9 表示ping0xA 表示pong0xB-F 暫時(shí)無(wú)定義,為以后的控制幀保留 Mask(占1位): 表示是否經(jīng)過(guò)掩碼處理, 1 是經(jīng)過(guò)掩碼的,0是沒(méi)有經(jīng)過(guò)掩碼的。 payload length (7位+16位,或者 7位+64位),定義負(fù)載數(shù)據(jù)的長(zhǎng)度。 Masking-key(0或者4個(gè)字節(jié)),該區(qū)塊用于存儲(chǔ)掩碼密鑰,只有在第二個(gè)子節(jié)中的mask為1,也就是消息進(jìn)行了掩碼處理時(shí)才有,否則沒(méi)有, Payload data 擴(kuò)展數(shù)據(jù),是0字節(jié),除非已經(jīng)協(xié)商了一個(gè)擴(kuò)展。 1-2 客戶端到服務(wù)器掩碼 二進(jìn)制位運(yùn)算符知識(shí)擴(kuò)展: >> 含義是右移運(yùn)算符, << 含義是左移運(yùn)算符 注意1: 在使用補(bǔ)碼作為機(jī)器數(shù)的機(jī)器中,正數(shù)的符號(hào)位為0,負(fù)數(shù)的符號(hào)位為1(一般情況下). 注意2:負(fù)數(shù)的二進(jìn)制位如何計(jì)算? 再來(lái)看一個(gè)列子: 數(shù)據(jù)幀解析的程序如下代碼:(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運(yùn)算,最后轉(zhuǎn)換成Base64 key = crypto.createHash('sha1').update(key+WS).digest('base64'); // 輸出字段數(shù)據(jù),返回到客戶端, 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頭結(jié)束 o.write('\r\n'); } else { // 數(shù)據(jù)處理 onmessage(e); } }) }).listen(8000);/* >> 含義是右移運(yùn)算符, 右移運(yùn)算符是將一個(gè)二進(jìn)制位的操作數(shù)按指定移動(dòng)的位數(shù)向右移動(dòng),移出位被丟棄,左邊移出的空位一律補(bǔ)0. 比如 11 >> 2, 意思是說(shuō)將數(shù)字11右移2位。 首先將11轉(zhuǎn)換為二進(jìn)制數(shù)為 0000 0000 0000 0000 0000 0000 0000 1011 , 然后把低位的最后2個(gè)數(shù)字移出,因?yàn)樵摂?shù)字是正數(shù), 所以在高位補(bǔ)零,則得到的最終結(jié)果為:0000 0000 0000 0000 0000 0000 0000 0010,轉(zhuǎn)換為10進(jìn)制是2. << 含義是左移運(yùn)算符 左移運(yùn)算符是將一個(gè)二進(jìn)制位的操作數(shù)按指定移動(dòng)的位數(shù)向左移位,移出位被丟棄,右邊的空位一律補(bǔ)0. 比如 3 << 2, 意思是說(shuō)將數(shù)字3左移2位, 首先將3轉(zhuǎn)換為二進(jìn)制數(shù)為 0000 0000 0000 0000 0000 0000 0000 0011 , 然后把該數(shù)字高位(左側(cè))的兩個(gè)零移出,其他的數(shù)字都朝左平移2位, 最后在右側(cè)的兩個(gè)空位補(bǔ)0,因此最后的結(jié)果是 0000 0000 0000 0000 0000 0000 0000 1100,則轉(zhuǎn)換為十進(jìn)制是12(1100 = 1*2的3次方 + 1*2的2字方) 注意1: 在使用補(bǔ)碼作為機(jī)器數(shù)的機(jī)器中,正數(shù)的符號(hào)位為0,負(fù)數(shù)的符號(hào)位為1(一般情況下). 比如:十進(jìn)制數(shù)13在計(jì)算機(jī)中表示為00001101,其中第一位0表示的是符號(hào) 注意2:負(fù)數(shù)的二進(jìn)制位如何計(jì)算? 比如二進(jìn)制的原碼為 10010101,它的補(bǔ)碼怎么計(jì)算呢? 首先計(jì)算它的反碼是 01101010; 那么補(bǔ)碼 = 反碼 + 1 = 01101011 再來(lái)看一個(gè)列子: -7 >> 2 意思是將數(shù)字 -7 右移2位。 負(fù)數(shù)先用它的絕對(duì)值正數(shù)取它的二進(jìn)制代碼,7的二進(jìn)制位為: 0000 0000 0000 0000 0000 0000 0000 0111 ,那么 -7的二進(jìn)制位就是 取反, 取反后再加1,就變成補(bǔ)碼。 因此-7的二進(jìn)制位: 1111 1111 1111 1111 1111 1111 1111 1001, 因此 -7右移2位就成 1111 1111 1111 1111 1111 1111 1111 1110 因此轉(zhuǎn)換成十進(jìn)制的話 -7 >> 2 ,值就變成 -2了。*/function decodeDataFrame(e) { var i = 0, j, s, arrs = [], frame = { // 解析前兩個(gè)字節(jié)的基本數(shù)據(jù) FIN: e[i] >> 7, Opcode: e[i++] & 15, Mask: e[i] >> 7, PayloadLength: e[i++] & 0x7F }; // 處理特殊長(zhǎng)度126和127 if (frame.PayloadLength === 126) { frame.PayloadLength = (e[i++] << 8) + e[i++]; } if (frame.PayloadLength === 127) { i += 4; // 長(zhǎng)度一般用4個(gè)字節(jié)的整型,前四個(gè)字節(jié)一般為長(zhǎng)整型留空的。 frame.PayloadLength = (e[i++] << 24)+(e[i++] << 16)+(e[i++] << 8) + e[i++]; } // 判斷是否使用掩碼 if (frame.Mask) { // 獲取掩碼實(shí)體 frame.MaskingKey = [e[i++], e[i++], e[i++], e[i++]]; // 對(duì)數(shù)據(jù)和掩碼做異或運(yùn)算 for(j = 0, arrs = []; j < frame.PayloadLength; j++) { arrs.push(e[i+j] ^ frame.MaskingKey[j%4]); } } else { // 否則的話 直接使用數(shù)據(jù) arrs = e.slice(i, i + frame.PayloadLength); } // 數(shù)組轉(zhuǎn)換成緩沖區(qū)來(lái)使用 arrs = new Buffer(arrs); // 如果有必要?jiǎng)t把緩沖區(qū)轉(zhuǎn)換成字符串來(lái)使用 if (frame.Opcode === 1) { arrs = arrs.toString(); } // 設(shè)置上數(shù)據(jù)部分 frame.PayloadLength = arrs; // 返回?cái)?shù)據(jù)幀 return frame; }function onmessage(e) { console.log(e) e = decodeDataFrame(e); // 解析數(shù)據(jù)幀 console.log(e); // 把數(shù)據(jù)幀輸出到控制臺(tái)} 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, 先進(jìn)入項(xiàng)目中對(duì)應(yīng)的目錄后,使用node decodeDataFrame.js, 然后打開(kāi)index.html后查看效果 如下: 這樣服務(wù)器接收客戶端穿過(guò)了的數(shù)據(jù)就沒(méi)問(wèn)題了。 (四) 生成數(shù)據(jù)幀從服務(wù)器發(fā)往客戶端的數(shù)據(jù)也是同樣的數(shù)據(jù)幀,但是從服務(wù)器發(fā)送到客戶端的數(shù)據(jù)幀不需要掩碼的。我們自己需要去生成數(shù)據(jù)幀,解析數(shù)據(jù)幀的時(shí)候我們需要分片。 消息分片: 如果大數(shù)據(jù)不能被碎片化,那么一端就必須將消息整個(gè)載入內(nèi)存緩沖之中,然后需要計(jì)算長(zhǎng)度等操作并發(fā)送,但是有了碎片化機(jī)制,服務(wù)器端或者中間件就可以選取適用的內(nèi)存緩沖長(zhǎng)度,然后當(dāng)緩沖滿了之后就發(fā)送一個(gè)消息碎片。 分片規(guī)則: 注意: 下面我們來(lái)理解下上面分片規(guī)則2中的話的含義: 還是看基本幀協(xié)議如下: 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解析: <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 ] } 上面返回的數(shù)據(jù)部分是16進(jìn)制,因此我們需要他們轉(zhuǎn)換成二進(jìn)制,有關(guān)16進(jìn)制,10進(jìn)制,2進(jìn)制的轉(zhuǎn)換表如下: 我們現(xiàn)在需要把 81 89 b0 23 52 5a 81 11 61 6e 85 15 65 62 89 這些16進(jìn)制先轉(zhuǎn)換成10進(jìn)制,然后轉(zhuǎn)換成二進(jìn)制,分析代碼如下: 16進(jìn)制 10進(jìn)制 2進(jìn)制81 8*16的1次方 + 1*16的0次方 = 129 10000001 89 8*16的1次方 + 9*16的0次方 = 137 10001001b0 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 010100105a 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 001111016e 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 我們把上面的轉(zhuǎn)換后的二進(jìn)制 對(duì)照上面的 基本幀協(xié)議表看下: 0x0 表示附加數(shù)據(jù)幀 注意:其中8進(jìn)制是以0開(kāi)頭的,16進(jìn)制是以0x開(kāi)頭的。 0001,是文本數(shù)據(jù)幀了。 3. 第九位是1,那么對(duì)應(yīng)的幀協(xié)議表就是MASK部分了,Mask(占1位): 表示是否經(jīng)過(guò)掩碼處理, 1 是經(jīng)過(guò)掩碼的,0是沒(méi)有經(jīng)過(guò)掩碼的。說(shuō)明是經(jīng)過(guò)掩碼處理的, 4. 第10~16位是 0001001 = 9 < 125, 對(duì)應(yīng)幀協(xié)議中的 payload length的部分了,數(shù)據(jù)長(zhǎng)度為9,因此小于125位,因此使用7位來(lái)表示實(shí)際數(shù)據(jù)長(zhǎng)度。 5. b0, 23, 52, 5a 對(duì)應(yīng)的部分是 屬于Masking-key(0或者4個(gè)字節(jié)),該區(qū)塊用于存儲(chǔ)掩碼密鑰,只有在第二個(gè)子節(jié)中的mask為1,也就是消息進(jìn)行了掩碼處理時(shí)才有。 6. 81 11 61 6e 85 15 65 62 89 這些就是對(duì)應(yīng)表中的數(shù)據(jù)部分了。 下面我們?cè)賮?lái)理解下 消息 123456789 怎么通過(guò)掩碼加密成 81 11 61 6e 85 15 65 62 89 這些數(shù)據(jù)了。 數(shù)字字符1的ASCLL碼的16進(jìn)制為31,轉(zhuǎn)換成10進(jìn)制就是49了。其他的數(shù)字依次類推+1; 數(shù)字 10進(jìn)制 二進(jìn)制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的二進(jìn)制位 00110001,掩碼b0的二進(jìn)制位 10110000, 因此: 00110001 進(jìn)行交配的話,二進(jìn)制就變成:10000001,轉(zhuǎn)換成10進(jìn)制為 129了,那么轉(zhuǎn)換成16進(jìn)制就是 81了。 6-2:字符2的二進(jìn)制位 00110010,掩碼23的二進(jìn)制位 00100011,因此: 00110010 進(jìn)行交配的話,二進(jìn)制就變成 00010001,轉(zhuǎn)換10進(jìn)制為17,那么轉(zhuǎn)換成16進(jìn)制就是 11了。 6-3: 字符3的二進(jìn)制位 00110011,掩碼52的二進(jìn)制位 01010010,因此: 00110011 進(jìn)行交配的話,二進(jìn)制就變成:01100001,轉(zhuǎn)換成10進(jìn)制為 97,那么轉(zhuǎn)換成16進(jìn)制就是 61了。 6-4: 字符4的二進(jìn)制位 00110100,掩碼 5a 的二進(jìn)制位 01011010,因此: 00110100 進(jìn)行交配的話,二進(jìn)制就變成 01101110,轉(zhuǎn)換成10進(jìn)制為 110,那么轉(zhuǎn)換成16進(jìn)制為 6e. 6-5: 字符5的二進(jìn)制位 00110101,掩碼b0的二進(jìn)制位 10110000, 因此: 00110101 進(jìn)行交配的話,二進(jìn)制就變成:10000101,轉(zhuǎn)換成10進(jìn)制為 133,那么轉(zhuǎn)換成16進(jìn)制就是 85了。 6-6: 字符6的二進(jìn)制位 00110110,掩碼23的二進(jìn)制位 00100011,因此: 00110110 進(jìn)行交配的話,二進(jìn)制就變成:00010101,轉(zhuǎn)換成10進(jìn)制為 21,那么轉(zhuǎn)換成16進(jìn)制就是 15了。 6-7: 字符7的二進(jìn)制位 00110111,掩碼52的二進(jìn)制位 01010010,因此: 00110111 進(jìn)行交配的話,二進(jìn)制就變成:01100101,轉(zhuǎn)換成10進(jìn)制為 101,那么轉(zhuǎn)換成16進(jìn)制就是 65了。 6-8: 字符8的二進(jìn)制位 00111000,掩碼 5a 的二進(jìn)制位 01011010,因此: 00111000 進(jìn)行交配的話,二進(jìn)制就變成:01100010,轉(zhuǎn)換成10進(jìn)制為 98,那么轉(zhuǎn)換成16進(jìn)制就是 62了。 6-9: 字符9的二進(jìn)制位 00111001,掩碼b0的二進(jìn)制位 10110000, 因此: 00111001 進(jìn)行交配的話,二進(jìn)制就變成:10001001,轉(zhuǎn)換成10進(jìn)制為 137,那么轉(zhuǎn)換成16進(jìn)制就是 89了。 字符123456789與掩碼加密的整個(gè)過(guò)程如上面分析,可以看到,字符分別依次與掩碼交配,如果掩碼不夠的話,依次從頭循環(huán)即可。 因此我們可以編寫如下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運(yùn)算,最后轉(zhuǎn)換成Base64 key = crypto.createHash('sha1').update(key+WS).digest('base64'); // 輸出字段數(shù)據(jù),返回到客戶端, 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頭結(jié)束 o.write('\r\n'); // 握手成功后給客戶端發(fā)送數(shù)據(jù) o.write(encodeDataFrame({ FIN: 1, Opcode: 1, PayloadData: "123456789" })) } else { } }) }).listen(8001);/* >> 含義是右移運(yùn)算符, 右移運(yùn)算符是將一個(gè)二進(jìn)制位的操作數(shù)按指定移動(dòng)的位數(shù)向右移動(dòng),移出位被丟棄,左邊移出的空位一律補(bǔ)0. 比如 11 >> 2, 意思是說(shuō)將數(shù)字11右移2位。 首先將11轉(zhuǎn)換為二進(jìn)制數(shù)為 0000 0000 0000 0000 0000 0000 0000 1011 , 然后把低位的最后2個(gè)數(shù)字移出,因?yàn)樵摂?shù)字是正數(shù), 所以在高位補(bǔ)零,則得到的最終結(jié)果為:0000 0000 0000 0000 0000 0000 0000 0010,轉(zhuǎn)換為10進(jìn)制是2. << 含義是左移運(yùn)算符 左移運(yùn)算符是將一個(gè)二進(jìn)制位的操作數(shù)按指定移動(dòng)的位數(shù)向左移位,移出位被丟棄,右邊的空位一律補(bǔ)0. 比如 3 << 2, 意思是說(shuō)將數(shù)字3左移2位, 首先將3轉(zhuǎn)換為二進(jìn)制數(shù)為 0000 0000 0000 0000 0000 0000 0000 0011 , 然后把該數(shù)字高位(左側(cè))的兩個(gè)零移出,其他的數(shù)字都朝左平移2位, 最后在右側(cè)的兩個(gè)空位補(bǔ)0,因此最后的結(jié)果是 0000 0000 0000 0000 0000 0000 0000 1100,則轉(zhuǎn)換為十進(jìn)制是12(1100 = 1*2的3次方 + 1*2的2字方) 注意1: 在使用補(bǔ)碼作為機(jī)器數(shù)的機(jī)器中,正數(shù)的符號(hào)位為0,負(fù)數(shù)的符號(hào)位為1(一般情況下). 比如:十進(jìn)制數(shù)13在計(jì)算機(jī)中表示為00001101,其中第一位0表示的是符號(hào) 注意2:負(fù)數(shù)的二進(jìn)制位如何計(jì)算? 比如二進(jìn)制的原碼為 10010101,它的補(bǔ)碼怎么計(jì)算呢? 首先計(jì)算它的反碼是 01101010; 那么補(bǔ)碼 = 反碼 + 1 = 01101011 再來(lái)看一個(gè)列子: -7 >> 2 意思是將數(shù)字 -7 右移2位。 負(fù)數(shù)先用它的絕對(duì)值正數(shù)取它的二進(jìn)制代碼,7的二進(jìn)制位為: 0000 0000 0000 0000 0000 0000 0000 0111 ,那么 -7的二進(jìn)制位就是 取反, 取反后再加1,就變成補(bǔ)碼。 因此-7的二進(jìn)制位: 1111 1111 1111 1111 1111 1111 1111 1001, 因此 -7右移2位就成 1111 1111 1111 1111 1111 1111 1111 1110 因此轉(zhuǎn)換成十進(jìn)制的話 -7 >> 2 ,值就變成 -2了。*/function decodeDataFrame(e) { var i = 0, j, s, arrs = [], frame = { // 解析前兩個(gè)字節(jié)的基本數(shù)據(jù) FIN: e[i] >> 7, Opcode: e[i++] & 15, Mask: e[i] >> 7, PayloadLength: e[i++] & 0x7F }; // 處理特殊長(zhǎng)度126和127 if (frame.PayloadLength === 126) { frame.PayloadLength = (e[i++] << 8) + e[i++]; } if (frame.PayloadLength === 127) { i += 4; // 長(zhǎng)度一般用4個(gè)字節(jié)的整型,前四個(gè)字節(jié)一般為長(zhǎng)整型留空的。 frame.PayloadLength = (e[i++] << 24)+(e[i++] << 16)+(e[i++] << 8) + e[i++]; } // 判斷是否使用掩碼 if (frame.Mask) { // 獲取掩碼實(shí)體 frame.MaskingKey = [e[i++], e[i++], e[i++], e[i++]]; // 對(duì)數(shù)據(jù)和掩碼做異或運(yùn)算 for(j = 0, arrs = []; j < frame.PayloadLength; j++) { arrs.push(e[i+j] ^ frame.MaskingKey[j%4]); } } else { // 否則的話 直接使用數(shù)據(jù) arrs = e.slice(i, i + frame.PayloadLength); } // 數(shù)組轉(zhuǎn)換成緩沖區(qū)來(lái)使用 arrs = new Buffer(arrs); // 如果有必要?jiǎng)t把緩沖區(qū)轉(zhuǎn)換成字符串來(lái)使用 if (frame.Opcode === 1) { arrs = arrs.toString(); } // 設(shè)置上數(shù)據(jù)部分 frame.PayloadLength = arrs; // 返回?cái)?shù)據(jù)幀 return frame; }function encodeDataFrame(e) { var arrs = [], o = new Buffer(e.PayloadData), l = o.length; // 處理第一個(gè)字節(jié) arrs.push((e.FIN << 7)+e.Opcode); // 處理第二個(gè)字節(jié),判斷它的長(zhǎng)度并放入相應(yīng)的后溪長(zhǎng)度 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 ); } // 返回頭部分和數(shù)據(jù)部分的合并緩沖區(qū) 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> 進(jìn)入目錄后,運(yùn)行node encodeDataFrame.js后,打開(kāi)index.html頁(yè)面,在控制臺(tái)看待效果圖如下: 使用分片的方式重新修改代碼: 上面是基本的使用方法,但是有時(shí)候我們需要將一個(gè)大的數(shù)據(jù)包需要分成多個(gè)數(shù)據(jù)幀來(lái)傳輸,因此分片它分為3個(gè)部分: 1個(gè)開(kāi)始幀:FIN=0, Opcode > 0; 因此之前的握手成功后發(fā)送的數(shù)據(jù)代碼: o.write(encodeDataFrame({ FIN: 1, Opcode: 1, PayloadData: "123456789"})) 需要分成三部分來(lái)發(fā)送了; 改成如下代碼: // 握手成功后給客戶端發(fā)送數(shù)據(jù)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"})); (五) 心跳及重連機(jī)制在使用websocket的過(guò)程中,有時(shí)候會(huì)遇到網(wǎng)絡(luò)斷開(kāi)的情況,但是在網(wǎng)絡(luò)斷開(kāi)的時(shí)候服務(wù)器端并沒(méi)有觸發(fā)onclose的事件。這樣會(huì)有:服務(wù)器會(huì)繼續(xù)向客戶端發(fā)送多余的鏈接,并且這些數(shù)據(jù)還會(huì)丟失。所以就需要一種機(jī)制來(lái)檢測(cè)客戶端和服務(wù)端是否處于正常的鏈接狀態(tài)。因此就有了websocket的心跳了。還有心跳,說(shuō)明還活著,沒(méi)有心跳說(shuō)明已經(jīng)掛掉了。 1. 為什么叫心跳包呢? 2. 心跳機(jī)制是? 那么需要怎么去實(shí)現(xiàn)它呢?如下所有代碼: <html> <head> <meta charset="utf-8"> <title>WebSocket Demo</title> </head> <body> <script type="text/javascript"> // var ws = new WebSocket("wss://echo.websocket.org"); /* ws.onerror = function(e) { console.log('已關(guān)閉'); }; ws.onopen = function(e) { console.log('握手成功'); ws.send('123456789'); } ws.onclose = function() { console.log('已關(guān)閉'); } ws.onmessage = function(e) { console.log('收到消息'); console.log(e); } */ var lockReconnect = false;//避免重復(fù)連接 var wsUrl = "wss://echo.websocket.org"; var ws; var tt; function createWebSocket() { try { ws = new WebSocket(wsUrl); init(); } catch(e) { console.log('catch'); reconnect(wsUrl); } } function init() { ws.onclose = function () { console.log('鏈接關(guān)閉'); reconnect(wsUrl); }; ws.onerror = function() { console.log('發(fā)生異常了'); reconnect(wsUrl); }; ws.onopen = function () { //心跳檢測(cè)重置 heartCheck.start(); }; ws.onmessage = function (event) { //拿到任何消息都說(shuō)明當(dāng)前連接是正常的 console.log('接收到消息'); heartCheck.start(); } } function reconnect(url) { if(lockReconnect) { return; }; lockReconnect = true; //沒(méi)連接上會(huì)一直重連,設(shè)置延遲避免請(qǐng)求過(guò)多 tt && clearTimeout(tt); tt = setTimeout(function () { createWebSocket(url); lockReconnect = false; }, 4000); } //心跳檢測(cè) var heartCheck = { timeout: 3000, timeoutObj: null, serverTimeoutObj: null, start: function(){ console.log('start'); var self = this; this.timeoutObj && clearTimeout(this.timeoutObj); this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj); this.timeoutObj = setTimeout(function(){ //這里發(fā)送一個(gè)心跳,后端收到后,返回一個(gè)心跳消息, console.log('55555'); ws.send("123456789"); self.serverTimeoutObj = setTimeout(function() { console.log(111); console.log(ws); ws.close(); // createWebSocket(); }, self.timeout); }, this.timeout) } } createWebSocket(wsUrl); </script> </body> </html> 具體的思路如下: function createWebSocket() { try { ws = new WebSocket(wsUrl); init(); } catch(e) { console.log('catch'); reconnect(wsUrl); } } 2. 第二步調(diào)用init方法,該方法內(nèi)把一些監(jiān)聽(tīng)事件封裝如下: function init() { ws.onclose = function () { console.log('鏈接關(guān)閉'); reconnect(wsUrl); }; ws.onerror = function() { console.log('發(fā)生異常了'); reconnect(wsUrl); }; ws.onopen = function () { //心跳檢測(cè)重置 heartCheck.start(); }; ws.onmessage = function (event) { //拿到任何消息都說(shuō)明當(dāng)前連接是正常的 console.log('接收到消息'); heartCheck.start(); } } 3. 如上第二步,當(dāng)網(wǎng)絡(luò)斷開(kāi)的時(shí)候,會(huì)先調(diào)用onerror,onclose事件可以監(jiān)聽(tīng)到,會(huì)調(diào)用reconnect方法進(jìn)行重連操作。正常的情況下,是先調(diào)用 4. 重連操作 reconnect代碼如下: var lockReconnect = false;//避免重復(fù)連接function reconnect(url) { if(lockReconnect) { return; }; lockReconnect = true; //沒(méi)連接上會(huì)一直重連,設(shè)置延遲避免請(qǐng)求過(guò)多 tt && clearTimeout(tt); tt = setTimeout(function () { createWebSocket(url); lockReconnect = false; }, 4000); } 如上代碼,如果網(wǎng)絡(luò)斷開(kāi)的話,會(huì)執(zhí)行reconnect方法,使用了一個(gè)定時(shí)器,4秒后會(huì)重新創(chuàng)建一個(gè)新的websocket鏈接,重新調(diào)用createWebSocket函數(shù), 5. 最后一步就是實(shí)現(xiàn)心跳檢測(cè)的代碼:如下: //心跳檢測(cè)var heartCheck = { timeout: 3000, timeoutObj: null, serverTimeoutObj: null, start: function(){ console.log('start'); var self = this; this.timeoutObj && clearTimeout(this.timeoutObj); this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj); this.timeoutObj = setTimeout(function(){ //這里發(fā)送一個(gè)心跳,后端收到后,返回一個(gè)心跳消息, //onmessage拿到返回的心跳就說(shuō)明連接正常 console.log('55555'); ws.send("123456789"); self.serverTimeoutObj = setTimeout(function() { console.log(111); console.log(ws); ws.close(); // createWebSocket(); }, self.timeout); }, this.timeout) } } 實(shí)現(xiàn)心跳檢測(cè)的思路是:每隔一段固定的時(shí)間,向服務(wù)器端發(fā)送一個(gè)ping數(shù)據(jù),如果在正常的情況下,服務(wù)器會(huì)返回一個(gè)pong給客戶端,如果客戶端通過(guò) |
|