最近太忙,所以沒有機會來寫IOCP的後續文章。今天好不容易有了時間來寫IOCP的粘包處理問題。
TCP數據粘包的產生原因在於TCP是一種流協議。在以太網中一個TCP的數據包長度是1500位。其中20位的IP包頭,20位的TCP包頭,其餘的1460都是我們可以發送的數據。在數據發送的時候,我們發送的數據長度有可能比1460短,這樣在TCP來說它還是以一個數據包來發送。從而降低了網絡的利用率。所以TCP在發送數據包的時候,會將下一個數據包和這個數據包合在一起發送以增加網絡利用率(雖然SOCKET 中可以強制關閉這種合併發送,但是我不建議使用)。這樣以來,在我們接受到一個數據包以後,就會發現在這個數據包中含有其它的數據包,從而很難處理。
處理粘包現象有多種方法。我的方法是在每發送一個數據的前面加入這次發送的數據長度(4位)。以char的方式加入。這樣以來我們的數據包結構就變成了:
數據包長度(4位)+實際數據。
在接收到數據包以後,我們首先得到數據包的長度,然後根據這個數據包長度來得到實際的數據。
以下是我的粘包處理函數實現(這個函數是對於多個套接字來處理的所以在這裏我使用了TList鏈表):
//用於處理粘包的數據結構
tagPacket = record
Socket:TSocket; //處理粘包的套接字
hThread:THANDLE; //線程句柄
ThreadID:DWORD; //線程ID
DataBuf:array[0..DATA_BUFSIZE-1] of char; //處理粘包的包
DataLen:Integer; //處理粘包的包長度
end;
TDealPacket = tagPacket;
PDealPacket = ^tagPacket;
{粘包處理函數}
function TClientNet.ComminutePacket(SorucePacket:array of char;SPLen:Integer;var Destpacket:array of char;
var DPLen:Integer;var SparePacket:array of char;
var SpareLen:Integer;var IsEnd:Boolean;socket:Tsocket):Boolean;
const
MaxPacket = 1024;
PacketLength = 4;
var
Temp:pchar;
TempLen,PacketHeader:Integer;
I,J:Integer;
TempArray:array[0..MaxPacket-1] of char;
TempCurr:Integer;
CurrListI:Integer;
SocketData:PDealPacket;
t_Ord:Integer;
begin
Result:=true;
try
//首先根據套接字來得到上次遺留的數據
Fillchar(TempArray,sizeof(TempArray),#0);
for I:=0 to DealDataList.Count-1 do
begin
SocketData:=DealDataList.Items[I];
if SocketData.Socket = socket then
begin
strmove(TempArray,SocketData.DataBuf,sizeof(SocketData.DataBuf));
TempCurr:=SocketData.DataLen;
CurrListI:=I;
break;
end;
end;
//我們將每次處理粘包以後剩餘的數據保存在一個TDealPacket的鏈表中DealDataList。每次根據套接字先得到上次是否有剩餘的數據。如果有則將這個數據拷貝到一個臨時處理的緩存中。
FillChar(Destpacket,sizeof(Destpacket),#0);
FillChar(SparePacket,sizeof(SparePacket),#0);
IsEnd:=false;
{以下就是對數據包的整合,其算法很簡單,讀者可以參考我的註釋來理解}
//對臨時緩存進行檢測
if TempCurr<>0 then //緩存中存在數據
begin
if TempCurr<PacketLength then //緩存中包含的數據包長度不足一個4位的數據包長度。
begin
TempLen:=PacketLength-TempCurr;
if TempLen>SPLen then //數據包中含有的數量不足包頭數量
begin
strmove(TempArray+TempCurr,SorucePacket,SPLen);
TempCurr:=TempCurr+SPLen;
//分解完畢,
IsEnd:=true;
end
else
begin
strmove(TempArray+TempCurr,SorucePacket,TempLen);
TempCurr:=TempCurr+TempLen;
GetMem(Temp,PacketLength+1);
Fillchar(Temp^,PacketLength+1,#0);
strmove(Temp,TempArray,PacketLength);
//最近在檢查代碼的時候發現這裏轉換包頭長度的時候,只是使用異常來判斷是不合適的。所以這裏進行了修改 (2008年3月24日)
{try
PacketHeader:=StrToInt(StrPas(Temp));
except
Result:=false;
exit;
end;
}
for J := 1 to 4 do
begin
t_Ord:=Ord(StrPas(Temp)[J]);
if (t_Ord<48) or (t_Ord>57) then
begin
Result := false;
IsEnd := true;
Exit;
end;
end;
if PacketHeader>SPLen-TempLen then //此包是不全包
begin
strmove(TempArray+TempCurr,SorucePacket+TempLen,SPLen-TempLen);
TempCurr:=TempCurr+SPLen-TempLen;
//已經將數據拷貝完成
IsEnd:=true;
end
else //此包是過包
begin
strmove(TempArray+TempCurr,SorucePacket+TempLen,PacketHeader);
strmove(Destpacket,TempArray,PacketHeader+PacketLength);
DPLen:=PacketHeader+PacketLength;
Strmove(SparePacket,SorucePacket+TempLen+PacketHeader,SPLen-(TempLen+PacketHeader));
SpareLen:=SPLen-(TempLen+PacketHeader);
FillChar(TempArray,sizeof(TempArray),#0);
TempCurr:=0;
IsEnd:=false;
end;
FreeMem(Temp);
end;
end
else //緩存中已經含有數據頭
begin
GetMem(Temp,PacketLength+1);
Fillchar(Temp^,PacketLength+1,#0);
strmove(Temp,TempArray,PacketLength);
//最近在檢查代碼的時候發現這裏轉換包頭長度的時候,只是使用異常來判斷是不合適的。所以這裏進行了修改 (2008年3月24日)
{try
PacketHeader:=StrToInt(StrPas(Temp));
except
Result:=false;
exit;
end;
}
for J := 1 to 4 do
begin
t_Ord:=Ord(StrPas(Temp)[J]);
if (t_Ord<48) or (t_Ord>57) then
begin
Result := false;
IsEnd := true;
Exit;
end;
end;
if PacketHeader>TempCurr-PacketLength then //數據包包頭
begin
TempLen:=(PacketHeader+PacketLength)-TempCurr;
if TempLen>SPLen then
begin
strmove(TempArray+TempCurr,SorucePacket,SPLen);
TempCurr:=TempCurr+SPLen;
IsEnd:=true;
end
else
begin
strmove(TempArray+TempCurr,SorucePacket,TempLen);
strmove(Destpacket,TempArray,PacketHeader+PacketLength);
DPLen:=PacketHeader+PacketLength;
Strmove(SparePacket,SorucePacket+TempLen,SPLen-TempLen);
SpareLen:=SPLen-TempLen;
TempCurr:=0;
FillChar(TempArray,sizeof(TempArray),#0);
IsEnd:=false;
end;
end
else
begin
strmove(TempArray+TempCurr,SorucePacket,TempLen+PacketLength);
strmove(Destpacket,TempArray,TempCurr+TempLen+PacketLength);
DPLen:=TempCurr+TempLen+PacketLength;
Strmove(SparePacket,SorucePacket+TempLen+PacketLength,SPLen-TempLen);
SpareLen:=SPLen-TempLen-PacketLength;
TempCurr:=0;
FillChar(TempArray,sizeof(TempArray),#0);
IsEnd:=false;
end;
FreeMem(Temp);
end;
end
else //緩存中不存在數據
begin
Fillchar(TempArray,sizeof(TempArray),#0);
if SPLen>=PacketLength then
begin
strmove(TempArray,SorucePacket,PacketLength);
GetMem(Temp,PacketLength+1);
Fillchar(Temp^,PacketLength+1,#0);
strmove(Temp,TempArray,PacketLength);
//最近在檢查代碼的時候發現這裏轉換包頭長度的時候,只是使用異常來判斷是不合適的。所以這裏進行了修改 (2008年3月24日)
{try
PacketHeader:=StrToInt(StrPas(Temp));
except
Result:=false;
exit;
end;}
for J := 1 to 4 do
begin
t_Ord:=Ord(StrPas(Temp)[J]);
if (t_Ord<48) or (t_Ord>57) then
begin
Result := false;
IsEnd := true;
Exit;
end;
end;
if PacketHeader>SPLen-PacketLength then
begin
strmove(TempArray+PacketLength,SorucePacket+PacketLength,SPLen-PacketLength);
TempCurr:=SPLen;
IsEnd:=true;
end
else
begin
strmove(TempArray+PacketLength,SorucePacket+PacketLength,PacketHeader);
strmove(Destpacket,TempArray,PacketHeader+PacketLength);
DPLen:=PacketHeader+PacketLength;
Strmove(SparePacket,SorucePacket+PacketHeader+PacketLength,SPLen-(PacketHeader+PacketLength));
SpareLen:=SPLen-(PacketHeader+PacketLength);
TempCurr:=0;
FillChar(TempArray,sizeof(TempArray),#0);
IsEnd:=false;
end;
FreeMem(Temp);
end
else
begin
strmove(TempArray,SorucePacket,SPLen);
TempCurr:=SPLen;
IsEnd:=true;
end;
end;
//恢復數據
SocketData.DataLen:=TempCurr;
Fillchar(SocketData.DataBuf,sizeof(SocketData.DataBuf),#0);
strmove(SocketData.DataBuf,TempArray,TempCurr);
except
Result:=false;
end;
end;
上面的函數就是對TCP協議中粘包的處理DLEPHI代碼,對於UDP數據來說是不存在粘包現象的。
我寫的IOCP的代碼已經在我編寫的網絡遊戲中使用,運行穩定。
下次我會講使用IOCP發送數據的方法。