浅析Delphi实现IOCP后的优化
推荐 原创
©著作权归作者所有:来自51CTO博客作者fxh7622的原创作品,请联系作者获取转载授权,否则将追究法律责任
在我的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的时候所感受到的,希望写出来和大家一起探讨一下。
下一篇:一个自己整理的HASH类
提问和评论都可以,用心的回复会被更多人看到
评论
发布评论
相关文章
-
Delphi-IOCP学习笔记<一>====从零开始学习IOCP
之前一直接从事企业管理的软件,接触网络比较少。网络方面的知识也很惨。最近想弄下iocp。网上找了很多资料,大部分是关于c++的。delphi的比较少。看了下,不适合刚接触网络的菜鸟学习!看过fxh的blog,觉得写的容易理解。http://fxh7622.blog.51cto.com/63841/7667基本上按照他的blog代码进行的编写。编译理解iocp的原理。首先我来个通俗的理解步骤。1.服务器创建一个iocp端口服务。2.服务器创建N个工作线程,不停的轮流处理这个端口上面的连接和请求。3.开启iocp的端口服务<绑定监听端口,进行监听>。如果接收到请求,通知iocp工作线程
delphi iocp iocp原理 iocp流程 Delphi-IOCP