淺析Delphi實現IOCP後的優化

 

在我的BLOG中有幾篇文章是關於如何用DLEPHI來實現IOCP,詳見我的BLOGDELPHI中完成端口(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的時候所感受到的,希望寫出來和大家一起探討一下。

 

                                                                                     
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章