基于Nodejs的Tcp封包和解包的理解
我們知道,TCP是面向連接流傳輸的,其采用Nagle算法,在緩沖區(qū)對上層數據進行了處理。避免觸發(fā)自動分片機制和網絡上大量小數據包的同時也造成了粘包(小包合并)和半包(大包拆分)問題,導致數據沒有消息保護邊界,接收端接收到一次數據無法判斷是否是一個完整數據包。那有什么方案可以解決這問題呢?
1、粘包問題解決方案及對比
很簡單,既然消息沒有邊界,那我們在消息往下傳之前給它加一個邊界識別就好了。
- 發(fā)送固定長度的消息
- 使用特殊標記來區(qū)分消息間隔
- 把消息的尺寸與消息一塊發(fā)送
第一種方案不夠靈活;第二種有風險,如果數據內剛好有該特殊字符會出問題;第三種方案雖然要增加對消息頭的解析,不過相對而言還是要安全一些。
2、分包與拆包
既然使用第三種方案,就必然涉及到封包和拆包的問題。
首先肯定需要定義數據包的結構,這類似Http包一樣,有包頭和包體。包頭其實上是個大小固定的結構體,其中有個結構體成員變量表示包體的長度,其他的結構體成員可根據需要自己定義。根據包頭長度固定以及包頭中含有包體長度的變量就能正確的拆分出一個完整的數據包。包體則存放數據內容。

在發(fā)送端,需要進行封包。封包就是給一段數據加上包頭,這樣一來數據包就分為包頭和包體兩部分內容了。
在接受端,則需要進行拆包。主要流程如下:
1. 為每一個連接動態(tài)分配一個緩沖區(qū),同時把此緩沖區(qū)和SOCKET關聯(lián).
2. 當接收到數據時首先把此段數據存放在緩沖區(qū)中.
3. 判斷緩存區(qū)中的數據長度是否夠一個包頭的長度,如不夠,則不進行拆包操作.
4. 根據包頭數據解析出里面代表包體長度的變量.
5. 判斷緩存區(qū)中除包頭外的數據長度是否夠一個包體的長度,如不夠,則不進行拆包操作.
6. 取出整個數據包.這里的"取"的意思是不光從緩沖區(qū)中拷貝出數據包,而且要把此數據包從緩存區(qū)中刪除掉.刪除的辦法就是把此包后面的數據移動到緩沖區(qū)的起始地址.
其中對于緩沖區(qū)的設計,主要由倆種:
2. 采用環(huán)形緩沖區(qū),定義兩個指針,分別指向有效數據的頭和尾.在存放數據和刪除數據時只是進行頭尾指針的移動

3、網絡字節(jié)序和本機字節(jié)序
定義了消息結構之后,發(fā)送端和接收端還需要統(tǒng)一字節(jié)序。我們知道,不同機器的本機字節(jié)序不同,絕大多數X86機器都是小端字節(jié)序,然后還是由少數機器是大端存儲的。因此在數據流進行傳輸時,必須先統(tǒng)一字節(jié)序。一般約定在傳輸時采用網絡字節(jié)序(大端),統(tǒng)一用unicode編碼。
4、代碼實現(xiàn)
了解以上知識之后,我們現(xiàn)在之后要做什么了。發(fā)送端按定義的協(xié)議規(guī)則封包,接受端把接收到的buffer放入緩沖區(qū),當緩沖區(qū)內有完整包時開始拆包。封包拆包過程需要注意,讀寫超過一個字節(jié)的數據時需要按大端字節(jié)序讀取。下面看node的代碼實現(xiàn)(只提供核心實現(xiàn)片段):
1)發(fā)送端封包:
let head = new Buffer(4); let jsonStr = JSON.stringify(json); let body = new Buffer(jsonStr); //超過一字節(jié)的大端寫入 head.writeInt32BE(body.byteLength, 0); let buffer = Buffer.concat([head, body]);
2)接收端收到buffer入緩沖區(qū):
let dataReadStart = 0; //新數據的起始位置
let dataLength = buffer.length; // 要拷貝數據的長度
let availableLen = _bufferLength - _dataLen; // 緩沖區(qū)剩余可用空間
// 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) {
// 空間夠用情況下,但是數據會沖破緩沖區(qū)尾部,部分存到緩沖區(qū)舊數據后,一部分存到緩沖區(qū)開始位置
// 緩沖區(qū)尾部剩余空間的長度
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 { // 剩余空間足夠存儲數據,直接拷貝數據到緩沖區(qū)
buffer.copy(_buffer, _writePointer, dataReadStart, dataReadStart + dataLength);
_dataLen = _dataLen + dataLength;
_writePointer = _writePointer + dataLength
}
3)取出緩沖區(qū)所有完整數據包(收到的buffer入緩沖區(qū)后)
let _dataHeadLen = 4;
timer && clearInterval(timer);
timer = setInterval(()=>{
// 緩沖區(qū)數據不夠解析出包頭
if (_dataLen < _dataHeadLen) {
console.log('數據長度小于包頭規(guī)定長度,等待數據......')
clearInterval(timer);
}
// 解析包頭長度
// 尾部最后剩余可讀字節(jié)長度
let restDataLen = _bufferLength - _readPointer;
let dataLen = 0;
let headBuffer = Buffer.alloc(_dataHeadLen);
// 數據包為分段存儲,不能直接解析出包頭,先拼接
if (restDataLen < _dataHeadLen) {
// 取出第一部分頭部字節(jié)
_buffer.copy(headBuffer, 0, _readPointer, _bufferLength)
// 取出第二部分頭部字節(jié)
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("緩沖區(qū)已有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長度有問題')
}
//傳遞給業(yè)務層
callback(content);
5、總結
從上面我們已經了解到了封包解包的一個過程。TCP是可靠傳輸的,同一時間在網絡上只會有一個數據包,并且丟包會重傳,因此不用擔心丟包或者數據包亂序問題。UDP有消息保護邊界,不需要進行拆包解包,然后其是非可靠傳輸,也需要解決其他一些問題,譬如丟包和數據包排序問題。
上面進行數據包結構設計時只是簡單地加了一個包體長度,事實上在業(yè)務場景可以自由增加需要的字段,譬如協(xié)議版本,協(xié)議類型等等。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
Node中node_modules文件夾及package.json文件的作用說明
這篇文章主要介紹了Node中node_modules文件夾及package.json文件的作用說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09
nodejs基于WS模塊實現(xiàn)WebSocket聊天功能的方法
這篇文章主要介紹了nodejs基于WS模塊實現(xiàn)WebSocket聊天功能的方法,結合實例形式分析了nodejs使用WS模塊進行WebSocket通信實現(xiàn)聊天功能的具體操作技巧,需要的朋友可以參考下2018-01-01

