[置顶] DELPHI高性能大容量SOCKET并发(二):IOCP完成端口控件封装

IOCP完成端口介绍:

完成端口模型是Windows平台下SOCKET端口模型最为复杂的一种I/O模型。如果一个应用程序需要同时管理为数众多的套接字,而且希望随着系统内安装的CPU数量的增多,应用程序的性能也可以线性提升,采用完成端口模型,往往可以达到最佳的系统性能。

完成端口可以管理成千上万的连接,长连接传文件可以支持5000个以上,长连接命令交互可以支持20000个以上。这么大并发的连接,更需要考虑的是应用场景,按照100M的网卡传输速度12.5MB/S,如果是5000个传文件连接,则每个连接能分到的速度2.56KB/S;如果是20000个命令交互连接,则每个连接分到的吞吐量是655B/S,这种速度的吞吐量对很多应用是不满足,这时就要考虑加大网卡的传输速度或实现水平扩展,这个我们后续会介绍。

完成端口是由系统内核管理多个线程之间的切换,比外部实现线程池性能要高,CPU利用率上内核和用户态可以达到1:1,很多应用线程池是无法达到的。因此同等连接数的情况下,完成端口要比INDY的TCPServer传输速度要快,吞吐量更高。

要使用完成端口,主要是以下三个函数的使用:CreateIoCompletionPort、GetQueuedCompletionStatus、PostQueuedCompletionStatus。

CreateIoCompletionPort的功能是:1、创建一个完成端口对象;2、将一个句柄和完成端口关联在一起;GetQueuedCompletionStatus是获取完成端口状态,是阻塞式调用,在指定时间内如果没有事件通知,会一直等待;PostQueuedCompletionStatus用于向完成端口投递一个完成事件通知。

function CreateIoCompletionPort(FileHandle, ExistingCompletionPort: THandle; CompletionKey, NumberOfConcurrentThreads: DWORD): THandle; stdcall;NumberOfConcurrentThreads参数定义了在一个完成端口上,同时允许执行的线程数量。将NumberOfConcurrentThreads设为0表示每个处理器各自负责一个线程的运行,为完成端口提供服务,避免过于频繁的线程场景切换。因此可以使用下列语句来创建一个完成端口FIocpHandle := CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);

执行线程个数

创建完成端口后,就可以将套接字句柄与对象关联在一起,这时就需要创建工作者线程,以便在完成端口收到数据后,为完成端口提供处理数据线程。到底创建多少个线程为完成端口服务,这个是完成端口最为复杂的一方面,创建多了线程会造成频繁的线程场景切换;创建少了线程如果某一个处理非常耗时,如连接数据库、读写文件,又会造成完成端口拥塞,因此这个参数需要提供设置,并根据最终的应用场景反复测试得出一个结果。一般的经验值是设置为CPU的个数*2+4;

IOCP完成端口一般使用步骤

1、创建一个完成端口;

2、判断系统内安装了多少个处理器;

3、创建工作者线程;

4、创建一个SOCKET套接字开始监听;

5、使用Accept接收连接;

6、调用CreateIoCompletionPort将连接和完成端口绑定在一起;

7、投递接收数据请求

8、工作者线程调用GetQueuedCompletionStatus获取事件通知,处理数据;

IOCP控件核心代码

第1步到第4步实现代码:

procedure TIocpServer.Open;
var
  WsaData: TWsaData;
  iNumberOfProcessors, i, iWorkThreadCount: Integer;
  WorkThread: TWorkThread;
  Addr: TSockAddr;
begin
  if WSAStartup($0202, WsaData) <> 0 then  //初始化SOCKET
    raise ESocketError.Create(GetLastWsaErrorStr);
  FIocpHandle := CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);  //创建一个完成端口
  if FIocpHandle = 0 then
    raise ESocketError.Create(GetLastErrorStr);
  FSocket := WSASocket(PF_INET, SOCK_STREAM, 0, nil, 0, WSA_FLAG_OVERLAPPED); //创建一个SOCKET句柄
  if FSocket = INVALID_SOCKET then
    raise ESocketError.Create(GetLastWsaErrorStr);
  FillChar(Addr, SizeOf(Addr), 0);
  Addr.sin_family := AF_INET;
  Addr.sin_port := htons(FPort);
  Addr.sin_addr.S_addr := htonl(INADDR_ANY); //在任何地址上监听,如果有多块网卡,会每块都监听,也可以指定只监听某一个IP地址
  if bind(FSocket, @Addr, SizeOf(Addr)) <> 0 then //把SOCKET句柄绑定端口
    raise ESocketError.Create(GetLastWsaErrorStr);
  if listen(FSocket, MaxInt) <> 0 then 
     raise ESocketError.Create(GetLastWsaErrorStr);
  iNumberOfProcessors := GetCPUCount; //获取CPU个数
  iWorkThreadCount := iNumberOfProcessors * 2 + 4; //由于服务器处理可能比较费时间,因此线程设为CPU*2+4
  if iWorkThreadCount < FMinWorkThrCount then //限定最大工作者线程和最小工作者线程
    iWorkThreadCount := FMinWorkThrCount;
  if iWorkThreadCount > FMaxWorkThrCount then
    iWorkThreadCount := FMaxWorkThrCount;
  for i := 0 to iWorkThreadCount - 1 do //创建工作者线程
  begin
    WorkThread := TWorkThread.Create(Self, True);
    FWorkThreads.Add(WorkThread);
    WorkThread.Resume;
  end;
  FAcceptThreadPool.Active := True; //启动监听线程池
  FAcceptThread := TAcceptThread.Create(Self, True); //启动监听线程
  FAcceptThread.Resume;
end;
第5步和第6步实现代码:
procedure TIocpServer.AcceptClient;
var
  ClientSocket: TSocket;
begin
  ClientSocket := WSAAccept(FSocket, nil, nil, nil, 0); //接收连接
  if ClientSocket <> INVALID_SOCKET then
  begin
    if not FActive then
    begin
      closesocket(ClientSocket);
      Exit;
    end;
    FAcceptThreadPool.PostSocket(ClientSocket); //这里使用线程池主要作用是为了判断发送的第一个字节身份标识,用来是判断协议类型
  end;
end;
FAcceptThreadPool是一个用完成端口实现的线程池TIocpThreadPool,主要是有TCheckThread来判断完成端口的返回,并检测是否6S内有发送标志位上来,主要实现过程是响应OnConnect事件,并在OnConnect事件中判断是否允许连接。
procedure TIocpServer.CheckClient(const ASocket: TSocket);
var
  SocketHandle: TSocketHandle;
  iIndex: Integer;
  ClientSocket: PClientSocket;
begin
  SocketHandle := nil;
  if not DoConnect(ASocket, SocketHandle) then //如果不允许连接,则退出
  begin
    closesocket(ASocket);
    Exit;
  end;
  FSocketHandles.Lock; //加到列表中
  try
    iIndex := FSocketHandles.Add(SocketHandle);
    ClientSocket := FSocketHandles.Items[iIndex];
  finally
    FSocketHandles.UnLock;
  end;
  if CreateIoCompletionPort(ASocket, FIOCPHandle, DWORD(ClientSocket), 0) = 0 then //将连接和完成端口绑定在一起
  begin
    DoError('CreateIoCompletionPort', GetLastWsaErrorStr);
    FSocketHandles.Lock; //如果投递到列表中失败,则删除
    try
      FSocketHandles.Delete(iIndex);
    finally
      FSocketHandles.UnLock;
    end;
  end
  else
  begin
    SocketHandle.PreRecv(nil); //投递接收请求
  end;
end;

procedure TDMDispatchCenter.IcpSvrConnect(const ASocket: Cardinal;
  var AAllowConnect: Boolean; var SocketHandle: TSocketHandle);
var
  BaseSocket: TBaseSocket;
  chFlag: Char;

  function GetSocket(const AEnable: Boolean; BaseSocketClass: TBaseSocketClass): TBaseSocket;
  begin
    if AEnable then
      Result := BaseSocketClass.Create(IcpSvr, ASocket)
    else
      Result := nil;
  end;
begin
  if (GIniOptions.MaxSocketCount > 0) and (IcpSvr.SocketHandles.Count >= GIniOptions.MaxSocketCount) then
  begin
    AAllowConnect := False;
    Exit;
  end;
  if IcpSvr.ReadChar(ASocket, chFlag, 6*1000) then //必须在6S内收到标志
  begin
    case TSocketFlag(Byte(chFlag)) of
      sfSQL: BaseSocket := GetSocket(GIniOptions.SQLProtocol, TSQLSocket);
      sfUpload: BaseSocket := GetSocket(GIniOptions.UploadProtocol, TUploadSocket);
      sfDownload: BaseSocket := GetSocket(GIniOptions.DownloadProtocol, TDownloadSocket);
      sfControl: BaseSocket := GetSocket(GIniOptions.ControlProtocol, TControlSocket);
      sfLog: BaseSocket := GetSocket(GIniOptions.LogProtocol, TLogSocket);
    else
      BaseSocket := nil;;
    end;
    if BaseSocket <> nil then
    begin
      SocketHandle := BaseSocket;
      WriteLogMsg(ltDebug, Format('Client Connect, Local Address: %s:%d; Remote Address: %s:%d',
        [SocketHandle.LocalAddress, SocketHandle.LocalPort, SocketHandle.RemoteAddress, SocketHandle.RemotePort]));
      WriteLogMsg(ltDebug, Format('Client Count: %d', [IcpSvr.SocketHandles.Count + 1]));
      AAllowConnect := True;
    end
    else
      AAllowConnect := False;
  end
  else
    AAllowConnect := False;
end;
其中ReadChar函数是用于判断指定时间是否有数据上来,函数实现过程用Select函数检测:
function TIocpServer.ReadChar(const ASocket: TSocket; var AChar: Char; const ATimeOutMS: Integer): Boolean;
var
  iRead: Integer;
begin
  Result := CheckTimeOut(ASocket, ATimeOutMS);
  if Result then
  begin
    iRead := recv(ASocket, AChar, 1, 0);
    Result := iRead = 1; 
  end;
end;

function TIocpServer.CheckTimeOut(const ASocket: TSocket;
  const ATimeOutMS: Integer): Boolean;
var
  tmTo: TTimeVal;
  FDRead: TFDSet;
begin
  FillChar(FDRead, SizeOf(FDRead), 0);
  FDRead.fd_count := 1;
  FDRead.fd_array[0] := ASocket;
  tmTo.tv_sec := ATimeOutMS div 1000;
  tmTo.tv_usec := (ATimeOutMS mod 1000) * 1000;
  Result := Select(0, @FDRead, nil, nil, @tmTO) = 1;
end;
第7步实现代码

接收连接之后要投递接收数据请求,实现代码:

procedure TSocketHandle.PreRecv(AIocpRecord: PIocpRecord);
var
  iFlags, iTransfer: Cardinal;
  iErrCode: Integer;
begin
  if not Assigned(AIocpRecord) then
  begin
    New(AIocpRecord);
    AIocpRecord.WsaBuf.buf := @FIocpRecvBuf;
    AIocpRecord.WsaBuf.len := MAX_IOCPBUFSIZE;
    FIocpRecv := AIocpRecord;
  end;
  AIocpRecord.Overlapped.Internal := 0;
  AIocpRecord.Overlapped.InternalHigh := 0;
  AIocpRecord.Overlapped.Offset := 0;
  AIocpRecord.Overlapped.OffsetHigh := 0;
  AIocpRecord.Overlapped.hEvent := 0;
  //AIocpRecord.WsaBuf.buf := @FIocpRecvBuf;
  //AIocpRecord.WsaBuf.len := MAX_IOCPBUFSIZE;
  AIocpRecord.IocpOperate := ioRead;
  iFlags := 0;
  if WSARecv(FSocket, @AIocpRecord.WsaBuf, 1, iTransfer, iFlags, @AIocpRecord.Overlapped,
    nil) = SOCKET_ERROR then
  begin
    iErrCode := WSAGetLastError;
    if iErrCode = WSAECONNRESET then //客户端被关闭
      FConnected := False;
    if iErrCode <> ERROR_IO_PENDING then //不抛出异常,触发异常事件
    begin
      FIocpServer.DoError('WSARecv', GetLastWsaErrorStr);
      ProcessNetError(iErrCode);
    end;
  end;
end;
第8步实现代码:
function TIocpServer.WorkClient: Boolean;
var
  ClientSocket: PClientSocket;
  IocpRecord: PIocpRecord;
  iWorkCount: Cardinal;
begin
  IocpRecord := nil;
  iWorkCount := 0;
  ClientSocket := nil;
  Result := False;
  if not GetQueuedCompletionStatus(FIocpHandle, iWorkCount, DWORD(ClientSocket),
    POverlapped(IocpRecord), INFINITE) then //此处有可能多个线程处理同一个SocketHandle对象,因此需要加锁
  begin  //客户端异常断开
    if Assigned(ClientSocket) and Assigned(ClientSocket.SocketHandle) then
    begin
      ClientSocket.SocketHandle.FConnected := False;
      Exit;
    end;
  end;
  if Cardinal(IocpRecord) = SHUTDOWN_FLAG then
    Exit;
  if not FActive then
    Exit;
  Result := True;
  if Assigned(ClientSocket) and Assigned(ClientSocket.SocketHandle) then
  begin
    if ClientSocket.SocketHandle.Connected then
    begin
      if iWorkCount > 0 then
      begin
        try
          ClientSocket.Lock.Enter;
          try
            if Assigned(ClientSocket.SocketHandle) then
              ClientSocket.SocketHandle.ProcessIOComplete(IocpRecord, iWorkCount);
          finally
            ClientSocket.Lock.Leave;
          end;
          if Assigned(ClientSocket.SocketHandle) and (not ClientSocket.SocketHandle.Connected) then
          begin
            ClientSocket.Lock.Enter;
            try
              if Assigned(ClientSocket.SocketHandle) then
                FreeSocketHandle(ClientSocket.SocketHandle);
            finally
              ClientSocket.Lock.Leave;
            end;
          end;
        except
          on E: Exception do
            DoError('ProcessIOComplete', E.Message);
        end;
      end
      else //如果完成个数为0,且状态为接收,则释放连接
      begin
        if IocpRecord.IocpOperate = ioRead then
        begin
          ClientSocket.Lock.Enter;
          try
            if Assigned(ClientSocket.SocketHandle) then
              FreeSocketHandle(ClientSocket.SocketHandle);
          finally
            ClientSocket.Lock.Leave;
          end;
        end
        else
          DoError('WorkClient', 'WorkCount = 0, Code: ' + IntToStr(GetLastError) + ', Message: ' + GetLastErrorStr);
      end;
    end
    else //断开连接
    begin
      ClientSocket.Lock.Enter;
      try
        if Assigned(ClientSocket.SocketHandle) then
          FreeSocketHandle(ClientSocket.SocketHandle);
      finally
        ClientSocket.Lock.Leave;
      end;
    end;
  end
  else //WorkCount为0表示发生了异常,记录日志
    DoError('GetQueuedCompletionStatus', 'Return SocketHandle nil');
end;
第8步主要是使用GetQueuedCompletionStatus函数来获取完成端口事件通知,如果有事件完成,则可以通过lpCompletionKey来知道是哪个句柄有数据收到。

这里有个技巧是我们在绑定的时候使用如下语句:CreateIoCompletionPort(ASocket, FIOCPHandle, DWORD(ClientSocket), 0),用lpCompletionKey来传递了一个指针,这其中就包含了一个锁和服务对象,定义结构如下:

{* 客户端对象和锁 *}
  TClientSocket = record
    Lock: TCriticalSection;
    SocketHandle: TSocketHandle;
  end;
  PClientSocket = ^TClientSocket;
因而我们在收到事件通知后,就可以直接调用TSocketHandle对象来完成分包解包以及后续的逻辑处理。
这样一个完成端口的整体骨架就有了,后续还有收发数据、分包解包和业务逻辑处理,以及对象和锁分离等细节处理。更详细代码见示例代码的IOCPSocket单元。

V1版下载地址:http://download.csdn.net/detail/sqldebug_fan/4510076,需要资源10分,有稳定性问题,可以作为研究稳定性用;

V2版下载地址:http://download.csdn.net/detail/sqldebug_fan/5560185,不需要资源分,解决了稳定性问题和提高性能;免责声明:此代码只是为了演示IOCP编程,仅用于学习和研究,切勿用于商业用途。水平有限,错误在所难免,欢迎指正和指导。邮箱地址:[email protected]


你可能感兴趣的:(exception,工作,socket,function,Integer)