c#解决TCP“粘包”问题
一:TCP粘包产生的原理
1,TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。
2,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。
3,这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
二:组件化解决粘包
通过第三方的组件处理这个问题比较方便和快捷,比如国产开源的HP-SOCKET等。
HP-Socket 是一套通用的高性能 TCP/UDP/HTTP 通信框架,包含服务端组件、客户端组件和Agent组件,广泛适用于各种不同应用场景的 TCP/UDP/HTTP 通信系统,提供 C/C++、C#、Delphi、E(易语言)、Java、Python 等编程语言接口。HP-Socket 对通信层实现完全封装,应用程序不必关注通信层的任何细节;HP-Socket 提供基于事件通知模型的 API 接口,能非常简单高效地整合到新旧应用程序中。
为了让使用者能方便快速地学习和使用 HP-Socket ,迅速掌握框架的设计思想和使用方法,特此精心制作了大量 Demo 示例(如:PUSH 模型示例、PULL 模型示例、PACK 模型示例、性能测试示例以及其它编程语言示例)。HP-Socket 目前支持 Windows 和 Linux 平台。
HP-Socket 的设计充分注重功能、通用型、易用性与伸缩性:
通用性
HP-Socket 的唯一职责就是接收和发送字节流,不参与应用程序的协议解析等工作。
HP-Socket 与应用程序通过接口进行交互,并完全解耦。任何应用只要实现了HP-Socket的接口规范都可以无缝整合 HP-Socket。
易用性
易用性对所有通用框架都是至关重要的,如果太难用还不如自己重头写一个来得方便。因此,HP-Socket 的接口设计得非常简单和统一。
HP-Socket 完全封装了所有底层通信细节,应用程序不必也不能干预底层通信操作。通信连接被抽象为Connection ID,Connection ID 作为连接的唯一标识提供给应用程序来处理不同的连接。
HP-Socket 提供 PUSH / PULL / PACK 等接收模型, 应用程序可以灵活选择以手工方式、 半自动方式或全自动方式处理封解包, PULL / PACK 接收模型在降低封解包处理复杂度的同时能大大减少出错几率。
高性能
Server 组件:基于IOCP / EPOLL通信模型,并结合缓存池、私有堆等技术实现高效内存管理,支持超大规模、高并发通信场景。
Agent 组件:Agent组件实质上是Multi-Client组件,与Server组件采用相同的技术架构。一个Agent组件对象可同时建立和高效处理大规模Socket连接。
Client 组件:基于Event Select / POLL通信模型,每个组件对象创建一个通信线程并管理一个Socket连接,适用于小规模客户端场景。
伸缩性
应用程序能够根据不同的容量要求、通信规模和资源状况等现实场景调整 HP-Socket 的各项性能参数(如:工作线程的数量、缓存池的大小、发送模式和接收模式等),优化资源配置,在满足应用需求的同时不必过度浪费资源。
三:解决原理及代码实现
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框架的服务器端来接收数据)
int headSize = ;//包头长度 固定4
byte[] surplusBuffer = null;//不完整的数据包,即用户自定义缓冲区
/// <summary>
/// 接收客户端发来的数据
/// </summary>
/// <param name="connId">每个客户的会话ID</param>
/// <param name="bytes">缓冲区数据</param>
/// <returns></returns>
private HandleResult OnReceive(IntPtr connId, byte[] bytes)
{
//bytes 为系统缓冲区数据
//bytesRead为系统缓冲区长度
int bytesRead = bytes.Length;
if (bytesRead > )
{
if (surplusBuffer == null)//判断是不是第一次接收,为空说是第一次
surplusBuffer = bytes;//把系统缓冲区数据放在自定义缓冲区里面
else
surplusBuffer = surplusBuffer.Concat(bytes).ToArray();//拼接上一次剩余的包
//已经完成读取每个数据包长度
int haveRead = ;
//这里totalLen的长度有可能大于缓冲区大小的(因为 这里的surplusBuffer 是系统缓冲区+不完整的数据包)
int totalLen = surplusBuffer.Length;
while (haveRead <= totalLen)
{
//如果在N此拆解后剩余的数据包连一个包头的长度都不够
//说明是上次读取N个完整数据包后,剩下的最后一个非完整的数据包
if (totalLen - haveRead < headSize)
{
byte[] byteSub = new byte[totalLen - haveRead];
//把剩下不够一个完整的数据包存起来
Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, , totalLen - haveRead);
surplusBuffer = byteSub;
totalLen = ;
break;
}
//如果够了一个完整包,则读取包头的数据
byte[] headByte = new byte[headSize];
Buffer.BlockCopy(surplusBuffer, haveRead, headByte, , headSize);//从缓冲区里读取包头的字节
int bodySize = BitConverter.ToInt32(headByte, );//从包头里面分析出包体的长度 //这里的 haveRead=等于N个数据包的长度 从0开始;0,1,2,3....N
//如果自定义缓冲区拆解N个包后的长度 大于 总长度,说最后一段数据不够一个完整的包了,拆出来保存
if (haveRead + headSize + bodySize > totalLen)
{
byte[] byteSub = new byte[totalLen - haveRead];
Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, , totalLen - haveRead);
surplusBuffer = byteSub;
break;
}
else
{
//挨个分解每个包,解析成实际文字
String strc = 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
}
}
}
}
return HandleResult.Ok;
}
值此完成拆包解析文字工作。但实际上还没完成,如果这段代码是客户端接收来自服务器的数据的话就没问题了。
仔细看IntPtr connId 每个连接的会话ID
private HandleResult OnReceive(IntPtr connId, byte[] bytes)
{
}
但是服务器端还要分辨出 每个数据包是哪个会话产生的,因为服务器端是多线程,多用户的模式,第一个数据包和第二个可能来自不同会话的数据,所以上面的代码只适用于单会话模式。
下面我要解决这个问题。
采用c#安全的ConcurrentDictionary,具体参考 https://msdn.microsoft.com/zh-cn/library/dd287191(v=vs.110).aspx
最新的代码
//线程安全的字典
ConcurrentDictionary<IntPtr, byte[]> dic = new ConcurrentDictionary<IntPtr, byte[]>();
int headSize = ;//包头长度 固定4
/// <summary>
/// 接收客户端发来的数据
/// </summary>
/// <param name="connId">每个客户的会话ID</param>
/// <param name="bytes">缓冲区数据</param>
/// <returns></returns>
private HandleResult OnReceive(IntPtr connId, byte[] bytes)
{
//bytes 为系统缓冲区数据
//bytesRead为系统缓冲区长度
int bytesRead = bytes.Length;
if (bytesRead > )
{
byte[] surplusBuffer = null;
if (dic.TryGetValue(connId, out surplusBuffer))
{
byte[] curBuffer = surplusBuffer.Concat(bytes).ToArray();//拼接上一次剩余的包
//更新会话ID 的最新字节
dic.TryUpdate(connId, curBuffer, surplusBuffer);
surplusBuffer = curBuffer;//同步
}
else
{
//添加会话ID的bytes
dic.TryAdd(connId, bytes);
surplusBuffer = bytes;//同步
} //已经完成读取每个数据包长度
int haveRead = ;
//这里totalLen的长度有可能大于缓冲区大小的(因为 这里的surplusBuffer 是系统缓冲区+不完整的数据包)
int totalLen = surplusBuffer.Length;
while (haveRead <= totalLen)
{
//如果在N此拆解后剩余的数据包连一个包头的长度都不够
//说明是上次读取N个完整数据包后,剩下的最后一个非完整的数据包
if (totalLen - haveRead < headSize)
{
byte[] byteSub = new byte[totalLen - haveRead];
//把剩下不够一个完整的数据包存起来
Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, , totalLen - haveRead);
dic.TryUpdate(connId, byteSub, surplusBuffer);
surplusBuffer = byteSub;
totalLen = ;
break;
}
//如果够了一个完整包,则读取包头的数据
byte[] headByte = new byte[headSize];
Buffer.BlockCopy(surplusBuffer, haveRead, headByte, , headSize);//从缓冲区里读取包头的字节
int bodySize = BitConverter.ToInt32(headByte, );//从包头里面分析出包体的长度 //这里的 haveRead=等于N个数据包的长度 从0开始;0,1,2,3....N
//如果自定义缓冲区拆解N个包后的长度 大于 总长度,说最后一段数据不够一个完整的包了,拆出来保存
if (haveRead + headSize + bodySize > totalLen)
{
byte[] byteSub = new byte[totalLen - haveRead];
Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, , totalLen - haveRead);
dic.TryUpdate(connId, byteSub, surplusBuffer);
surplusBuffer = byteSub;
break;
}
else
{
//挨个分解每个包,解析成实际文字
String strc = Encoding.UTF8.GetString(surplusBuffer, haveRead + headSize, bodySize);
AddMsg(string.Format(" > {0}[OnReceive] -> {1}", connId, strc));
//依次累加当前的数据包的长度
haveRead = haveRead + headSize + bodySize; //如果当前接收的数据包长度正好等于缓冲区长度,则待拼接的不规则数据长度归0
if (headSize + bodySize == bytesRead)
{
byte[] xbtye=null;
dic.TryRemove(connId, out xbtye);
surplusBuffer = null;//设置空 回到原始状态
totalLen = ;//清0
}
}
}
}
return HandleResult.Ok;
}
这样就解决了,多客户端会话造成的接收混乱。至此所有工作完成。以上代码就是为了参考学习,如果实在不想这么麻烦。可以直接使用HP-SOCKET通信框架的PACK模型,里面自动实现了解决粘包的问题。
c#解决TCP“粘包”问题的更多相关文章
- python套接字解决tcp粘包问题
python套接字解决tcp粘包问题 目录 什么是粘包 演示粘包现象 解决粘包 实际应用 什么是粘包 首先只有tcp有粘包现象,udp没有粘包 socket收发消息的原理 发送端可以是一K一K地发送数 ...
- Netty使用LineBasedFrameDecoder解决TCP粘包/拆包
TCP粘包/拆包 TCP是个”流”协议,所谓流,就是没有界限的一串数据.TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TC ...
- 深入学习Netty(5)——Netty是如何解决TCP粘包/拆包问题的?
前言 学习Netty避免不了要去了解TCP粘包/拆包问题,熟悉各个编解码器是如何解决TCP粘包/拆包问题的,同时需要知道TCP粘包/拆包问题是怎么产生的. 在此博文前,可以先学习了解前几篇博文: 深入 ...
- netty 解决TCP粘包与拆包问题(二)
TCP以流的方式进行数据传输,上层应用协议为了对消息的区分,采用了以下几种方法. 1.消息固定长度 2.第一篇讲的回车换行符形式 3.以特殊字符作为消息结束符的形式 4.通过消息头中定义长度字段来标识 ...
- netty 解决TCP粘包与拆包问题(一)
1.什么是TCP粘包与拆包 首先TCP是一个"流"协议,犹如河中水一样连成一片,没有严格的分界线.当我们在发送数据的时候就会出现多发送与少发送问题,也就是TCP粘包与拆包.得不到我 ...
- 1. Netty解决Tcp粘包拆包
一. TCP粘包问题 实际发送的消息, 可能会被TCP拆分成很多数据包发送, 也可能把很多消息组合成一个数据包发送 粘包拆包发生的原因 (1) 应用程序一次写的字节大小超过socket发送缓冲区大小 ...
- 【转】Netty之解决TCP粘包拆包(自定义协议)
1.什么是粘包/拆包 一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据.TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消 ...
- Netty之解决TCP粘包拆包(自定义协议)
1.什么是粘包/拆包 一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据.TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消 ...
- golang 解决 TCP 粘包问题
什么是 TCP 粘包问题以及为什么会产生 TCP 粘包,本文不加讨论.本文使用 golang 的 bufio.Scanner 来实现自定义协议解包. 协议数据包定义 本文模拟一个日志服务器,该服务器接 ...
随机推荐
- 8天入门docker系列 —— 第四天 使用aspnetcore小案例熟悉端口映射和挂载目录
到目前为止大家应该对镜像和容器有了一个大概认知,而且也用了docker进行了一个简单化的部署,但仔细一看问题还有很多,所以这篇我们继续完善. 一:如何让外网访问到容器内应用 我们知道容器内拥有自己的子 ...
- JS 图片放大镜
今天练习一个小demo, 从本地读取图片, 然后实现类似淘宝放大镜的效果, 再加两个需求 1 .可以调节缩放比例,默认放大两倍 2 . 图片宽高自适应, 不固定宽高 话不多说先看效果: 原理:1, 右 ...
- java游戏开发杂谈 - java编程怎么学
java语言包含很多的知识点,我们并不需要把java语言的知识点都学全了,才开始编程. 你只需要了解: 1,java的数据类型和变量定义 2,类和对象的初步印象. 3,if-else, wh ...
- Python3|ddt|unittest|浅议数据驱动测试
目录 1.DDT简介 2.data装饰器 3.unpack装饰器 4.file_data装饰器 5.总结 1.DDT简介 Data-Driven Tests(DDT)即数据驱动测试.它允许您通过不同的 ...
- 10 年三线小城 IT 开发的感悟
一贯都是写技术博客,从来没写过感悟类文章,因为文笔不好.今天看到了大飞的一篇文章,<技术人,请不要封闭自己>,真的感触太深了. 一 先说说我自己,我并非科班出身,大学毕业后一直没找到好的工 ...
- 女朋友也能看懂的Zookeeper分布式锁原理
前言 关于分布式锁,在互联网行业的使用场景还是比较多的,比如电商的库存扣减,秒杀活动,集群定时任务执行等需要进程互斥的场景.而实现分布式锁的手段也很多,大家比较常见的就是redis跟zookeep ...
- 关于vue使用form上传文件
在vue中使用form表单上传文件文件的时候出现了一些问题,获取文件的时候一直返回null, 解决之后又出现发送到后台的file文件后台显示为空,解决源码 <template> <d ...
- Aria2序之导言
aria2是什么,如果此时此刻你看到标题读到第一行正在思索着一个问题,很抱歉 ,这篇以及后续我的blog不适合你,请移步. 我在aria2的官方站点抄袭了如下语句用来充实本文: aria2 is a ...
- ajax分页借鉴
大家好这是我分页是用的代码希望大家可以相互交流ajax局部刷新 var pageindex = 1; var where = ""; var Pname = "" ...
- JS点击图片更改照片
<img src="../../img/20190224185111.png" alt="" id="zhaopian"/> - ...