淺析nodejs實現(xiàn)Websocket的數(shù)據(jù)接收與發(fā)送
WebSocket是HTML5開始提供的一種瀏覽器與服務(wù)器間進行全雙工通訊的網(wǎng)絡(luò)技術(shù)。在WebSocket API中,瀏覽器和服務(wù)器只需要要做一個握手(handshaking)的動作,然后,瀏覽器和服務(wù)器之間就形成了一條快速通道。兩者之間就直接可以數(shù)據(jù)互相傳送。
WebSocket是一個通信的協(xié)議,分為服務(wù)器和客戶端。服務(wù)器放在后臺,保持與客戶端的長連接,完成雙方通信的任務(wù)??蛻舳艘话愣际菍崿F(xiàn)在支持HTML5瀏覽器核心中,通過提供JavascriptAPI使用網(wǎng)頁可以建立websocket連接。
在我寫這篇文章里:基于html5和nodejs相結(jié)合實現(xiàn)websocket即使通訊,里面主要是借助了nodejs-websocket這個插件,后來還用了socket.io做了些demo,但是,這些都是借助于別人封裝好的插件做出來的,websocket到底是怎么實現(xiàn)的呢?之前真沒想過,最近看樸靈大神的《深入淺析node.js》時候,看到了websocket那一節(jié),看了websocket的數(shù)據(jù)幀定義,想著用nodejs實現(xiàn)。經(jīng)過一番折騰實現(xiàn)了。
客戶端的代碼就不說了,websocket的API還是很簡單的,就通過onmessage、onopen、onclose,以及send方法就可以實現(xiàn)了。
websocket api通過onmessage、onopen、onclose以及send方法實現(xiàn)客戶端的代碼。具體詳情就不多說了。
主要說服務(wù)端的代碼:
首先是協(xié)議的升級,這個比較簡單,就簡述一下:當(dāng)在客戶端執(zhí)行new Websocket("ws://XXX.com/")的時候,客戶端就會發(fā)起請求報文進行握手申請,報文中有個很重要的key就是Sec-WebSocket-Key,服務(wù)端獲取到key,然后將這個key與字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11相連,對新的字符串通過sha1安全散列算法計算出結(jié)果后,再進行base64編碼,并且將結(jié)果放在請求頭的"Sec-WebSocket-Accept"中返回即可完成握手。具體請看代碼:
server.on('upgrade', function (req, socket, upgradeHead) { var key = req.headers['sec-websocket-key']; key = crypto.createHash("sha1").update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest("base64"); var headers = [ 'HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', 'Sec-WebSocket-Accept: ' + key ]; socket.setNoDelay(true); socket.write(headers.join("\r\n") + "\r\n\r\n", 'ascii'); var ws = new WebSocket(socket); webSocketCollector.push(ws); callback(ws); });
upgrade事件其實是http這個模塊的封裝,再往底層就是net模塊的實現(xiàn),其實都差不多,如果直接用net模塊來實現(xiàn)的話,就是監(jiān)聽net.createServer返回的server對象的data事件,接收到的第一份數(shù)據(jù)就是客戶端發(fā)來的升級請求報文。
上面那段代碼就完成了websocket的握手,然后就可以開始數(shù)據(jù)傳輸了。
看數(shù)據(jù)傳輸之前,先看看websocket數(shù)據(jù)幀的定義(因為覺得深入淺出nodejs里的幀定義圖最容易理解,所以就貼這張了):
上面的圖中,每一列就是一個字節(jié),一個字節(jié)總共是8位,每一位就是一個二進制數(shù),不同位的值會對應(yīng)不同的意義。
fin:指示這個是消息的最后片段。第一個片段可能也是最后的片段。如果為1即為最后片段,(其實這個位的用途我個人有點疑惑,按照書上以及網(wǎng)上查的資料,當(dāng)數(shù)據(jù)被分片的時候,不同片應(yīng)該都會有fin位,會根據(jù)fin為是不是0來判斷是否為最后一幀,但是實際實現(xiàn)中卻發(fā)現(xiàn),當(dāng)數(shù)據(jù)比較大需要分片時,服務(wù)端收到的數(shù)據(jù)就只有第一幀是有fin位為1,其他幀則整個幀都是數(shù)據(jù)段,也就是說,感覺這個fin位似乎用不上,至少我自己寫的demo中是通過數(shù)據(jù)長度來判斷是否到了最后一幀,完全沒用到這個fin位是否為1來判斷)
rsv1、rsv2、rsv3:各占一個位,用于擴展協(xié)商,基本上不怎么需要理,一般都是0
opcode:占四個位,可以表示0~15的十進制,0表示為附加數(shù)據(jù)幀,1表示為文本數(shù)據(jù)幀,2表示二進制數(shù)據(jù)幀,8表示發(fā)送一個連接關(guān)閉的數(shù)據(jù)幀,9表示ping,10表示pong,ping和pong都是用于心跳檢測,當(dāng)一端發(fā)送ping時,另一端必須響應(yīng)pong表示自己仍處于響應(yīng)狀態(tài)。
masked:占一個位,表示是否進行掩碼處理,客戶端發(fā)送給服務(wù)端時為1,服務(wù)端發(fā)送給客戶端時為0
payload length:占7位,或者7+16位、或者7+64位。如果第二個字節(jié)的后面七個位的十進制值小于或等于125,則直接用這七個位表示數(shù)據(jù)長度;如果該值為126,說明 125<數(shù)據(jù)長度<65535(16個位能描述的最大值,也就是16個1的時候),就用第三個字節(jié)及第四個字節(jié)即16個位來表示;如果該值為127,則說明數(shù)據(jù)長度已經(jīng)大于65535,16個位也已經(jīng)不足以描述數(shù)據(jù)長度了,就用第三到第十個字節(jié)這八個字節(jié)來描述數(shù)據(jù)長度。
masking key:當(dāng)masked為1的時候才存在,用于對我們需要的數(shù)據(jù)進行解密。
payload data:我們需要的數(shù)據(jù),如果masked為1,該數(shù)據(jù)會被加密,要通過masking key進行異或運算解密才能獲取到真實數(shù)據(jù)。
幀定義解釋完了,就可以根據(jù)數(shù)據(jù)來進行解析了,當(dāng)有data過來的時候,先獲取需要的數(shù)據(jù)信息,下面這段代碼將獲取到數(shù)據(jù)在data里的位置,以及數(shù)據(jù)長度,masking key以及opcode:
WebSocket.prototype.handleDataStat = function (data) { if (!this.stat) { var dataIndex = 2; //數(shù)據(jù)索引,因為第一個字節(jié)和第二個字節(jié)肯定不為數(shù)據(jù),所以初始值為2 var secondByte = data[1]; //代表masked位和可能是payloadLength位的第二個字節(jié) var hasMask = secondByte >= 128; //如果大于或等于128,說明masked位為1 secondByte -= hasMask ? 128 : 0; //如果有掩碼,需要將掩碼那一位去掉 var dataLength, maskedData; //如果為126,則后面16位長的數(shù)據(jù)為數(shù)據(jù)長度,如果為127,則后面64位長的數(shù)據(jù)為數(shù)據(jù)長度 if (secondByte == 126) { dataIndex += 2; dataLength = data.readUInt16BE(2); } else if (secondByte == 127) { dataIndex += 8; dataLength = data.readUInt32BE(2) + data.readUInt32BE(6); } else { dataLength = secondByte; } //如果有掩碼,則獲取32位的二進制masking key,同時更新index if (hasMask) { maskedData = data.slice(dataIndex, dataIndex + 4); dataIndex += 4; } //數(shù)據(jù)量最大為10kb if (dataLength > 10240) { this.send("Warning : data limit 10kb"); } else { //計算到此處時,dataIndex為數(shù)據(jù)位的起始位置,dataLength為數(shù)據(jù)長度,maskedData為二進制的解密數(shù)據(jù) this.stat = { index: dataIndex, totalLength: dataLength, length: dataLength, maskedData: maskedData, opcode: parseInt(data[0].toString(16).split("")[1] , 16) //獲取第一個字節(jié)的opcode位 }; } } else { this.stat.index = 0; } };
代碼中均有注釋,理解起來應(yīng)該不難,直接看下一步,獲取到數(shù)據(jù)信息后,就要對數(shù)據(jù)進行實際解析了:
經(jīng)過上面handleDataStat方法的處理,stat中已經(jīng)有了data的相關(guān)數(shù)據(jù),先判斷opcode,如果為9說明是客戶端發(fā)起的ping心跳檢測,直接返回pong響應(yīng),如果為10則為服務(wù)端發(fā)起的心跳檢測。如果有masking key,則遍歷數(shù)據(jù)段,對每個字節(jié)都與masking key的字節(jié)進行異或運算(網(wǎng)上看到一個說法很形象:就是輪流發(fā)生X關(guān)系),^符號就是進行異或運算啦。如果沒有masking key則直接通過slice方法把數(shù)據(jù)截取下來。
獲取到數(shù)據(jù)后,放進datas里保存,因為有可能數(shù)據(jù)被分片了,所以再將stat里的長度減去當(dāng)前數(shù)據(jù)長度,只有當(dāng)stat里的長度為0的時候,說明當(dāng)前幀為最后一幀,然后通過Buffer.concat將所有數(shù)據(jù)合并,此時再判斷一下opcode,如果opcode為8,則說明客戶端發(fā)起了一個關(guān)閉請求,而我們獲取到的數(shù)據(jù)則是關(guān)閉原因。如果不為8,則這數(shù)據(jù)就是我們需要的數(shù)據(jù)。然后再將stat重置為null,datas數(shù)組置空即可。至此,我們的數(shù)據(jù)解析就完成了。
WebSocket.prototype.dataHandle = function (data) { this.handleDataStat(data); var stat; if (!(stat = this.stat)) return; //如果opcode為9,則發(fā)送pong響應(yīng),如果opcode為10則置pingtimes為0 if (stat.opcode === 9 || stat.opcode === 10) { (stat.opcode === 9) ? (this.sendPong()) : (this.pingTimes = 0); this.reset(); return; } var result; if (stat.maskedData) { result = new Buffer(data.length-stat.index); for (var i = stat.index, j = 0; i < data.length; i++, j++) { //對每個字節(jié)進行異或運算,masked是4個字節(jié),所以%4,借此循環(huán) result[j] = data[i] ^ stat.maskedData[j % 4]; } } else { result = data.slice(stat.index, data.length); } this.datas.push(result); stat.length -= (data.length - stat.index); //當(dāng)長度為0,說明當(dāng)前幀為最后幀 if (stat.length == 0) { var buf = Buffer.concat(this.datas, stat.totalLength); if (stat.opcode == 8) { this.close(buf.toString()); } else { this.emit("message", buf.toString()); } this.reset(); } };
完成了客戶端發(fā)來的數(shù)據(jù)解析,還需要一個服務(wù)端發(fā)數(shù)據(jù)至客戶端的方法,也就是按照上面所說的幀定義來組裝數(shù)據(jù)并且發(fā)送出去。下面的代碼中基本上每一行都有注釋,應(yīng)該還是比較容易理解的。
//數(shù)據(jù)發(fā)送 WebSocket.prototype.send = function (message) { if(this.state !== "OPEN") return; message = String(message); var length = Buffer.byteLength(message); // 數(shù)據(jù)的起始位置,如果數(shù)據(jù)長度16位也無法描述,則用64位,即8字節(jié),如果16位能描述則用2字節(jié),否則用第二個字節(jié)描述 var index = 2 + (length > 65535 ? 8 : (length > 125 ? 2 : 0)); // 定義buffer,長度為描述字節(jié)長度 + message長度 var buffer = new Buffer(index + length); // 第一個字節(jié),fin位為1,opcode為1 buffer[0] = 129; // 因為是由服務(wù)端發(fā)至客戶端,所以無需masked掩碼 if (length > 65535) { buffer[1] = 127; // 長度超過65535的則由8個字節(jié)表示,因為4個字節(jié)能表達(dá)的長度為4294967295,已經(jīng)完全夠用,因此直接將前面4個字節(jié)置0 buffer.writeUInt32BE(0, 2); buffer.writeUInt32BE(length, 6); } else if (length > 125) { buffer[1] = 126; // 長度超過125的話就由2個字節(jié)表示 buffer.writeUInt16BE(length, 2); } else { buffer[1] = length; } // 寫入正文 buffer.write(message, index); this.socket.write(buffer); };
最后還要實現(xiàn)一個功能,就是心跳檢測:防止服務(wù)端長時間不與客戶端交互而導(dǎo)致客戶端關(guān)閉連接,所以每隔十秒都會發(fā)送一次ping進行心跳檢測
//每隔10秒進行一次心跳檢測,若連續(xù)發(fā)出三次心跳卻沒收到響應(yīng)則關(guān)閉socket WebSocket.prototype.checkHeartBeat = function () { var that = this; setTimeout(function () { if (that.state !== "OPEN") return; if (that.pingTimes >= 3) { that.close("time out"); return; } //記錄心跳次數(shù) that.pingTimes++; that.sendPing(); that.checkHeartBeat(); }, 10000); }; WebSocket.prototype.sendPing = function () { this.socket.write(new Buffer(['0x89', '0x0'])) }; WebSocket.prototype.sendPong = function () { this.socket.write(new Buffer(['0x8A', '0x0'])) };
至此,整個websocket的實現(xiàn)就完成了,此demo只是大概實現(xiàn)了一下websocket而已,在安全之類方面肯定還是有很多問題,若是真正生產(chǎn)環(huán)境中還是用socket.io這類成熟的插件比較好。不過這還是很值得一學(xué)的。
以上內(nèi)容就是小編給大家分享的淺析nodejs實現(xiàn)Websocket的數(shù)據(jù)接收與發(fā)送的全部內(nèi)容,希望大家喜歡。
- Nodejs學(xué)習(xí)筆記之NET模塊
- Node.js利用Net模塊實現(xiàn)多人命令行聊天室的方法
- Nodejs核心模塊之net和http的使用詳解
- Nodejs進階:核心模塊net入門學(xué)習(xí)與實例講解
- Node.js net模塊功能及事件監(jiān)聽用法分析
- 用nodejs搭建websocket服務(wù)器
- WebSocket+node.js創(chuàng)建即時通信的Web聊天服務(wù)器
- 基于Node.js的WebSocket通信實現(xiàn)
- node.js基于express使用websocket的方法
- NodeJS簡單實現(xiàn)WebSocket功能示例
- Node.js websocket使用socket.io庫實現(xiàn)實時聊天室
- node.js 使用 net 模塊模擬 websocket 握手進行數(shù)據(jù)傳遞操作示例
相關(guān)文章
koa2 用戶注冊、登錄校驗與加鹽加密的實現(xiàn)方法
這篇文章主要介紹了koa2 用戶注冊、登錄校驗與加鹽加密的實現(xiàn)方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價值 ,需要的朋友可以參考下2019-07-07Node.js API詳解之 string_decoder用法實例分析
這篇文章主要介紹了Node.js API詳解之 string_decoder用法,結(jié)合實例形式分析了Node.js API中string_decoder的功能、用法及操作注意事項,需要的朋友可以參考下2020-04-04socket.io學(xué)習(xí)教程之深入學(xué)習(xí)篇(三)
這篇文章更加深入的給大家介紹了socket.io的相關(guān)資料,之前已經(jīng)介紹了socket.io的基本教程和應(yīng)用,本文更為深入的來介紹下socket.io的使用,需要的朋友可以參考借鑒,下面來一起看看吧。2017-04-04node.js中的fs.createReadStream方法使用說明
這篇文章主要介紹了node.js中的fs.createReadStream方法使用說明,本文介紹了fs.createReadStream方法說明、語法、接收參數(shù)、使用實例和實現(xiàn)源碼,需要的朋友可以參考下2014-12-12express提供http服務(wù)功能實現(xiàn)示例
這篇文章主要為大家介紹了express提供http服務(wù)功能實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10