delphi 下一种对socket异步模式接收数据分包与粘包的处理方式原理及其代码
前言:
网络上传输的数据总是一份一份的,每一份数据称为一个完整的数据包。数据包由包头和包体(数据)构成。包头描述数据的性质,大小等相关属性。当我们用socket的recv()异步模式接收数据时,由于网络的差异性,复杂性,第一次的recv()接收往往不是一个完整的数据包。有时候只接收到包头,下一个recv()只接收到包体:这种只接收到数据包一部分的现象称为分包现象。还有一种情况是一次recv()接收到多于一个数据包:这种同时接收到多个数据包的现象称为粘包现象。
为了处理异步模式下的分包与粘包现象,需要使用组包技术。
今天探讨的组包技术在项目实践中运行良好,毫无Bug。需要的朋友请自行COPY。
原理:
一、使用递归调用组包。组包过程如下:
一次recv()时,不是先接收到包头,就是先接收到包体。当第一次先接收到包头时,对包头进行校验,校验通过后将包头复制到组包缓冲区mDataPackageArr[mCount],同时置接收标志mMergeStateFlag为:fCopyHeaderComplete,置新数据大小(dwNewDataSize)等于接收到的数据大小(dwDataSize)减去包头大小(dwHeaderSize)。
如果新数据大小(dwNewDataSize)=0说明本次只接收到数据头,退出递归函数。
如果新数据大小(dwNewDataSize)>0说明本次还接收到包体,置新数据指针为:pNewdata:=pointer(DWORD(pData)+dwNewDataSize);。
然后将新数据大小和指针带入递归函数调用。当进入递归函数时,判断接收标志mMergeStateFlag,如果是fCopyHeaderComplete,说明本次入口数据为包体。复制包体到组包缓冲区mDataPackageArr[mCount],同时置:mMergeStateFlag:=fCopyDataComplete;再次递减dwDataSize,如果为零,退出递归函数。如果大于零,则再次递归。
当进入递归函数时,判断接收标志mMergeStateFlag,如果是fCopyDataComplete,说明本次入口数据为包头。复制包头到组包缓冲区mDataPackageArr[mCount],同时置:mMergeStateFlag:=fCopyHeaderComplete;再次递减dwDataSize,如果为零,退出递归函数。如果大于零,则再次递归。递归函数如下:
procedure TDataPackageParser.copyData(pData:pointer;dataSize: DWORD);
var
dataSize2:DWORD;
pdata2:pointer;
begin
if(mMergeStateFlag=fCopyNone)or(mMergeStateFlag=fCopyDataComplete)then
begin //fCopyNone为第一次数据到来;fCopyDataComplete为包体已复制完;
if(dataSize<mHeaderSize)then exit;
if not VerifyPackageHeader(PPackageHeader(pData)) then exit;//校验数据头;
copymemory(@mDataPackageArr[mCount].header,pData,mHeaderSize);//复制数据头;
mMergeStateFlag:=fCopyHeaderComplete; //置组合标志为:fCopyHeaderComplete
dataSize2:=dataSize-mHeaderSize; //新数据大小;
if(dataSize2=0)then exit; //本次数据已复制完;
pdata2:=pointer(DWORD(pData)+mHeaderSize); //新数据指针;
copyData(pData2,dataSize2); //递归组合数据包;
end
else if (mMergeStateFlag=fCopyHeaderComplete) then //本次调用需要复制包体;
begin
dataSize2:=mDataPackageArr[mCount].header.dwSize-mHeaderSize; //取数据包大小 ;
if(dataSize<dataSize2)then //数据未接收完全;因为接收的包体为小数据量,不大于2K,所以可以直接退出;
begin //如果包体数据量巨大,这儿需要修改;
Log('MergePackage:fCopyDataHalf');
mMergeStateFlag:=fCopyNone;
exit;
end;
copymemory(@mDataPackageArr[mCount].data[0],pdata,dataSize2); //复制包体到缓冲区;
mMergeStateFlag:=fCopyDataComplete; //置组合标志为:fCopyDataComplete
pdata2:=pointer(DWORD(pData)+dataSize2); //新数据指针;
dataSize2:=dataSize-dataSize2; //新数据大小;
mCount:=mCount+1; //置缓冲区数据包数量加1;
if(mCount>=PACKAGE_LINK_MAX_COUNT)then mCount:=0;
if(dataSize2=0)then exit; //数据已复制完则退出;
if(dataSize2-mHeaderSize<0)then //这种情况发生的概率极小,可以直接丢包;
begin
Log('MergePackage:fCopyHeadHalf');
exit;
end;
copyData(pdata2,dataSize2); //递归组包;
end;
end;
二、使用另一个线程解析包。解析包过程主要如下:
解析包在另一个子线程里,子线程监测到组包缓冲区有数据时,开始取包(一个完整的数据包)解析。具体实现代码为://取包子线程:
procedure TDataPackageParser.Execute;
var
s:TSocket;
begin
while bProcess do //循环监测
begin
Log('mProcess<mCount:'+inttostr(mProcess)+' '+inttostr(mCount)); //日志;
if(mProcess<mCount)then//关键代码:mProcess为已解析包的数量;mCount为当前缓冲区包的数量;
begin
Parsepackage(mProcess); //解析包;
postMessage(mhform, WM_PACKAGE_PARSER,cardinal(mDataFlag),0); //发送解析完通知;
mProcess:=mProcess+1; //解析数量+1;
end else
begin
if(mMergeWorkingFlag<>fBuzy)and(mCount>0)and(mMergeStateFlag<>fCopyHeaderComplete)then
begin //关键代码:当处理完所有数据包并且组合包函数空闲时,将缓冲区清零。
mCount:=0;
mProcess:=0;
end;
sleep(1000); //缓冲区无数据包时,睡眠一秒。
end;
end;
end;
整个组包与解析包已封装为线程类:TDataPackageParser,类的框架结构如下:
unit uDataPakageParser;
interface
uses
windows,classes,winsock2,sysutils,strutilsmessages;
const
WM_PACKAGE_PARSER = WM_USER+1004; //数据包解析消息
PACKAGE_DATA_MAX_SIZE=2048; //定义包内数据大小 ;
PACKAGE_LINK_MAX_COUNT=16; //定义粘包最大数量 ;
type
TMergeWorkingFlag=(fbuzy,fIdle); //组合包处理标志:忙,空闲
TMergeStateFlag=(fCopyNone,fCopyHeaderComplete,fCopyDataComplete,fCopyDataHalf); //组合包处理标志:无复制,复制了包头,复制了数据体,复制了一半数据体;
//-------------------------------------------------------------------------------------------
stSocketHeader=record //原始socket数据包信息;
id:DWORD; //编号
s:TSocket; //保存旧连接信息
dwDirection:DWORD; //数据方向:接收0,发送1;
localPort:WORD;
remotePort:WORD;
localAddr:array[0..15] of ansichar;
remoteAddr:array[0..15] of ansichar;
dwDataSize:DWORD;
end;
//--------------------------------------业务逻辑 包头----------------------------------------
PPackageHeader=^stPackageHeader;
stPackageHeader=packed record
dwSize:DWORD;
header:array[0..11] of byte;
ordHigh:byte;
ordLow:byte;
dwDataSize:DWORD;
end;
stDataPackage=record //一个完整的包
header:stPackageHeader;
data:array[0..PACKAGE_DATA_MAX_SIZE-1] of byte;
end;
//-------------------------------------------------------------------------------------------
TDataPackageParser = class(TThread) //包解析类
private
bProcess:BOOLEAN; //线程运行控制
mCount,mProcess:integer; //数据包的数量;处理的数量
mDataPackageArr:array[0..PACKAGE_LINK_MAX_COUNT-1] of stDataPackage; //组包缓冲区;
mHeaderSize:DWORD; //包头大小 ;
mMergeWorkingFlag:TMergeWorkingFlag; //组合包处理结果;
mMergeStateFlag:TMergeStateFlag; //组合包状态;
//-------------------------------------属性--------------------------------------------
mhForm:HWND; //消息发送到的窗体;
function convertInt(i:integer):integer; //网络int类型数据纠正;
function VerifyPackageHeader(pHeader:PPackageHeader):boolean; //数据包校验;
procedure copyData(pData:pointer;dataSize: DWORD); //递归组包函数;
procedure ParsePackage(i:integer); //解析数据包;i为缓冲区数据包位置;
//--------------------------------------------------------------------------------------
procedure SetFormHandle(hForm:HWND);
protected
procedure Execute; override;
public
constructor Create(hForm:HWND); overload;
destructor Destroy;override;
//socketHeader为原始socket数据包信息
procedure MergePackage(socketHeader:stSocketHeader;pData:pointer); //组合包
property hForm:HWND read mhForm write SetFormHandle; //
end;
implementation
constructor TDataPackageParser.Create(hForm:HWND);
begin
inherited Create(false);
bProcess:=true; //程序开始时启动线程,直到程序结束;
mhForm:=hForm;
mHeaderSize:=sizeof(stPackageHeader); //包头大小
end;
destructor TDataPackageParser.Destroy;
begin
bProcess:=false;
end;
procedure TDataPackageParser.Execute;
var
s:TSocket;
begin
while bProcess do //循环监测
begin
Log('mProcess<mCount:'+inttostr(mProcess)+' '+inttostr(mCount)); //日志;
if(mProcess<mCount)then//关键代码:mProcess为已解析包的数量;mCount为当前缓冲区包的数量;
begin
Parsepackage(mProcess); //解析包;
postMessage(mhform, WM_PACKAGE_PARSER,cardinal(mDataFlag),0); //发送解析完通知;
mProcess:=mProcess+1; //解析数量+1;
end else
begin
if(mMergeWorkingFlag<>fBuzy)and(mCount>0)and(mMergeStateFlag<>fCopyHeaderComplete)then
begin //关键代码:当处理完所有数据包并且组合包函数空闲时,将缓冲区清零。
mCount:=0;
mProcess:=0;
end;
sleep(1000); //缓冲区无数据包时,睡眠一秒。
end;
end;
end;
procedure TDataPackageParser.MergePackage(socketHeader:stSocketHeader;pData:pointer); //组合包
begin
mMergeWorkingFlag:=fbuzy; //置组合包线程为忙;
copyData(pData,socketHeader.dwDataSize); //递归组合包;
mMergeWorkingFlag:=fIdle; //置组合包线程为空闲;
end;
//递归组合包过程:
procedure TDataPackageParser.copyData(pData:pointer;dataSize: DWORD);
var
dataSize2:DWORD;
pdata2:pointer;
begin
if(mMergeStateFlag=fCopyNone)or(mMergeStateFlag=fCopyDataComplete)then
begin //fCopyNone为第一次数据到来;fCopyDataComplete为包体已复制完;
if(dataSize<mHeaderSize)then exit;
if not VerifyPackageHeader(PPackageHeader(pData)) then exit;//校验数据头;
copymemory(@mDataPackageArr[mCount].header,pData,mHeaderSize);//复制数据头;
mMergeStateFlag:=fCopyHeaderComplete; //置组合标志为:fCopyHeaderComplete
dataSize2:=dataSize-mHeaderSize; //新数据大小;
if(dataSize2=0)then exit; //本次数据已复制完;
pdata2:=pointer(DWORD(pData)+mHeaderSize); //新数据指针;
copyData(pData2,dataSize2); //递归组合数据包;
end
else if (mMergeStateFlag=fCopyHeaderComplete) then //本次调用需要复制包体;
begin
dataSize2:=mDataPackageArr[mCount].header.dwSize-mHeaderSize; //取数据包大小 ;
if(dataSize<dataSize2)then //数据未接收完全;因为接收的包体为小数据量,不大于2K,所以可以直接退出;
begin //如果包体数据量巨大,这儿需要修改;
Log('MergePackage:fCopyDataHalf');
mMergeStateFlag:=fCopyNone;
exit;
end;
copymemory(@mDataPackageArr[mCount].data[0],pdata,dataSize2); //复制包体到缓冲区;
mMergeStateFlag:=fCopyDataComplete; //置组合标志为:fCopyDataComplete
pdata2:=pointer(DWORD(pData)+dataSize2); //新数据指针;
dataSize2:=dataSize-dataSize2; //新数据大小;
mCount:=mCount+1; //置缓冲区数据包数量加1;
if(mCount>=PACKAGE_LINK_MAX_COUNT)then mCount:=0;
if(dataSize2=0)then exit; //数据已复制完则退出;
if(dataSize2-mHeaderSize<0)then //这种情况发生的概率极小,可以直接丢包;
begin
Log('MergePackage:fCopyHeadHalf');
exit;
end;
copyData(pdata2,dataSize2); //递归组包;
end;
end;
//此校验函数请根据具体业务逻辑及包头修改:
function TDataPackageParser.VerifyPackageHeader(pHeader:PPackageHeader):boolean; //数据包校验;
var
dwSize,dwDataSize:DWORD;
begin
result:=false;
dwSize:=pHeader^.dwSize;
dwDataSize:=pHeader^.dwDataSize;
dwSize:=convertInt(dwSize);
dwDataSize:=convertInt(dwDataSize);
if(dwSize>PACKAGE_DATA_MAX_SIZE)then
begin
uLog.Log('VerifyPackageHeader false:dwSize='+inttostr(dwSize));
exit;
end;
if(dwDataSize>PACKAGE_DATA_MAX_SIZE)then
begin
uLog.Log('VerifyPackageHeader false:dwDataSize='+inttostr(dwDataSize));
exit;
end;
if(pHeader^.ordHigh>10)then
begin
uLog.Log('VerifyPackageHeader false:ordHigh='+inttostr(pHeader^.ordHigh));
exit;
end;
if(pHeader^.ordLow>10)then
begin
uLog.Log('VerifyPackageHeader false:ordLow='+inttostr(pHeader^.ordLow));
exit;
end;
if(pHeader^.header[0]<>$ff)then
begin
uLog.Log('VerifyPackageHeader false:ff='+inttostr(pHeader^.header[0]));
exit;
end;
pHeader^.dwSize:=dwSize;
pHeader^.dwDataSize:=dwDataSize;
result:=true;
end;
procedure TDataPackageParser.ParsePackage(i:integer);
var
pHeader:PPackageHeader;
//cryptedData,jsonData:ansiString;
pData:pointer;
dataSize:DWORD;
pOut:PoutData;
begin
try
pHeader:=PPackageHeader(@mDataPackageArr[i].header);
pData:=pointer(@mDataPackageArr[i].data[0]);
mDataFlag:=fNone;
//.....................
//这儿是具体业务逻辑过程;
//.....................
except
//分别保存错误包头及包体,以待分析; uFuncs.saveTofile(getFileName(uConfig.datadir,inttostr(i)+'packageParseErr','.txt'),pHeader,mHeaderSize);
uFuncs.saveTofile(getFileName(uConfig.datadir,inttostr(i)+'packageParseErr','.txt'),pData,pHeader^.dwDataSize);
end;
end;
//-----------------------------------------内部功能---------------------------------------------
//整形数据字节转换:
function TDataPackageParser.convertInt(i:integer):integer;
var
b1,b2:array[0..3] of byte;
begin
move(i,b1,4);
b2[0]:=b1[3];
b2[1]:=b1[2];
b2[2]:=b1[1];
b2[3]:=b1[0];
move(b2,result,4);
end;
//-----------------------------------------属性---------------------------------------------
procedure TDataPackageParser.SetFormHandle(hForm:HWND);
begin
mHForm:=hForm;
end;
initialization
finalization
end.