在我的
BLOG
中有几篇文章是关于如何用
DLEPHI
来实现
IOCP
,详见我的
BLOG
《
DELPHI
中完成端口
(IOCP)
的简单分析
》。在这几篇文章中介绍了如何编写一个简单的
IOCP
的方法。
最近我重新对这些文章中的一些
BUG
和效率低下的部分做了修正(其实相当于重新编写),通过几个不同的途径对
IOCP
进行了实现。下面我就来说一下我对以前代码的优化方法。
1
:结构定义部分。
首先我们必须定义一个
IO
数据结构,在我的
BLOG
中我当时是这样定义的。
(
1
):单
IO
数据结构
LPVOID = Pointer;
LPPER_IO_OPERATION_DATA = ^ PER_IO_OPERATION_DATA ;
PER_IO_OPERATION_DATA = packed record
Overlapped: OVERLAPPED;
DataBuf: TWSABUF;
Buffer: array [0..1024] of CHAR;
BytesSEND: DWORD;
BytesRECV: DWORD;
end;
和一个
(
2
):“单句柄数据结构”
LPPER_HANDLE_DATA = ^ PER_HANDLE_DATA;
PER_HANDLE_DATA = packed record
Socket: TSocket;
end;
其实为什么我们不能将他们进行合并定义成一个结构呢?
//IO
结构
PIOData = ^TIOData;
TIOData = record
Overlapped: OVERLAPPED;
DataBuf: TWSABUF;
Socket:TSocket; //
套接字
OperationType:TOperation; //
操作类型
BufferLen:Integer; //
数据长度
Buffer:array[0..DATA_BUFSIZE-1] of char; //
数据信息,包括数据头信息
end;
这种结构当我们调用
GetQueuedCompletionStatus
函数的时候,用该函数的第
4
个参数来返回这个结构,这样一来我们就不用定义
2
个结构来处理
不知道大家是否还记得在我的
BLOG
中关于粘包的文章(我们暂且不说它是否应该叫这个名字)。关于粘包的造成原理我这里就不讲述了,如有需要可以参看我的
BLOG
。这里只是说明一下,粘包的处理是我们将通过
IOCP
得到的数据,和这个套接字上次处理并剩余的数据合并在一起,看新合并后的数据包中是否包含一个完整的数据结构,如果包含则进行相关处理,并将处理后的剩余数据进行再次判断,反复如此。一般我们会将这个合并的数据放在一个
TList
链表中,有的时候为了加快它的查找速度,我们会将它放在一个
HASH
表中,以套接字做为
KEY
。自然放在
HASH
表中的速度要比放在单纯的链表中快一些。可是我们有没有想过直接放在上面的这个
IO
结构中呢?也就是说将粘包处理的数组放在
IO
结构中,这样当
GetQueuedCompletionStatus
返回的时候就会直接将数据进行粘包处理,又可以免去一次的数据查找过程。这样一来,上面的数据结构就变成了:
//IO
结构
PIOData = ^TIOData;
TIOData = record
Overlapped: OVERLAPPED;
DataBuf: TWSABUF;
Socket:TSocket; //
套接字
OperationType:TOperation; //
操作类型
BufferLen:Integer; //
数据长度
Buffer:array[0..DATA_BUFSIZE-1] of char; //
数据信息,包括数据头信息
SpareBuffer:array[0..2*DATA_BUFSIZE - 1 ] of char; //
处理粘包数组
SpareBufferlen:Integer; //
粘包数组中剩余的数据长度
end;
这样做从理论上来说
IOCP
的速度会提高不少。但是由于我们指定了粘包处理的数组大小,这样就会出现――当我们发送过来的数据结构的长度大于粘包数数组长度的时,粘包处理就会出现问题。这个时候我觉得处理方法有两个:
1
:加大粘包数组长度,这个数组的长度设置成你的所有数据结构中最大者长度的
2
倍。
2
:使用链表来进行粘包处理,我们可以将粘包设置成一个链表,这样就避免了粘包数组长度的限制,我们可以发送一个很大的数据结构。但是这样的设置又会带来新的问题,即每次需要申请新的内存。不过这也算是一种方法,适合于数据包大小变化很大的情况。具体结构为:
PSpareBuffer = ^TSpareBuffer;
TSpareBuffer = record
Postion:Integer;
SpareBuffer:array[0..DATA_BUFSIZE-1] of char;
Next:PSpareBuffer;
end;
//IO
结构
PIOData = ^TIOData;
TIOData = record
Overlapped: OVERLAPPED;
DataBuf: TWSABUF;
Socket:TSocket; //
套接字
OperationType:TOperation; //
操作类型
BufferLen:Integer; //
数据长度
Buffer:array[0..DATA_BUFSIZE-1] of char; //
数据信息,包括数据头信息
FirstBuffer:PSpareBuffer; //
粘包处理链表的第一个指针
LastBuffer:PSpareBuffer; //
粘包处理链表的最后一个指针
end;
具体的实现方法,我这里就不讲述了,大家有时间可以自行实现。
还有可以提高效率的地方吗?我们来看看以前对于粘包的处理,以前我在对粘包的处理部分使用的是将一个代表数据包长度的
Integer
类型,转换成一个
4
位的
char
并加入到数据包的头部,然后根据这个包头长度来处理,所以代码中就出现了不少这样的代码:
PacketHeader:=StrToInt(StrPas(Temp));
这个可以优化吗?当然可以,将我们发送的数据格式修改成这样:
TNetPacketed = record
DataLen:Integer;
end;
PNetPacketed = ^TNetPacketed;
procedure TIOCPServer.DataProcess(PerIoData:PRecvIOData);
var
Offset:Word;
pPacketed:PNetPacketed;
Data:PChar;
begin
Offset:=0;
while PerIoData.SpareBufferLen - Offset >= SizeOf(TNetPacketed) do
begin
pPacketed:=PNetPacketed(@PerIoData.SpareBuffer[Offset]);
if (PerIoData.SpareBufferLen - Offset) >= (pPacketed.DataLen) then
begin
GetMem(Data,pPacketed.DataLen - SizeOf(TNetPacketed));
StrMove(Data,@PerIoData.SpareBuffer[Offset+SizeOf(TNetPacketed)],pPacketed.DataLen - SizeOf(TNetPacketed));
if Assigned(OnRecive) then
begin
OnRecive(Data,pPacketed.DataLen - SizeOf(TNetPacketed),PerIoData.Socket);
end;
FreeMem(Data);
Inc(Offset,pPacketed.DataLen);
end
else
begin
Break;
end;
end;
if (Offset>0)then
begin
Dec(PerIoData.SpareBufferLen,Offset);
Move(PerIoData.SpareBuffer[Offset],PerIoData.SpareBuffer,PerIoData.SpareBufferLen);
end;
end;
本来在这里想写一些比较详细的文字来说明粘包的处理过程,可是后来觉得使用代码应该更能说明问题。所以就将我现用的代码贴了出来,我想大家看到代码就应该明白我的意思了呵呵。
2
:使用内存池。
使用内存池来提高
IOCP
的效率,这几乎是大家的共识。可是如何使用内存池呢?有人喜欢使用环形内存池,有人喜欢用链表内存池,也有人直接使用
FASTMM
来做内存池的。我觉得这些方法都可以达到一定的目的,我使用内存池的类是这样的:
//
发送内存池管理类
TMemPoolControl = class
private
{ Private declarations }
FMemFirst:PIOData;
FMemCS:TRTLCriticalSection;
procedure CreateMem;
procedure AddMem(p_IOData:PIOData);
public
{ Public declarations }
FMemCount:Integer;
constructor Create;
destructor Destroy; override;
//
申请一个发送空间
procedure AllocateBuffer(var p_IOData: PIOData);
procedure ReleaseBuffer(p_IOData: PIOData);
end;
{ TMemPoolControl }
procedure TMemPoolControl.AddMem(p_IOData: PIOData);
var
p_MoveIOData,p_OldIOData:PIOData;
begin
if FMemCount > MAXPOOLNUMS then
begin
//
内存池数据太多,直接释放
HeapFree(GetProcessHeap, 0, p_IOData);
Exit;
end;
//
初始化此内存块
FillChar(p_IOData.Buffer,SizeOf(p_IOData.Buffer),#0);
p_IOData.BufferLen:=0;
p_IOData.Next:=nil;
p_OldIOData:=nil;
p_MoveIOData:=FMemFirst;
if not Assigned(FMemFirst) then
begin
FMemFirst:=p_IOData;
end
else
begin
//
循环查找最后一个内存指针
while Assigned(p_MoveIOData) do
begin
p_OldIOData:=p_MoveIOData;
p_MoveIOData:=p_MoveIOData.Next;
end;
p_OldIOData.Next:=p_IOData;
end;
Inc(FMemCount);
end;
procedure TMemPoolControl.AllocateBuffer(var p_IOData: PIOData);
begin
EnterCriticalSection(FMemCS);
try
if Assigned(FMemFirst) then
begin
p_IOData:=FMemFirst;
FMemFirst:=FMemFirst.Next;
p_IOData.Next:=nil;
Dec(FMemCount);
end
else
begin
CreateMem;
p_IOData:=FMemFirst;
FMemFirst:=FMemFirst.Next;
p_IOData.Next:=nil;
Dec(FMemCount);
end;
finally
LeaveCriticalSection(FMemCS);
end;
end;
constructor TMemPoolControl.Create;
begin
FMemFirst:=nil;
FMemCount:=0;
InitializeCriticalSection(FMemCS);
end;
procedure TMemPoolControl.CreateMem;
var
I:Integer;
Buf:PIOData;
begin
for I:=1 to (MAXPOOLNUMS - FMemCount) do
begin
Buf:=PIOData(HeapAlloc(GetProcessHeap,HEAP_ZERO_MEMORY,sizeof(TIOData)));
Buf.Next:=nil;
if Assigned(Buf) then
begin
AddMem(Buf);
end;
end;
end;
destructor TMemPoolControl.Destroy;
var
p_IOData:PIOData;
begin
EnterCriticalSection(FMemCS);
try
//
清空接收缓冲池
while Assigned(FMemFirst) do
begin
p_IOData:=FMemFirst;
FMemFirst:=FMemFirst.Next;
p_IOData.Next:=nil;
HeapFree(GetProcessHeap, 0, p_IOData);
end;
FMemCount:=0;
FMemFirst:=nil;
finally
LeaveCriticalSection(FMemCS);
DeleteCriticalSection(FMemCS);
end;
inherited;
end;
procedure TMemPoolControl.ReleaseBuffer(p_IOData: PIOData);
begin
EnterCriticalSection(FMemCS);
try
if Assigned(p_IOData) then
begin
p_IOData.BufferLen:=0;
FillChar(p_IOData.Buffer,SizeOf(p_IOData.Buffer),#0);
p_IOData.Next:=nil;
AddMem(p_IOData);
end;
finally
LeaveCriticalSection(FMemCS);
end;
end;
这个代码中有一个比较慢的地方,我只定义了这个内存池的头指针,而没有定义尾指针。所以在加入一个新内存的时候就要从头查找一遍,降低了效率。大家可以在这里定义一个尾指针用于加快插入速度。
3
:连接池。
连接池的时候主要是使用
ACCEPTEX
函数来代替
WSAAccept
函数。这个函数最大的好处是可以实现创建出多个套接字。但是在我实际使用中却发现,它有几个不好的地方
(
1
):控制麻烦:我相信使用过
ACCEPTEX
的朋友应该会同意我的观点。较之
WSAAccept
函数来说,
ACCEPTEX
函数使用起来繁琐很多。首先要将此函数引入,然后预先创建多个套接字并将这些套节字都投递
accept
请求,并将这些套接字放在一个链表中。投递请求后,设置
2
个事件放在事件数组中,这时创建工作者线程,并将工作者线程的句柄保存在事件数组中,然后使用
WSAWaitForMultipleEvents
函数
WSAWaitForMultipleEvents( FNetServer.EventSerial + 1, @FNetServer.Eventarray[0], FALSE, 1000, False );
来等待相应的事件出发,对于超时事件我们需要对链表中的套接字进行检测是否超时,对于
…….
说着我就头大。实现的代码,和我写的其它版本的
IOCP
对比了一下,复杂程度和可控制程度麻烦了许多,代码越多出错几率就会越大,所以我不建议大家使用
ACCEPTEX
。
(
2
):连接判断:对于使用
ACCEPTEX
函数最大的问题,我觉得是它无法做到对于连接请求是否允许连接进行的判断。我们知道在
WSAAccept
函数中有个参数
LPCONDITIONPROC lpfnCondition,
这个参数是一个回调函数,此函数用于判断此次连接是否被允许。如果这个函数返回
CF_ACCEPT
表示允许连接,如果返回
CF_REJECT
则表示不可以连接。而在
ACCEPTEX
函数中我们却看不到这种功能的存在(兴许有,但是我没有找到)。
综上所述,我不建议大家使用
ACCEPTEX
。当然如果有人使用的话如果有什么问题,可以在我的
BLOG
中留言我会尽量帮助大家。
4
:多次投递。
在《
windows
网络与通信程序设计》一书中,关于
IOCP
的注意事项讲述的“包重新排序问题”中有提到。结论是“这个问题可以通过仅使用一个工作线程,仅提交一个
I/O
调用,然后等待它完成来避免。但是这样就丧失了
IOCP
的所有优点。”在我写的
IOCP
的各版本中,有使用多次投递的方式来实现的,也有使用单次投递方式来实现的。不过我的测试发现他们之间的效果相差不大。这也可能是我的实现或者测试方法的问题,希望大家有机会可以用这种方法也做一下测试,看看两者之间的差距。多次投递的理论在于我们一次性投递多次的
WSARECV
。正如我们一次性给系统
5
个空篮子一样,如果有数据到来的时候,系统会根据我们投递
WSARECV
的顺序给不同篮子以数据(这里注意的是,系统给每个篮子数据的时候,不是随机给的,而是依据你投递的顺序给的,例如你第一个给的是
A
篮子,第二个给的是
B
篮子,那么系统一定会将第一个数据放在
A
篮子中)。但是由于
IOCP
使用的是多线程来处理的,那么在我们得到的时候有可能是先得到
B
篮子,这个时候就需要进行重新排序的过程。具体的方法可以类似书中讲述的方法。
以上
4
点是我编写
IOCP
的时候所感受到的,希望写出来和大家一起探讨一下。