原文:C#高性能大容量SOCKET并发(五):粘包、分包、解包

粘包

使用TCP长连接就会引入粘包的问题,粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。粘包可能由发送方造成,也可能由接收方造成。TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据,造成多个数据包的粘连。如果接收进程不及时接收数据,已收到的数据就放在系统接收缓冲区,用户进程读取数据时就可能同时读到多个数据包。

粘包一般的解决办法是制定通讯协议,由协议来规定如何分包解包。

分包

在NETIOCPDemo例子程序中,我们分包的逻辑是先发一个长度,然后紧接着是数据包内容,这样就可以把每个包分开。

应用层数据包格式如下:

应用层数据包格式  
数据包长度Len:Cardinal(4字节无符号整数) 数据包内容,长度为Len

AsyncSocketInvokeElement分包处理主要代码,我们收到的数据都是在ProcessReceive方法中处理,处理的方法是把收到的数据存到缓冲区数组中,然后取前4个字节为长度,如果剩下的字节数大于等于长度,则取到一个完整包,进行后续逻辑处理,如果取到的不够一个包,则不处理,等待后续包接收,具体代码如下:

        public virtual bool ProcessReceive(byte[] buffer, int offset, int count) //接收异步事件返回的数据,用于对数据进行缓存和分包
{
m_activeDT = DateTime.UtcNow;
DynamicBufferManager receiveBuffer = m_asyncSocketUserToken.ReceiveBuffer; receiveBuffer.WriteBuffer(buffer, offset, count);
if (receiveBuffer.DataCount > sizeof(int))
{
//按照长度分包
int packetLength = BitConverter.ToInt32(receiveBuffer.Buffer, 0); //获取包长度
if (NetByteOrder)
packetLength = System.Net.IPAddress.NetworkToHostOrder(packetLength); //把网络字节顺序转为本地字节顺序 if ((packetLength > 10 * 1024 * 1024) | (receiveBuffer.DataCount > 10 * 1024 * 1024)) //最大Buffer异常保护
return false; if ((receiveBuffer.DataCount - sizeof(int)) >= packetLength) //收到的数据达到包长度
{
bool result = ProcessPacket(receiveBuffer.Buffer, sizeof(int), packetLength);
if (result)
receiveBuffer.Clear(packetLength + sizeof(int)); //从缓存中清理
return result;
}
else
{
return true;
}
}
else
{
return true;
}
}

解包

由于我们应用层数据包既可以传命令也可以传数据,因而针对每个包我们进行解包,分出命令和数据分别处理,因而每个Socket服务对象都需要解包,我们解包的逻辑是放在ProcessPacket中,命令和数据的包格式为:

命令长度Len:Cardinal(4字节无符号整数) 命令 数据
        public virtual bool ProcessPacket(byte[] buffer, int offset, int count) //处理分完包后的数据,把命令和数据分开,并对命令进行解析
{
if (count < sizeof(int))
return false;
int commandLen = BitConverter.ToInt32(buffer, offset); //取出命令长度
string tmpStr = Encoding.UTF8.GetString(buffer, offset + sizeof(int), commandLen);
if (!m_incomingDataParser.DecodeProtocolText(tmpStr)) //解析命令
return false; return ProcessCommand(buffer, offset + sizeof(int) + commandLen, count - sizeof(int) - commandLen); //处理命令
}

每个包中包含多个协议关键字,每个协议关键字用回车换行分开,因此我们需要调用文本分开函数,然后针对每条命令解析出关键字和值,具体代码在IncomingDataParser.DecodeProtocolText如下:

        public bool DecodeProtocolText(string protocolText)
{
m_header = "";
m_names.Clear();
m_values.Clear();
int speIndex = protocolText.IndexOf(ProtocolKey.ReturnWrap);
if (speIndex < 0)
{
return false;
}
else
{
string[] tmpNameValues = protocolText.Split(new string[] { ProtocolKey.ReturnWrap }, StringSplitOptions.RemoveEmptyEntries);
if (tmpNameValues.Length < 2) //每次命令至少包括两行
return false;
for (int i = 0; i < tmpNameValues.Length; i++)
{
string[] tmpStr = tmpNameValues[i].Split(new string[] { ProtocolKey.EqualSign }, StringSplitOptions.None);
if (tmpStr.Length > 1) //存在等号
{
if (tmpStr.Length > 2) //超过两个等号,返回失败
return false;
if (tmpStr[0].Equals(ProtocolKey.Command, StringComparison.CurrentCultureIgnoreCase))
{
m_command = tmpStr[1];
}
else
{
m_names.Add(tmpStr[0].ToLower());
m_values.Add(tmpStr[1]);
}
}
}
return true;
}
}

处理命令

解析出命令后,需要对每个命令进行处理,各个协议实现类从AsyncSocketInvokeElement.ProcessCommand继承,然后编写各自协议处理逻辑,如吞吐量的测试协议逻辑实现代码如下:

namespace SocketAsyncSvr
{
class ThroughputSocketProtocol : BaseSocketProtocol
{
public ThroughputSocketProtocol(AsyncSocketServer asyncSocketServer, AsyncSocketUserToken asyncSocketUserToken)
: base(asyncSocketServer, asyncSocketUserToken)
{
m_socketFlag = "Throughput";
} public override void Close()
{
base.Close();
} public override bool ProcessCommand(byte[] buffer, int offset, int count) //处理分完包的数据,子类从这个方法继承
{
ThroughputSocketCommand command = StrToCommand(m_incomingDataParser.Command);
m_outgoingDataAssembler.Clear();
m_outgoingDataAssembler.AddResponse();
m_outgoingDataAssembler.AddCommand(m_incomingDataParser.Command);
if (command == ThroughputSocketCommand.CyclePacket)
return DoCyclePacket(buffer, offset, count);
else
{
Program.Logger.Error("Unknow command: " + m_incomingDataParser.Command);
return false;
}
} public ThroughputSocketCommand StrToCommand(string command)
{
if (command.Equals(ProtocolKey.CyclePacket, StringComparison.CurrentCultureIgnoreCase))
return ThroughputSocketCommand.CyclePacket;
else
return ThroughputSocketCommand.None;
} public bool DoCyclePacket(byte[] buffer, int offset, int count)
{
int cycleCount = 0;
if (m_incomingDataParser.GetValue(ProtocolKey.Count, ref cycleCount))
{
m_outgoingDataAssembler.AddSuccess();
cycleCount = cycleCount + 1;
m_outgoingDataAssembler.AddValue(ProtocolKey.Count, cycleCount);
}
else
m_outgoingDataAssembler.AddFailure(ProtocolCode.ParameterError, "");
return DoSendResult(buffer, offset, count);
}
}
}

DEMO下载地址:http://download.csdn.net/detail/sqldebug_fan/7467745

免责声明:此代码只是为了演示C#完成端口编程,仅用于学习和研究,切勿用于商业用途。水平有限,C#也属于初学,错误在所难免,欢迎指正和指导。邮箱地址:fansheng_hx@163.com。

C#高性能大容量SOCKET并发(五):粘包、分包、解包的更多相关文章

  1. C#高性能大容量SOCKET并发(六):超时Socket断开(守护线程)和心跳包

    原文:C#高性能大容量SOCKET并发(六):超时Socket断开(守护线程)和心跳包 守护线程 在服务端版Socket编程需要处理长时间没有发送数据的Socket,需要在超时多长时间后断开连接,我们 ...

  2. C#高性能大容量SOCKET并发(转)

    C#高性能大容量SOCKET并发(零):代码结构说明 C#高性能大容量SOCKET并发(一):IOCP完成端口例子介绍 C#高性能大容量SOCKET并发(二):SocketAsyncEventArgs ...

  3. C#高性能大容量SOCKET并发(十一):编写上传客户端

    原文:C#高性能大容量SOCKET并发(十一):编写上传客户端 客户端封装整体框架 客户端编程基于阻塞同步模式,只有数据正常发送或接收才返回,如果发生错误则抛出异常,基于TcpClient进行封装,主 ...

  4. C#高性能大容量SOCKET并发(零):代码结构说明

    原文:C#高性能大容量SOCKET并发(零):代码结构说明 C#版完成端口具有以下特点: 连接在线管理(提供在线连接维护,连接会话管理,数据接收,连接断开等相关事件跟踪): 发送数据智能合并(组件会根 ...

  5. C#高性能大容量SOCKET并发(九):断点续传

    原文:C#高性能大容量SOCKET并发(九):断点续传 上传断点续传 断点续传主要是用在上传或下载文件,一般做法是开始上传的时候,服务器返回上次已经上传的大小,如果上传完成,则返回-1:下载开始的时候 ...

  6. C#高性能大容量SOCKET并发(七):协议字符集

    原文:C#高性能大容量SOCKET并发(七):协议字符集 UTF-8 UTF-8是UNICODE的一种变长字符编码又称万国码,由Ken Thompson于1992年创建.现在已经标准化为RFC 362 ...

  7. C#高性能大容量SOCKET并发(三):接收、发送

    原文:C#高性能大容量SOCKET并发(三):接收.发送 异步数据接收有可能收到的数据不是一个完整包,或者接收到的数据超过一个包的大小,因此我们需要把接收的数据进行缓存.异步发送我们也需要把每个发送的 ...

  8. C#高性能大容量SOCKET并发(四):缓存设计

    原文:C#高性能大容量SOCKET并发(四):缓存设计 在编写服务端大并发的应用程序,需要非常注意缓存设计,缓存的设计是一个折衷的结果,需要通过并发测试反复验证.有很多服务程序是在启动时申请足够的内存 ...

  9. C#高性能大容量SOCKET并发(二):SocketAsyncEventArgs封装

    原文:C#高性能大容量SOCKET并发(二):SocketAsyncEventArgs封装 1.SocketAsyncEventArgs介绍 SocketAsyncEventArgs是微软提供的高性能 ...

随机推荐

  1. Mybatis找不到参数错误:There is no getter for property named 'categoryId' in 'class java.lang.Integer'。

    Mybatis找不到参数错误:There is no getter for property named 'categoryId' in 'class java.lang.Integer'. 错误Li ...

  2. 【codeforces 750F】New Year and Finding Roots

    time limit per test2 seconds memory limit per test256 megabytes inputstandard input outputstandard o ...

  3. java类中属性优先执行顺序

    1.父类静态代码块 (  java虚拟机加载类时,就会执行该块代码,故只执行一次) 2 .子类静态代码块 (  java虚拟机加载类时,就会执行该块代码,故只执行一次) 3. 父类属性对象初始化 4. ...

  4. Hibernate的数据操作(4.*以上版本)

    Hibernate的基本数据操作 适用于4.* ..sessionFactory的创建区别 public class NewsTest { private Session session = null ...

  5. P2P理财友情提示

    最近2年,P2P理财非常火,但是出现的问题也是越来越频繁. 2014年12月,据说有70多家平台出现了问题,加上以前的,一共有300多家了,出现问题的占总比有20%~30%了. 这个真的是非常的可怕. ...

  6. radio选择事件 onchange事件 onclick事件

    单选框按钮(radio)选择事件怎么设置呢? 既可以在radio标签里设置onclick事件实现,也可以设置它的onchange事件实现,效果一样,代码如下: <input id="r ...

  7. hudson搭建经验总结

    作者:朱金灿 来源:http://blog.csdn.net/clever101 hudson 是一种革命性的开放源码 CI (持续集成)服务器,随着工程源码越来越庞大,把源码编译工作放在本地机器已不 ...

  8. Network Function Virtualization for a Network Device

    An apparatus for performing network function virtualization (NFV), comprising: a memory, a processor ...

  9. Editplus配置Python的开发环境

    Python 有很多集成开发工具,商业的有komodo,Wingide,Boa 等,还有Python 自带的集成环境IDLE,Windows 下还有PythonWin 等.但是,这些工具有的是过于复杂 ...

  10. 编程算法 - 旋转矩阵 代码(C)

    旋转矩阵 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 输出旋转矩阵, 使矩阵是按对角线螺旋上升, 在输出规则确定以后, 就能够推断, 上升规律是, ...