基于Nodejs对Tcp包和解包的理解
我们知道,TCP面向连接流传输,使用Nagle算法处理缓冲区中的上层数据。避免触发网络上的自动分片机制和大量的小数据包,还会造成卡包(包合并)和半包(包拆分)的问题,导致数据没有报文保护边界,接收方收到一次数据后无法判断是否是完整的数据包。那么这个问题有什么解决办法呢?
1.包装粘连问题的解决方法及比较
很简单,由于消息没有边界,我们应该在消息向下传输之前给它添加一个边界识别。
发送固定长度的消息使用特殊的标记来区分消息间隔,第一种方案不够灵活,无法与消息一起发送消息;第二个有风险。如果数据中只有这种特殊的字符,就会出现问题;虽然第三种方案需要增加消息头的解析,但相对来说还是比较安全的。
2.分包和拆包
既然采用第三种方案,必然会涉及到包装和拆封的问题。
首先,需要定义数据包的结构,它和Http包一样,都有一个头和包体。包头实际上是一个固定大小的结构,其中一个结构成员变量代表包的长度,其他结构成员可以根据自己的需要自行定义。一个完整的数据包可以根据包头的固定长度和包头中包含的包体长度的变量被正确分割。包主体存储数据内容。
在发送端,需要打包。Packet是给一段数据加上一个包头,使数据包分成两部分:包头和包体。
在接收端,需要拆包。主要过程如下:
1.为每个连接动态分配一个缓冲区,并将该缓冲区与SOCKET相关联。2.当接收数据时,首先将该数据存储在缓冲区中。3.判断缓冲区中的数据长度对于一个包头是否足够。如果没有,则不执行解包操作。4.根据包头数据分析代表包体长度的变量。5.确定缓冲区中除包头之外的数据长度是否足够长。否则,不执行解包操作。6.取出整个数据包。这里的“提取”不仅意味着从缓冲区复制数据包,还意味着从缓冲区删除数据包。删除的方法是将数据包后面的数据移动到
其中,缓冲设计有两种:
1.缓冲区大小根据数据大小进行调整。这个方案有一个缺点。为了避免缓冲区不断增长,每次解析完一个完整的数据包后,都需要将缓冲区中剩余的数据复制到缓冲区头,这增加了系统负载。2.使用循环缓冲区,定义了两个指针,分别指向有效数据的头部和尾部。存储数据和删除数据时,只移动头指针和尾指针。
3.网络字节顺序和本机字节顺序
定义消息结构后,发送方和接收方也需要统一的字节顺序。我们知道,不同机器的本机字节顺序是不同的。大多数X86机器是小端字节顺序,然后少数机器是大端存储。因此,在传输数据流时,必须首先统一字节顺序。一般同意传输采用网络字节顺序(大端),编码使用unicode。
4.代码实现
了解了以上知识,现在和以后我们要做什么?发送方根据定义的协议规则打包数据包,接收方将接收到的缓冲区放入缓冲区,当缓冲区中有完整的数据包时,开始解包。应注意数据包拆包过程。当读写一个以上字节的数据时,需要按大字节顺序读取。我们来看看node的代码实现(只提供核心实现片段):
1)发送端包:
让head=新的Buffer(4);让jsonStr=JSON . stringify(JSON);让body=new Buffer(jsonStr);//写头。writeint32be (body。bytelength,0 ),位于超过一个字节的大端;让buffer=Buffer.concat([head,body]);2)接收端将缓冲区接收到缓冲区中:
让DataReadStart=0;//新数据的起始位置让数据长度=buffer.length//要拷贝数据的长度让可用len=_ bufferLength-_ dataLen;//缓冲区剩余可用空间//缓冲区剩余空间不足够存储本次数据if(可用len dataLength){让新长度=数学。ceil((_ dataLen dataLength)/_缓冲区长度)* _缓冲区长度;let _ tempBuffer=buffer。alloc(NewLength);//将旧数据复制到新缓冲器并且修正相关参数if(_写指针_读指针){//数据存储在旧缓冲器的尾部头部的顺序让dataTailLen=_ bufferLength-_读指针;_buffer.copy(_tempBuffer,0,_readPointer,_ read pointer datatailen);_buffer.copy(_tempBuffer,dataTailLen,0,_ write pointer);} else { //数据是按照顺序进行的完整存储_buffer.copy(_tempBuffer,0,_readPointer,_ write pointer);} _ buffer length=newLength _ buffer=_ tempBuffer _ tempBuffer=null _ read pointer=0;_ writePointer=_ dataLen//存储新到来的buffer.copy(_buffer,_writePointer,dataReadStart,DataLength);_ dataLen=dataLength _ write pointer=dataLength } else if(_ write pointer dataLength _ buffer length){//空间够用情况下,但是数据会冲破缓冲区尾部,部分存到缓冲区旧数据后,一部分存到缓冲区开始位置//缓冲区尾部剩余空间的长度让buffer tail lengh=_ buffer length-_写指针;//数据尾部位置让dataEndPosition=dataReadStart bufferTailLength;buffer.copy(_buffer,_writePointer,dataReadStart,dataend位置);//数据剩余未拷贝进缓存的长度让restDataLen=dataLength-bufferTailLength;buffer.copy(_buffer,0,dataEndPosition,dataLength);_ dataLen=_ dataLen dataLength _ write指针=restDataLen } else {//剩余空间足够存储数据,直接拷贝数据到缓冲区buffer.copy(_buffer,_writePointer,dataReadStart,DataLength);_ dataLen=_ dataLen dataLength _写指针=_写指针dataLength } 3)取出缓冲区所有完整数据包(收到的缓冲器入缓冲区后)
let _ dataHeadLen=4;定时器clearInterval(定时器);Timer=setInterval(()={ //如果(_ datalen _ dataheadlen){ console . log,缓冲区数据不足以解析标头('数据长度小于标头的指定长度,请等待数据.')clearInterval(计时器);}//解析头长度//尾部最后剩余可读字节长度let rest datalen=_ buffer length-_ read pointer;让dataLen=0;让head buffer=buffer . alloc(_ dataHeadLen);//数据包是分段存储的,所以无法直接解析包头。首先拼接if (restDataLen _dataHeadLen) {//取出第一个头字节_ buffer.copy(头缓冲区,0,_ readpointer,_ buffer length)//取出第二个头字节let unradheadlen=_ dataHeadLen-rest datalen;_buffer.copy(headBuffer,restDataLen,0,Unradheadlen)dataLen=head buffer . readuint 32 be(0);} else { _buffer.copy(headBuffer,0,_readPointer,_ read pointer _ dataHeadLen);dataLen=head buffer . readuint 32 be(0);}//数据长度不够读取,所以直接返回if(_ datalen-_ dataheadlendatalen){ log . info('缓冲区中正文数据的长度小于头定义的正文长度,等待数据.')clearInterval(计时器);} else {//数据足够读取,读取的数据包let package=buffer . alloc(datalen);//数据是分段存储的,如果(_ buffer length-_ readpointerdata len){ let first part len=_ buffer length-_ read pointer,则需要读取两次;//读取第一部分,并将数据直接写到character _ buffer.copy的末尾(package,0,_ readpointer,first part len _ read pointer);//读取第二部分,开头存储的数据让secondpartlen=datalen-first partlen;_buffer.copy(package,firstPartLen,0,secondPartLen);_ readPointer=secondPartLen//更新可读的起点} else {//直接读取数据_ buffer.copy (package,0,_ readpointer,_ read pointer datalen);_ readPointer=dataLen//更新可读起点} _ dataLen-=readdata . length;//更新数据长度//读取所有数据if(_ read pointer===_ write pointer){ clear interval(timer)}//开始解包回调(package);}}, 50);4)解包获取数据
让HeAdBytes=4;让head=新缓冲区(head bytes);buffer.copy(head,0,0,HeAdBytes);让dataLen=head . readuint 32 be();const body=新缓冲区(DataLen);buffer.copy(body,0,headBytes,headBytes dataLen)让内容=null请尝试{ const str=body . tostring(' utf-8 ');if(str==' '){ content=null;} else { content=JSON . parse(body);}} catch (e) {log.error('head指定正文长度有问题')}//传递给业务层回调(content);5.摘要
从上面,我们学习了一个包拆包的过程。TCP传输可靠,同时网络上只有一个包,丢包会重传,不用担心丢包或者包乱。UDP有报文保护边界,不需要解包、解包,那么就是不可靠的传输,还有一些其他问题需要解决,比如丢包、包排序等。
在设计上面的数据包结构时,我们只是简单的增加了一个包长,其实我们可以自由的添加所需的字段,比如协议版本、协议类型等等。
以上就是本文的全部内容。希望对大家的学习有帮助,支持我们。
版权声明:基于Nodejs对Tcp包和解包的理解是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。