DELPHI高性能大容量SOCKET并发(四):粘包、分包、解包
粘包
使用TCP长连接就会引入粘包的问题,粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。粘包可能由发送方造成,也可能由接收方造成。TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据,造成多个数据包的粘连。如果接收进程不及时接收数据,已收到的数据就放在系统接收缓冲区,用户进程读取数据时就可能同时读到多个数据包。
粘包一般的解决办法是制定通讯协议,由协议来规定如何分包解包。
分包
在IOCPDemo例子程序中,我们分包的逻辑是先发一个长度,然后紧接着是数据包内容,这样就可以把每个包分开。
应用层数据包格式如下:
应用层数据包格式 数据包长度Len:Cardinal(4字节无符号整数) 数据包内容,长度为Len IOCPSocket分包处理主要代码,我们收到的数据都是在TSocketHandle.ProcessIOComplete方法中处理:
- procedure TSocketHandle.ProcessIOComplete(AIocpRecord: PIocpRecord;
- const ACount: Cardinal);
- begin
- case AIocpRecord.IocpOperate of
- ioNone: Exit;
- ioRead: //收到数据
- begin
- FActiveTime := Now;
- ReceiveData(AIocpRecord.WsaBuf.buf, ACount);
- if FConnected then
- PreRecv(AIocpRecord); //投递请求
- end;
- ioWrite: //发送数据完成,需要释放AIocpRecord的指针
- begin
- FActiveTime := Now;
- FSendOverlapped.Release(AIocpRecord);
- end;
- ioStream:
- begin
- FActiveTime := Now;
- FSendOverlapped.Release(AIocpRecord);
- WriteStream; //继续发送流
- end;
- end;
- end;
如果是收到数据,则调用ReceiveData函数,ReceiveData主要功能是把数据的写入流中,然后调用Process分包。FInputBuf是一个内存流(FInputBuf: TMemoryStream),内存流的每次写入会造成一次内存分配,如果要获得更高的效率,可以替换为内存池等更好的内存管理方式。还有一种更好的解决方案是规定每次发包的大小,如每个包最大不超过64K,哪么缓冲区的最大大小可以设置为128K(缓存两个数据包),这样就可以每次创建对象时一次分配好,减少内存分配次数,提高效率。(内存的分配和释放比内存的读写效率要低)
- procedure TSocketHandle.ReceiveData(AData: PAnsiChar; const ALen: Cardinal);
- begin
- FInputBuf.Write(AData^, ALen);
- Process;
- end;
Process则根据收到的数据进行分包逻辑,如果不够一个包,则继续等待接收数据,如果够一个或多个包,则循环调用Execute函数进行处理,代码如下:
- procedure TSocketHandle.Process;
- var
- AData, ALast, NewBuf: PByte;
- iLenOffset, iOffset, iReserveLen: Integer;
- function ReadLen: Integer;
- var
- wLen: Word;
- cLen: Cardinal;
- begin
- FInputBuf.Position := iOffset;
- if FLenType = ltWord then
- begin
- FInputBuf.Read(wLen, SizeOf(wLen));
- //wLen := ntohs(wLen);
- Result := wLen;
- end
- else
- begin
- FInputBuf.Read(cLen, SizeOf(cLen));
- //cLen := ntohl(cLen);
- Result := cLen;
- end;
- end;
- begin
- case FLenType of
- ltWord, ltCardinal:
- begin
- if FLenType = ltWord then
- iLenOffset := 2
- else
- iLenOffset := 4;
- iReserveLen := 0;
- FPacketLen := 0;
- iOffset := 0;
- if FPacketLen <= 0 then
- begin
- if FInputBuf.Size < iLenOffset then Exit;
- FInputBuf.Position := 0; //移动到最前面
- FPacketLen := ReadLen;
- iOffset := iLenOffset;
- iReserveLen := FInputBuf.Size - iOffset;
- if FPacketLen > iReserveLen then //不够一个包的长度
- begin
- FInputBuf.Position := FInputBuf.Size; //移动到最后,以便接收后续数据
- FPacketLen := 0;
- Exit;
- end;
- end;
- while (FPacketLen > 0) and (iReserveLen >= FPacketLen) do //如果数据够长,则处理
- begin //多个包循环处理
- AData := Pointer(Longint(FInputBuf.Memory) + iOffset); //取得当前的指针
- Execute(AData, FPacketLen);
- iOffset := iOffset + FPacketLen; //移到下一个点
- FPacketLen := 0;
- iReserveLen := FInputBuf.Size - iOffset;
- if iReserveLen > iLenOffset then //剩下的数据
- begin
- FPacketLen := ReadLen;
- iOffset := iOffset + iLenOffset;
- iReserveLen := FInputBuf.Size - iOffset;
- if FPacketLen > iReserveLen then //不够一个包的长度,需要把长度回退
- begin
- iOffset := iOffset - iLenOffset;
- iReserveLen := FInputBuf.Size - iOffset;
- FPacketLen := 0;
- end;
- end
- else //不够长度字节数
- FPacketLen := 0;
- end;
- if iReserveLen > 0 then //把剩下的自己缓存起来
- begin
- ALast := Pointer(Longint(FInputBuf.Memory) + iOffset);
- GetMem(NewBuf, iReserveLen);
- try
- CopyMemory(NewBuf, ALast, iReserveLen);
- FInputBuf.Clear;
- FInputBuf.Write(NewBuf^, iReserveLen);
- finally
- FreeMemory(NewBuf);
- end;
- end
- else
- begin
- FInputBuf.Clear;
- end;
- end;
- else
- begin
- FInputBuf.Position := 0;
- AData := Pointer(Longint(FInputBuf.Memory)); //取得当前的指针
- Execute(AData, FInputBuf.Size);
- FInputBuf.Clear;
- end;
- end;
- end;
解包
由于我们应用层数据包既可以传命令也可以传数据,因而针对每个包我们进行解包,分出命令和数据分别处理,因而每个Socket服务对象都需要解包,我们解包的逻辑是放在TBaseSocket.DecodePacket中,命令和数据的包格式为:
命令长度Len:Cardinal(4字节无符号整数) 命令 数据 这里和第一版公布的代码不同,这版的代码对命令进行了编码,采用UTF-8编码,代码如下:
- function TBaseSocket.DecodePacket(APacketData: PByte;
- const ALen: Integer): Boolean;
- var
- CommandLen: Integer;
- UTF8Command: UTF8String;
- begin
- if ALen > 4 then //命令长度为4字节,因而长度必须大于4
- begin
- CopyMemory(@CommandLen, APacketData, SizeOf(Cardinal)); //获取命令长度
- Inc(APacketData, SizeOf(Cardinal));
- SetLength(UTF8Command, CommandLen);
- CopyMemory(PUTF8String(UTF8Command), APacketData, CommandLen); //读取命令
- Inc(APacketData, CommandLen);
- FRequestData := APacketData; //数据
- FRequestDataLen := ALen - SizeOf(Cardinal) - CommandLen; //数据长度
- FRequest.Text := Utf8ToAnsi(UTF8Command); //把UTF8转为Ansi
- Result := True;
- end
- else
- Result := False;
- end;
具体每个协议可以集成Execute方法,调用DecodePacket进行解包,然后根据命令进行协议逻辑处理,例如TSQLSocket主要代码如下:
- {* SQL查询SOCKET基类 *}
- TSQLSocket = class(TBaseSocket)
- private
- {* 开始事务创建TADOConnection,关闭事务时释放 *}
- FBeginTrans: Boolean;
- FADOConn: TADOConnection;
- protected
- {* 处理数据接口 *}
- procedure Execute(AData: PByte; const ALen: Cardinal); override;
- {* 返回SQL语句执行结果 *}
- procedure DoCmdSQLOpen;
- {* 执行SQL语句 *}
- procedure DoCmdSQLExec;
- {* 开始事务 *}
- procedure DoCmdBeginTrans;
- {* 提交事务 *}
- procedure DoCmdCommitTrans;
- {* 回滚事务 *}
- procedure DoCmdRollbackTrans;
- public
- procedure DoCreate; override;
- destructor Destroy; override;
- {* 获取SQL语句 *}
- function GetSQL: string;
- property BeginTrans: Boolean read FBeginTrans;
- end;
Exceute是调用DecodePacket进行解包,然后获取命令分别调用不同的命令处理逻辑,代码如下:
- procedure TSQLSocket.Execute(AData: PByte; const ALen: Cardinal);
- var
- sErr: string;
- begin
- inherited;
- FRequest.Clear;
- FResponse.Clear;
- try
- AddResponseHeader;
- if ALen = 0 then
- begin
- DoFailure(CIPackLenError);
- DoSendResult;
- Exit;
- end;
- if DecodePacket(AData, ALen) then
- begin
- FResponse.Clear;
- AddResponseHeader;
- case StrToSQLCommand(Command) of
- scLogin:
- begin
- DoCmdLogin;
- DoSendResult;
- end;
- scActive:
- begin
- DoSuccess;
- DoSendResult;
- end;
- scSQLOpen:
- begin
- DoCmdSQLOpen;
- end;
- scSQLExec:
- begin
- DoCmdSQLExec;
- DoSendResult;
- end;
- scBeginTrans:
- begin
- DoCmdBeginTrans;
- DoSendResult;
- end;
- scCommitTrans:
- begin
- DoCmdCommitTrans;
- DoSendResult;
- end;
- scRollbackTrans:
- begin
- DoCmdRollbackTrans;
- DoSendResult;
- end;
- else
- DoFailure(CINoExistCommand, 'Unknow Command');
- DoSendResult;
- end;
- end
- else
- begin
- DoFailure(CIPackFormatError, 'Packet Must Include \r\n\r\n');
- DoSendResult;
- end;
- except
- on E: Exception do //发生未知错误,断开连接
- begin
- sErr := RemoteAddress + ':' + IntToStr(RemotePort) + CSComma + 'Unknow Error: ' + E.Message;
- WriteLogMsg(ltError, sErr);
- Disconnect;
- end;
- end;
- end;
更详细代码见示例代码的IOCPSocket单元。
V1版下载地址:http://download.csdn.net/detail/sqldebug_fan/4510076,需要资源10分,有稳定性问题,可以作为研究稳定性用;
V2版下载地址:http://download.csdn.net/detail/sqldebug_fan/5560185,不需要资源分,解决了稳定性问题和提高性能;免责声明:此代码只是为了演示IOCP编程,仅用于学习和研究,切勿用于商业用途。水平有限,错误在所难免,欢迎指正和指导。邮箱地址:fansheng_hx@163.com。
http://blog.csdn.net/sqldebug_fan/article/details/7907765
DELPHI高性能大容量SOCKET并发(四):粘包、分包、解包的更多相关文章
- DELPHI高性能大容量SOCKET并发(八):断点续传(上传也可以续传)
断点续传 断点续传主要是用在上传或下载文件,一般做法是开始上传的时候,服务器返回上次已经上传的大小,如果上传完成,则返回-1:下载开始的时候,由客户端上报本地已经下载大小,服务器根据位置信息下发数据, ...
- DELPHI高性能大容量SOCKET并发(九):稳定性问题解决
http://blog.csdn.net/sqldebug_fan/article/details/9043699
- C#高性能大容量SOCKET并发(五):粘包、分包、解包
原文:C#高性能大容量SOCKET并发(五):粘包.分包.解包 粘包 使用TCP长连接就会引入粘包的问题,粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一 ...
- C#高性能大容量SOCKET并发(四):缓存设计
原文:C#高性能大容量SOCKET并发(四):缓存设计 在编写服务端大并发的应用程序,需要非常注意缓存设计,缓存的设计是一个折衷的结果,需要通过并发测试反复验证.有很多服务程序是在启动时申请足够的内存 ...
- C#高性能大容量SOCKET并发(六):超时Socket断开(守护线程)和心跳包
原文:C#高性能大容量SOCKET并发(六):超时Socket断开(守护线程)和心跳包 守护线程 在服务端版Socket编程需要处理长时间没有发送数据的Socket,需要在超时多长时间后断开连接,我们 ...
- C#高性能大容量SOCKET并发(转)
C#高性能大容量SOCKET并发(零):代码结构说明 C#高性能大容量SOCKET并发(一):IOCP完成端口例子介绍 C#高性能大容量SOCKET并发(二):SocketAsyncEventArgs ...
- C#高性能大容量SOCKET并发(十一):编写上传客户端
原文:C#高性能大容量SOCKET并发(十一):编写上传客户端 客户端封装整体框架 客户端编程基于阻塞同步模式,只有数据正常发送或接收才返回,如果发生错误则抛出异常,基于TcpClient进行封装,主 ...
- C#高性能大容量SOCKET并发(零):代码结构说明
原文:C#高性能大容量SOCKET并发(零):代码结构说明 C#版完成端口具有以下特点: 连接在线管理(提供在线连接维护,连接会话管理,数据接收,连接断开等相关事件跟踪): 发送数据智能合并(组件会根 ...
- C#高性能大容量SOCKET并发(九):断点续传
原文:C#高性能大容量SOCKET并发(九):断点续传 上传断点续传 断点续传主要是用在上传或下载文件,一般做法是开始上传的时候,服务器返回上次已经上传的大小,如果上传完成,则返回-1:下载开始的时候 ...
随机推荐
- 硬件——STM32 , SN74HC573锁存器
74HC573是一款高速CMOS器件: 上图中:输出使能为:OE 锁存使能为:LE 典型电路: 上图中:PWR-AL-0,PWR-AL-1,PWR-AL-2:是单片机输出的高低电平给573 对应的 ...
- 【hdu 6208】The Dominator of Strings
[链接]h在这里写链接 [题意] 问你n个串里面有没有一个串,使得其余n-1个串都是他的子串. [题解] 后缀数组. 答案肯定是那个最长的串. 则,把那个串求一下Sa数组(注意仅仅那个最长的串求). ...
- 10、bitmap格式分析
说到图片,位图(Bitmap)当然是最简单的,它Windows显示图片的基本格式,其文件扩展名为*.BMP.在Windows下,任何各式的图片文件(包括视频播放)都要转化为位图个时候才能显示出来,各种 ...
- EJBCA在Linux上的安装
在windows上安装为了測试用,装在linux服务器上的由于CN用的ip须要重装.....又是折腾一番,以下介绍一些须要注意的地方 一.所需文件 准备的内容就不说了,參考我的上上篇<EJBCA ...
- php实现反转链表(链表题一定记得画图)(指向链表节点的指针本质就是一个记录地址的变量)($p->next表示的是取p节点的next域里面的数值,next只是p的一个属性)
php实现反转链表(链表题一定记得画图)(指向链表节点的指针本质就是一个记录地址的变量)($p->next表示的是取p节点的next域里面的数值,next只是p的一个属性) 一.总结 链表反转两 ...
- ZOJ FatMouse' Trade 贪心
得之我幸,不得,我命.仅此而已. 学姐说呀,希望下次看到你的时候依然潇洒如故.(笑~) 我就是这么潇洒~哈哈. 感觉大家比我还紧张~ 我很好的.真的 ------------------------- ...
- (六)RabbitMQ消息队列-消息任务分发与消息ACK确认机制(PHP版)
原文:(六)RabbitMQ消息队列-消息任务分发与消息ACK确认机制(PHP版) 在前面一章介绍了在PHP中如何使用RabbitMQ,至此入门的的部分就完成了,我们内心中一定还有很多疑问:如果多个消 ...
- 【33.10%】【codeforces 604C】Alternative Thinking
time limit per test2 seconds memory limit per test256 megabytes inputstandard input outputstandard o ...
- 制作Kinect体感控制小车教程 <一>
转载请注明出处:http://blog.csdn.net/lxk7280 Kinect体感控制小车 Kine ...
- 特征点提取之Harris角点提取法
1. 特征点提取的意义 2.角点 3. Harris角点检測的基本原理 4.Harris角点检測算法的步骤 5.Harris角点提取算法设计 <span style="font-siz ...