Nodejs中Tcp封包和解包的示例分析

這篇文章給大家分享的是有關(guān)Nodejs中Tcp封包和解包的示例分析的內(nèi)容。小編覺(jué)得挺實(shí)用的,因此分享給大家做個(gè)參考,一起跟隨小編過(guò)來(lái)看看吧。

站在用戶的角度思考問(wèn)題,與客戶深入溝通,找到宜良網(wǎng)站設(shè)計(jì)與宜良網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗(yàn),讓設(shè)計(jì)與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個(gè)性化、用戶體驗(yàn)好的作品,建站類(lèi)型包括:網(wǎng)站設(shè)計(jì)、成都網(wǎng)站建設(shè)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣、國(guó)際域名空間、虛擬主機(jī)、企業(yè)郵箱。業(yè)務(wù)覆蓋宜良地區(qū)。

1、粘包問(wèn)題解決方案及對(duì)比

很簡(jiǎn)單,既然消息沒(méi)有邊界,那我們?cè)谙⑼聜髦敖o它加一個(gè)邊界識(shí)別就好了。

  • 發(fā)送固定長(zhǎng)度的消息

  • 使用特殊標(biāo)記來(lái)區(qū)分消息間隔

  • 把消息的尺寸與消息一塊發(fā)送

第一種方案不夠靈活;第二種有風(fēng)險(xiǎn),如果數(shù)據(jù)內(nèi)剛好有該特殊字符會(huì)出問(wèn)題;第三種方案雖然要增加對(duì)消息頭的解析,不過(guò)相對(duì)而言還是要安全一些。

2、分包與拆包

既然使用第三種方案,就必然涉及到封包和拆包的問(wèn)題。

首先肯定需要定義數(shù)據(jù)包的結(jié)構(gòu),這類(lèi)似Http包一樣,有包頭和包體。包頭其實(shí)上是個(gè)大小固定的結(jié)構(gòu)體,其中有個(gè)結(jié)構(gòu)體成員變量表示包體的長(zhǎng)度,其他的結(jié)構(gòu)體成員可根據(jù)需要自己定義。根據(jù)包頭長(zhǎng)度固定以及包頭中含有包體長(zhǎng)度的變量就能正確的拆分出一個(gè)完整的數(shù)據(jù)包。包體則存放數(shù)據(jù)內(nèi)容。

Nodejs中Tcp封包和解包的示例分析

在發(fā)送端,需要進(jìn)行封包。封包就是給一段數(shù)據(jù)加上包頭,這樣一來(lái)數(shù)據(jù)包就分為包頭和包體兩部分內(nèi)容了。

在接受端,則需要進(jìn)行拆包。主要流程如下:

1. 為每一個(gè)連接動(dòng)態(tài)分配一個(gè)緩沖區(qū),同時(shí)把此緩沖區(qū)和SOCKET關(guān)聯(lián).
2. 當(dāng)接收到數(shù)據(jù)時(shí)首先把此段數(shù)據(jù)存放在緩沖區(qū)中.
3. 判斷緩存區(qū)中的數(shù)據(jù)長(zhǎng)度是否夠一個(gè)包頭的長(zhǎng)度,如不夠,則不進(jìn)行拆包操作.
4. 根據(jù)包頭數(shù)據(jù)解析出里面代表包體長(zhǎng)度的變量.
5. 判斷緩存區(qū)中除包頭外的數(shù)據(jù)長(zhǎng)度是否夠一個(gè)包體的長(zhǎng)度,如不夠,則不進(jìn)行拆包操作.
6. 取出整個(gè)數(shù)據(jù)包.這里的"取"的意思是不光從緩沖區(qū)中拷貝出數(shù)據(jù)包,而且要把此數(shù)據(jù)包從緩存區(qū)中刪除掉.刪除的辦法就是把此包后面的數(shù)據(jù)移動(dòng)到緩沖區(qū)的起始地址.

其中對(duì)于緩沖區(qū)的設(shè)計(jì),主要由倆種:

1. 采用動(dòng)態(tài)變化的緩沖區(qū)暫存,根據(jù)數(shù)據(jù)大小調(diào)整緩沖區(qū)大小。這個(gè)方案有個(gè)缺點(diǎn),為了避免緩沖區(qū)不斷增長(zhǎng),每次解析出一個(gè)完整包后需要將緩沖區(qū)殘留的數(shù)據(jù)拷貝到緩沖區(qū)首部,這增加了系統(tǒng)負(fù)載。
2. 采用環(huán)形緩沖區(qū),定義兩個(gè)指針,分別指向有效數(shù)據(jù)的頭和尾.在存放數(shù)據(jù)和刪除數(shù)據(jù)時(shí)只是進(jìn)行頭尾指針的移動(dòng)

Nodejs中Tcp封包和解包的示例分析 Nodejs中Tcp封包和解包的示例分析

3、網(wǎng)絡(luò)字節(jié)序和本機(jī)字節(jié)序

定義了消息結(jié)構(gòu)之后,發(fā)送端和接收端還需要統(tǒng)一字節(jié)序。我們知道,不同機(jī)器的本機(jī)字節(jié)序不同,絕大多數(shù)X86機(jī)器都是小端字節(jié)序,然后還是由少數(shù)機(jī)器是大端存儲(chǔ)的。因此在數(shù)據(jù)流進(jìn)行傳輸時(shí),必須先統(tǒng)一字節(jié)序。一般約定在傳輸時(shí)采用網(wǎng)絡(luò)字節(jié)序(大端),統(tǒng)一用unicode編碼。

Nodejs中Tcp封包和解包的示例分析 

4、代碼實(shí)現(xiàn)

了解以上知識(shí)之后,我們現(xiàn)在之后要做什么了。發(fā)送端按定義的協(xié)議規(guī)則封包,接受端把接收到的buffer放入緩沖區(qū),當(dāng)緩沖區(qū)內(nèi)有完整包時(shí)開(kāi)始拆包。封包拆包過(guò)程需要注意,讀寫(xiě)超過(guò)一個(gè)字節(jié)的數(shù)據(jù)時(shí)需要按大端字節(jié)序讀取。下面看node的代碼實(shí)現(xiàn)(只提供核心實(shí)現(xiàn)片段):

1)發(fā)送端封包:

let head = new Buffer(4);
let jsonStr = JSON.stringify(json);
let body = new Buffer(jsonStr);
//超過(guò)一字節(jié)的大端寫(xiě)入
head.writeInt32BE(body.byteLength, 0);
let buffer = Buffer.concat([head, body]);

2)接收端收到buffer入緩沖區(qū):

let dataReadStart = 0; //新數(shù)據(jù)的起始位置
let dataLength = buffer.length; // 要拷貝數(shù)據(jù)的長(zhǎng)度
let availableLen = _bufferLength - _dataLen; // 緩沖區(qū)剩余可用空間

// buffer剩余空間不足夠存儲(chǔ)本次數(shù)據(jù)
if (availableLen < dataLength) {
 let newLength = Math.ceil((_dataLen + dataLength) / _bufferLength) * _bufferLength;
 let _tempBuffer = Buffer.alloc(newLength);
 
 // 將舊數(shù)據(jù)復(fù)制到新buffer并且修正相關(guān)參數(shù)
 if (_writePointer < _readPointer) { // 數(shù)據(jù)存儲(chǔ)在舊buffer的尾部+頭部的順序
  let dataTailLen = _bufferLength - _readPointer;
  _buffer.copy(_tempBuffer, 0, _readPointer, _readPointer + dataTailLen);
  _buffer.copy(_tempBuffer, dataTailLen, 0, _writePointer);
 } else { // 數(shù)據(jù)是按照順序進(jìn)行的完整存儲(chǔ)
  _buffer.copy(_tempBuffer, 0, _readPointer, _writePointer);
 }
 _bufferLength = newLength;
 _buffer = _tempBuffer;
 _tempBuffer = null;
 _readPointer = 0;
 _writePointer = _dataLen;

 //存儲(chǔ)新到來(lái)的buffer
 buffer.copy(_buffer, _writePointer, dataReadStart, dataReadStart + dataLength);
 _dataLen += dataLength;
 _writePointer += dataLength;

} else if (_writePointer + dataLength > _bufferLength) {
// 空間夠用情況下,但是數(shù)據(jù)會(huì)沖破緩沖區(qū)尾部,部分存到緩沖區(qū)舊數(shù)據(jù)后,一部分存到緩沖區(qū)開(kāi)始位置
 // 緩沖區(qū)尾部剩余空間的長(zhǎng)度
 let bufferTailLength = _bufferLength - _writePointer;

 // 數(shù)據(jù)尾部位置
 let dataEndPosition = dataReadStart + bufferTailLength;
 buffer.copy(_buffer, _writePointer, dataReadStart, dataEndPosition);

 // data剩余未拷貝進(jìn)緩存的長(zhǎng)度
 let restDataLen = dataLength - bufferTailLength;
 buffer.copy(_buffer, 0, dataEndPosition, dataLength);

 _dataLen = _dataLen + dataLength;
 _writePointer = restDataLen

} else { // 剩余空間足夠存儲(chǔ)數(shù)據(jù),直接拷貝數(shù)據(jù)到緩沖區(qū)
 buffer.copy(_buffer, _writePointer, dataReadStart, dataReadStart + dataLength);
 _dataLen = _dataLen + dataLength;
 _writePointer = _writePointer + dataLength
}

3)取出緩沖區(qū)所有完整數(shù)據(jù)包(收到的buffer入緩沖區(qū)后)

let _dataHeadLen = 4;
timer && clearInterval(timer);
timer = setInterval(()=>{
 // 緩沖區(qū)數(shù)據(jù)不夠解析出包頭
 if (_dataLen < _dataHeadLen) {
  console.log('數(shù)據(jù)長(zhǎng)度小于包頭規(guī)定長(zhǎng)度,等待數(shù)據(jù)......')
  clearInterval(timer);
 }
 // 解析包頭長(zhǎng)度
 // 尾部最后剩余可讀字節(jié)長(zhǎng)度
 let restDataLen = _bufferLength - _readPointer;
 let dataLen = 0;
 let headBuffer = Buffer.alloc(_dataHeadLen);
 // 數(shù)據(jù)包為分段存儲(chǔ),不能直接解析出包頭,先拼接
 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);;
 }

 // 數(shù)據(jù)長(zhǎng)度不夠讀取,直接返回
 if (_dataLen - _dataHeadLen < dataLen) {
  log.info("緩沖區(qū)已有body數(shù)據(jù)長(zhǎng)度小于包頭定義body的長(zhǎng)度,等待數(shù)據(jù)......")
  clearInterval(timer);

 } else { // 數(shù)據(jù)夠讀,讀取數(shù)據(jù)包 
  let package = Buffer.alloc(dataLen);
  // 數(shù)據(jù)是分段存儲(chǔ),需要分兩次讀取
  if (_bufferLength - _readPointer < dataLen) {
   let firstPartLen = _bufferLength - _readPointer;
   // 讀取第一部分,直接到字符尾部的數(shù)據(jù)
   _buffer.copy(package, 0, _readPointer, firstPartLen + _readPointer);
   // 讀取第二部分,存儲(chǔ)在開(kāi)頭的數(shù)據(jù)
   let secondPartLen = dataLen - firstPartLen;
   _buffer.copy(package, firstPartLen, 0, secondPartLen);
   _readPointer = secondPartLen; //更新可讀起點(diǎn)

  } else { // 直接讀取數(shù)據(jù)
   _buffer.copy(package, 0, _readPointer, _readPointer + dataLen);
   _readPointer += dataLen; //更新可讀起點(diǎn)
  }

  _dataLen -= readData.length; //更新數(shù)據(jù)長(zhǎng)度
  // 已經(jīng)讀取完所有數(shù)據(jù)
  if (_readPointer === _writePointer) {
   clearInterval(timer)
  }

  //開(kāi)始解包
  callback(package);
   
 }
}, 50);

4)拆包得到數(shù)據(jù)

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長(zhǎng)度有問(wèn)題')
}
//傳遞給業(yè)務(wù)層
callback(content);

感謝各位的閱讀!關(guān)于“Nodejs中Tcp封包和解包的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,讓大家可以學(xué)到更多知識(shí),如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到吧!

新聞名稱(chēng):Nodejs中Tcp封包和解包的示例分析
文章URL:http://muchs.cn/article28/gcepcp.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供用戶體驗(yàn)、ChatGPT微信小程序、品牌網(wǎng)站建設(shè)、外貿(mào)網(wǎng)站建設(shè)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

小程序開(kāi)發(fā)