这篇文章给大家分享的是有关如何解决C#中TCP粘包问题的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。
一、TCP粘包产生的原理
1.TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。
2.发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。
3.这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。、
二、解决原理及代码实现
1.采用包头(固定长度,里面存着包体的长度,发送时动态获取)+包体的传输机制。如图
HeaderSize 存放着包体的长度,其HeaderSize本身是定长4字节;
一个完整的数据包(L)=HeaderSize+BodySize;
2.分包算法
其基本思路是首先将待处理的接收数据流即系统缓冲区数据(长度设为M)强行转换成预定的结构数据形式,并从中取出结构数据长度字段L,而后根据包头计算得到第一包数据长度。
M=系统缓冲区大小;L=用户发送的数据包=HeaderSize+BodySize;
1)若L<M,则表明数据流包含多包数据,从其头部截取若干个字节存入临时缓冲区,剩余部分数据依此继续循环处理,直至结束。
2)若L=M,则表明数据流内容恰好是一完整结构数据(即用户自定义缓冲区等于系统接收缓冲区大小),直接将其存入临时缓冲区即可。
3)若L>M,则表明数据流内容尚不够构成一完整结构数据,需留待与下一包数据合并后再行处理。
4)下面是代码代码实现(HP-SOCKET框架的服务器端来接收数据)
intheadSize=4;//包头长度固定4 byte[]surplusBuffer=null;//不完整的数据包,即用户自定义缓冲区 ///<summary> ///接收客户端发来的数据 ///</summary> ///<paramname="connId">每个客户的会话ID</param> ///<paramname="bytes">缓冲区数据</param> ///<returns></returns> privateHandleResultOnReceive(IntPtrconnId,byte[]bytes) { //bytes为系统缓冲区数据 //bytesRead为系统缓冲区长度 intbytesRead=bytes.Length; if(bytesRead>0) { if(surplusBuffer==null)//判断是不是第一次接收,为空说是第一次 surplusBuffer=bytes;//把系统缓冲区数据放在自定义缓冲区里面 else surplusBuffer=surplusBuffer.Concat(bytes).ToArray();//拼接上一次剩余的包 //已经完成读取每个数据包长度 inthaveRead=0; //这里totalLen的长度有可能大于缓冲区大小的(因为这里的surplusBuffer是系统缓冲区+不完整的数据包) inttotalLen=surplusBuffer.Length; while(haveRead<=totalLen) { //如果在N此拆解后剩余的数据包连一个包头的长度都不够 //说明是上次读取N个完整数据包后,剩下的最后一个非完整的数据包 if(totalLen-haveRead<headSize) { byte[]byteSub=newbyte[totalLen-haveRead]; //把剩下不够一个完整的数据包存起来 Buffer.BlockCopy(surplusBuffer,haveRead,byteSub,0,totalLen-haveRead); surplusBuffer=byteSub; totalLen=0; break; } //如果够了一个完整包,则读取包头的数据 byte[]headByte=newbyte[headSize]; Buffer.BlockCopy(surplusBuffer,haveRead,headByte,0,headSize);//从缓冲区里读取包头的字节 intbodySize=BitConverter.ToInt32(headByte,0);//从包头里面分析出包体的长度 //这里的haveRead=等于N个数据包的长度从0开始;0,1,2,3....N //如果自定义缓冲区拆解N个包后的长度大于总长度,说最后一段数据不够一个完整的包了,拆出来保存 if(haveRead+headSize+bodySize>totalLen) { byte[]byteSub=newbyte[totalLen-haveRead]; Buffer.BlockCopy(surplusBuffer,haveRead,byteSub,0,totalLen-haveRead); surplusBuffer=byteSub; break; } else { //挨个分解每个包,解析成实际文字 Stringstrc=Encoding.UTF8.GetString(surplusBuffer,haveRead+headSize,bodySize); //AddMsg(string.Format(">[OnReceive]->{0}",strc)); //依次累加当前的数据包的长度 haveRead=haveRead+headSize+bodySize; if(headSize+bodySize==bytesRead)//如果当前接收的数据包长度正好等于缓冲区长度,则待拼接的不规则数据长度归0 { surplusBuffer=null;//设置空回到原始状态 totalLen=0;//清0 } } } } returnHandleResult.Ok; }
值此完成拆包解析文字工作。但实际上还没完成,如果这段代码是客户端接收来自服务器的数据的话就没问题了。
仔细看IntPtr connId 每个连接的会话ID
private HandleResult OnReceive(IntPtr connId, byte[] bytes)
{
}
但是服务器端还要分辨出 每个数据包是哪个会话产生的,因为服务器端是多线程,多用户的模式,第一个数据包和第二个可能来自不同会话的数据,所以上面的代码只适用于单会话模式。
下面我要解决这个问题。
采用c#安全的ConcurrentDictionary,具体参考 https://msdn.microsoft.com/zh-cn/pbrary/dd287191(v=vs.110).aspx
最新的代码
//线程安全的字典 ConcurrentDictionary<IntPtr,byte[]>dic=newConcurrentDictionary<IntPtr,byte[]>(); intheadSize=4;//包头长度固定4 ///<summary> ///接收客户端发来的数据 ///</summary> ///<paramname="connId">每个客户的会话ID</param> ///<paramname="bytes">缓冲区数据</param> ///<returns></returns> privateHandleResultOnReceive(IntPtrconnId,byte[]bytes) { //bytes为系统缓冲区数据 //bytesRead为系统缓冲区长度 intbytesRead=bytes.Length; if(bytesRead>0) { byte[]surplusBuffer=null; if(dic.TryGetValue(connId,outsurplusBuffer)) { byte[]curBuffer=surplusBuffer.Concat(bytes).ToArray();//拼接上一次剩余的包 //更新会话ID的最新字节 dic.TryUpdate(connId,curBuffer,surplusBuffer); surplusBuffer=curBuffer;//同步 } else { //添加会话ID的bytes dic.TryAdd(connId,bytes); surplusBuffer=bytes;//同步 } //已经完成读取每个数据包长度 inthaveRead=0; //这里totalLen的长度有可能大于缓冲区大小的(因为这里的surplusBuffer是系统缓冲区+不完整的数据包) inttotalLen=surplusBuffer.Length; while(haveRead<=totalLen) { //如果在N此拆解后剩余的数据包连一个包头的长度都不够 //说明是上次读取N个完整数据包后,剩下的最后一个非完整的数据包 if(totalLen-haveRead<headSize) { byte[]byteSub=newbyte[totalLen-haveRead]; //把剩下不够一个完整的数据包存起来 Buffer.BlockCopy(surplusBuffer,haveRead,byteSub,0,totalLen-haveRead); dic.TryUpdate(connId,byteSub,surplusBuffer); surplusBuffer=byteSub; totalLen=0; break; } //如果够了一个完整包,则读取包头的数据 byte[]headByte=newbyte[headSize]; Buffer.BlockCopy(surplusBuffer,haveRead,headByte,0,headSize);//从缓冲区里读取包头的字节 intbodySize=BitConverter.ToInt32(headByte,0);//从包头里面分析出包体的长度 //这里的haveRead=等于N个数据包的长度从0开始;0,1,2,3....N //如果自定义缓冲区拆解N个包后的长度大于总长度,说最后一段数据不够一个完整的包了,拆出来保存 if(haveRead+headSize+bodySize>totalLen) { byte[]byteSub=newbyte[totalLen-haveRead]; Buffer.BlockCopy(surplusBuffer,haveRead,byteSub,0,totalLen-haveRead); dic.TryUpdate(connId,byteSub,surplusBuffer); surplusBuffer=byteSub; break; } else { //挨个分解每个包,解析成实际文字 Stringstrc=Encoding.UTF8.GetString(surplusBuffer,haveRead+headSize,bodySize); AddMsg(string.Format(">{0}[OnReceive]->{1}",connId,strc)); //依次累加当前的数据包的长度 haveRead=haveRead+headSize+bodySize; if(headSize+bodySize==bytesRead)//如果当前接收的数据包长度正好等于缓冲区长度,则待拼接的不规则数据长度归0 { byte[]xbtye=null; dic.TryRemove(connId,outxbtye); surplusBuffer=null;//设置空回到原始状态 totalLen=0;//清0 } } } } returnHandleResult.Ok; }
这样就解决了,多客户端会话造成的接收混乱。至此所有工作完成。以上代码就是为了参考学习,如果实在不想这么麻烦。可以直接使用HP-SOCKET通信框架的PACK模型,里面自动实现了解决粘包的问题。
感谢各位的阅读!关于“如何解决C#中TCP粘包问题”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!
以上就是如何解决C#中TCP粘包问题的相关内容分享,字数约5841字,希望这对大家有所帮助!屹东网往后会继续推荐如何解决C#中TCP粘包问题相关内容。