您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關Nodejs中Tcp封包和解包的示例分析的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
1、粘包問題解決方案及對比
很簡單,既然消息沒有邊界,那我們在消息往下傳之前給它加一個邊界識別就好了。
發送固定長度的消息
使用特殊標記來區分消息間隔
把消息的尺寸與消息一塊發送
第一種方案不夠靈活;第二種有風險,如果數據內剛好有該特殊字符會出問題;第三種方案雖然要增加對消息頭的解析,不過相對而言還是要安全一些。
2、分包與拆包
既然使用第三種方案,就必然涉及到封包和拆包的問題。
首先肯定需要定義數據包的結構,這類似Http包一樣,有包頭和包體。包頭其實上是個大小固定的結構體,其中有個結構體成員變量表示包體的長度,其他的結構體成員可根據需要自己定義。根據包頭長度固定以及包頭中含有包體長度的變量就能正確的拆分出一個完整的數據包。包體則存放數據內容。
在發送端,需要進行封包。封包就是給一段數據加上包頭,這樣一來數據包就分為包頭和包體兩部分內容了。
在接受端,則需要進行拆包。主要流程如下:
1. 為每一個連接動態分配一個緩沖區,同時把此緩沖區和SOCKET關聯.
2. 當接收到數據時首先把此段數據存放在緩沖區中.
3. 判斷緩存區中的數據長度是否夠一個包頭的長度,如不夠,則不進行拆包操作.
4. 根據包頭數據解析出里面代表包體長度的變量.
5. 判斷緩存區中除包頭外的數據長度是否夠一個包體的長度,如不夠,則不進行拆包操作.
6. 取出整個數據包.這里的"取"的意思是不光從緩沖區中拷貝出數據包,而且要把此數據包從緩存區中刪除掉.刪除的辦法就是把此包后面的數據移動到緩沖區的起始地址.
其中對于緩沖區的設計,主要由倆種:
1. 采用動態變化的緩沖區暫存,根據數據大小調整緩沖區大小。這個方案有個缺點,為了避免緩沖區不斷增長,每次解析出一個完整包后需要將緩沖區殘留的數據拷貝到緩沖區首部,這增加了系統負載。
2. 采用環形緩沖區,定義兩個指針,分別指向有效數據的頭和尾.在存放數據和刪除數據時只是進行頭尾指針的移動
3、網絡字節序和本機字節序
定義了消息結構之后,發送端和接收端還需要統一字節序。我們知道,不同機器的本機字節序不同,絕大多數X86機器都是小端字節序,然后還是由少數機器是大端存儲的。因此在數據流進行傳輸時,必須先統一字節序。一般約定在傳輸時采用網絡字節序(大端),統一用unicode編碼。
4、代碼實現
了解以上知識之后,我們現在之后要做什么了。發送端按定義的協議規則封包,接受端把接收到的buffer放入緩沖區,當緩沖區內有完整包時開始拆包。封包拆包過程需要注意,讀寫超過一個字節的數據時需要按大端字節序讀取。下面看node的代碼實現(只提供核心實現片段):
1)發送端封包:
let head = new Buffer(4); let jsonStr = JSON.stringify(json); let body = new Buffer(jsonStr); //超過一字節的大端寫入 head.writeInt32BE(body.byteLength, 0); let buffer = Buffer.concat([head, body]);
2)接收端收到buffer入緩沖區:
let dataReadStart = 0; //新數據的起始位置 let dataLength = buffer.length; // 要拷貝數據的長度 let availableLen = _bufferLength - _dataLen; // 緩沖區剩余可用空間 // buffer剩余空間不足夠存儲本次數據 if (availableLen < dataLength) { let newLength = Math.ceil((_dataLen + dataLength) / _bufferLength) * _bufferLength; let _tempBuffer = Buffer.alloc(newLength); // 將舊數據復制到新buffer并且修正相關參數 if (_writePointer < _readPointer) { // 數據存儲在舊buffer的尾部+頭部的順序 let dataTailLen = _bufferLength - _readPointer; _buffer.copy(_tempBuffer, 0, _readPointer, _readPointer + dataTailLen); _buffer.copy(_tempBuffer, dataTailLen, 0, _writePointer); } else { // 數據是按照順序進行的完整存儲 _buffer.copy(_tempBuffer, 0, _readPointer, _writePointer); } _bufferLength = newLength; _buffer = _tempBuffer; _tempBuffer = null; _readPointer = 0; _writePointer = _dataLen; //存儲新到來的buffer buffer.copy(_buffer, _writePointer, dataReadStart, dataReadStart + dataLength); _dataLen += dataLength; _writePointer += dataLength; } else if (_writePointer + dataLength > _bufferLength) { // 空間夠用情況下,但是數據會沖破緩沖區尾部,部分存到緩沖區舊數據后,一部分存到緩沖區開始位置 // 緩沖區尾部剩余空間的長度 let bufferTailLength = _bufferLength - _writePointer; // 數據尾部位置 let dataEndPosition = dataReadStart + bufferTailLength; buffer.copy(_buffer, _writePointer, dataReadStart, dataEndPosition); // data剩余未拷貝進緩存的長度 let restDataLen = dataLength - bufferTailLength; buffer.copy(_buffer, 0, dataEndPosition, dataLength); _dataLen = _dataLen + dataLength; _writePointer = restDataLen } else { // 剩余空間足夠存儲數據,直接拷貝數據到緩沖區 buffer.copy(_buffer, _writePointer, dataReadStart, dataReadStart + dataLength); _dataLen = _dataLen + dataLength; _writePointer = _writePointer + dataLength }
3)取出緩沖區所有完整數據包(收到的buffer入緩沖區后)
let _dataHeadLen = 4; timer && clearInterval(timer); timer = setInterval(()=>{ // 緩沖區數據不夠解析出包頭 if (_dataLen < _dataHeadLen) { console.log('數據長度小于包頭規定長度,等待數據......') clearInterval(timer); } // 解析包頭長度 // 尾部最后剩余可讀字節長度 let restDataLen = _bufferLength - _readPointer; let dataLen = 0; let headBuffer = Buffer.alloc(_dataHeadLen); // 數據包為分段存儲,不能直接解析出包頭,先拼接 if (restDataLen < _dataHeadLen) { // 取出第一部分頭部字節 _buffer.copy(headBuffer, 0, _readPointer, _bufferLength) // 取出第二部分頭部字節 let unReadHeadLen = _dataHeadLen - restDataLen; _buffer.copy(headBuffer, restDataLen, 0, unReadHeadLen) dataLen = headBuffer.readUInt32BE(0); } else { _buffer.copy(headBuffer, 0, _readPointer, _readPointer + _dataHeadLen); dataLen = headBuffer.readUInt32BE(0);; } // 數據長度不夠讀取,直接返回 if (_dataLen - _dataHeadLen < dataLen) { log.info("緩沖區已有body數據長度小于包頭定義body的長度,等待數據......") clearInterval(timer); } else { // 數據夠讀,讀取數據包 let package = Buffer.alloc(dataLen); // 數據是分段存儲,需要分兩次讀取 if (_bufferLength - _readPointer < dataLen) { let firstPartLen = _bufferLength - _readPointer; // 讀取第一部分,直接到字符尾部的數據 _buffer.copy(package, 0, _readPointer, firstPartLen + _readPointer); // 讀取第二部分,存儲在開頭的數據 let secondPartLen = dataLen - firstPartLen; _buffer.copy(package, firstPartLen, 0, secondPartLen); _readPointer = secondPartLen; //更新可讀起點 } else { // 直接讀取數據 _buffer.copy(package, 0, _readPointer, _readPointer + dataLen); _readPointer += dataLen; //更新可讀起點 } _dataLen -= readData.length; //更新數據長度 // 已經讀取完所有數據 if (_readPointer === _writePointer) { clearInterval(timer) } //開始解包 callback(package); } }, 50);
4)拆包得到數據
let headBytes = 4; let head = new Buffer(headBytes); buffer.copy(head, 0, 0, headBytes); let dataLen = head.readUInt32BE(); const body = new Buffer(dataLen); buffer.copy(body, 0, headBytes, headBytes + dataLen) let content = null; try { const str = body.toString('utf-8'); if(str === ''){ content = null; }else{ content = JSON.parse(body); } } catch (e) { log.error('head指定body長度有問題') } //傳遞給業務層 callback(content);
感謝各位的閱讀!關于“Nodejs中Tcp封包和解包的示例分析”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。