我们知道,TCP是面向连接流传输的,其采用Nagle算法,在缓冲区对上层数据进行了处理。避免触发自动分片机制和网络上大量小数据包的同时也造成了粘包(小包合并)和半包(大包拆分)问题,导致数据没有消息保护边界,接收端接收到一次数据无法判断是否是一个完整数据包。那有什么方案可以解决这问题呢?

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);

5、总结

从上面我们已经了解到了封包解包的一个过程。TCP是可靠传输的,同一时间在网络上只会有一个数据包,并且丢包会重传,因此不用担心丢包或者数据包乱序问题。UDP有消息保护边界,不需要进行拆包解包,然后其是非可靠传输,也需要解决其他一些问题,譬如丢包和数据包排序问题。

上面进行数据包结构设计时只是简单地加了一个包体长度,事实上在业务场景可以自由增加需要的字段,譬如协议版本,协议类型等等。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持Devmax。

基于Nodejs的Tcp封包和解包的理解的更多相关文章

  1. 当iOS应用程序进入后台时,TCP和UDP(与多播)连接会发生什么

    我创建了几个实验:设置1:我创建了一个TCPSender应用程序和一个TCPReceiver应用程序.在本次实验中,我在iOS设备上启动了TCPSender,在另一台iOS设备上启动了TCPReceiver.然后两者都经过验证已建立连接并发送和接收数据.然后我将TCPReceiver应用程序置于后台.TCPSender应用程序指示连接丢失和崩溃(是的,我打算这样).设置2:我创建了一个UDPSen

  2. 在iOS中,如何增加主机的HTTP连接限制?

    使用Xcode网络工具,我分析说,我每次只能建立每个主机的4个TCP连接.似乎iOS的每个主机的默认TCP连接限制为4.我们如何增加这个限制?

  3. Swift开发:GCDAsyncSocket通信之TCP服务器

    overridefuncviewDidLoad(){super.viewDidLoad()clientSockets=NSMutableArray()msgTextView.backgroundColor=UIColor.grayColor()msgTextView.text="接收的客户端消息:\n"}//发送消息按钮@IBActionfuncsendBtnClick{ifclientSockets.count==0{return}letmsg=inputTextInput.text!//1.处理请求,

  4. Swift开发:GCDAsyncSocket通信之TCP 客户端

    varmainQueue=dispatch_get_main_queue()overridefuncviewDidLoad(){super.viewDidLoad()}//连接服务器按钮事件@IBActionfuncconBtnClick{do{clientSocket=GCDAsyncSocket()clientSocket.delegate=selfclientSocket.delegateQueue=dispatch_get_global_queue(0,0)tryclientSocket.conn

  5. Android GCM讯息需要太长时间才能到来

    我在应用程序中使用GCM,我有一个问题.大部分时间我马上收到邮件,但有时邮件会在5分钟后再次出现,就像他们被困在路上.这是正常吗?

  6. Android TCP连接最佳做法

    我正在处理一个需要TCP连接到TCP服务器的Android应用程序我的AndroidTCP客户端正在工作可以来回发送消息.我的奇怪问题是:>在Android中处理与服务器的TCP连接的最佳方式是什么?>如何维护连接正确关闭连接)?

  7. nodejs npm package.json中文文档

    这篇文章主要介绍了nodejs npm package.json中文文档,本文档中描述的很多行为都受npm-config(7)的影响,需要的朋友可以参考下

  8. 浅析Nodejs npm常用命令

    这篇文章主要介绍了浅析Nodejs npm常用命令的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下

  9. nodejs 使用nodejs-websocket模块实现点对点实时通讯

    这篇文章主要介绍了nodejs 使用nodejs-websocket模块实现点对点实时通讯的实例代码,代码简单易懂,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下

  10. nodeJs链接Mysql做增删改查的简单操作

    本篇文章主要介绍了nodeJs链接Mysql做增删改查的简单操作,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

随机推荐

  1. Error: Cannot find module ‘node:util‘问题解决

    控制台 安装 Vue-Cli 最后一步出现 Error: Cannot find module 'node:util' 问题解决方案1.问题C:\Windows\System32>cnpm install -g @vue/cli@4.0.3internal/modules/cjs/loader.js:638 throw err; &nbs

  2. yarn的安装和使用(全网最详细)

    一、yarn的简介:Yarn是facebook发布的一款取代npm的包管理工具。二、yarn的特点:速度超快。Yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快。超级安全。在执行代码之前,Yarn 会通过算法校验每个安装包的完整性。超级可靠。使用详细、简洁的锁文件格式和明确的安装算法,Yarn 能够保证在不同系统上无差异的工作。三、y

  3. 前端环境 本机可切换node多版本 问题源头是node使用的高版本

    前言投降投降 重头再来 重装环境 也就分分钟的事 偏要折腾 这下好了1天了 还没折腾出来问题的源头是node 使用的高版本 方案那就用 本机可切换多版本最终问题是因为nodejs的版本太高,导致的node-sass不兼容问题,我的node是v16.14.0的版本,项目中用了"node-sass": "^4.7.2"版本,无法匹配当前的node版本根据文章的提

  4. nodejs模块学习之connect解析

    这篇文章主要介绍了nodejs模块学习之connect解析,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  5. nodejs npm package.json中文文档

    这篇文章主要介绍了nodejs npm package.json中文文档,本文档中描述的很多行为都受npm-config(7)的影响,需要的朋友可以参考下

  6. 详解koa2学习中使用 async 、await、promise解决异步的问题

    这篇文章主要介绍了详解koa2学习中使用 async 、await、promise解决异步的问题,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  7. Node.js编写爬虫的基本思路及抓取百度图片的实例分享

    这篇文章主要介绍了Node.js编写爬虫的基本思路及抓取百度图片的实例分享,其中作者提到了需要特别注意GBK转码的转码问题,需要的朋友可以参考下

  8. CentOS 8.2服务器上安装最新版Node.js的方法

    这篇文章主要介绍了CentOS 8.2服务器上安装最新版Node.js的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  9. node.js三个步骤实现一个服务器及Express包使用

    这篇文章主要介绍了node.js三个步骤实现一个服务器及Express包使用,文章通过新建一个文件展开全文内容,具有一定的参考价值,需要的小伙伴可以参考一下

  10. node下使用UglifyJS压缩合并JS文件的方法

    下面小编就为大家分享一篇node下使用UglifyJS压缩合并JS文件的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

返回
顶部