长亭百川云 - 文章详情

重叠IO之事件通知

Delphi研习社

54

2024-07-13

说来惭愧第二季都快断更一年了,不是不更新是winsocket的IO模型对于新人太难了。以我的编程能力整整研究了两天才把重叠IO搞定

重叠 I/O 的概念

当调用 ReadFile 和 WriteFile 时,如果最后一个参数 lpOverlapped 设置为 NULL,那么线程就阻塞在这里,直到读写完指定的数据后,它们才返回。这样在读写大文件的时候,很多时间都浪费在等待 ReadFile 和 WriteFile 的返回上面。如果 ReadFile 和 WriteFile 是往管道里读写数据,那么有可能阻塞得更久,导致程序性能下降。

为了解决这个问题,windows 引进了重叠 I/O 的概念,它能够同时以多个线程处理多个 I/O

设置了 OVERLAPPED 参数后,ReadFile/WriteFile 的调用会立即返回,这时候你可以去做其他的事 所谓异步,系统会自动替你完成 ReadFile/WriteFile 相关的 I/O 操作。你也可以同时发出几个 ReadFile/WriteFile 的调用 所谓重叠

参考资料:http://blog.pfan.cn/xman/44361.html

需要注意的是,有两个方式可以用来管理重叠 IO 请求的完成情况(就是说接到重叠操作完成的通知):

  1. 事件对象通知(event object notification)

  2. 完成例程(completion routines) 注意:这里并不是完成端口

重叠模型的优点

重叠 I/O 模型便能适用于安装了 Winsock 2 的所有 Windows 平台

比起阻塞、select、WSAAsyncSelect 以及 WSAEventSelect 等模型,重叠 I/O 模型使应用程序能达到更佳的系统性能。

我们常用的 send, sendto, recv, recvfrom 也都要被 WSASend, WSASendto, WSARecv, WSARecvFrom 替换掉

服务器端编码

在正式开始之前,先说一下重叠 I/O 模型的编程步骤:

  1. 创建一个套接字,开始在指定的端口上监听连接请求

  2. 接受一个客户端进入的连接请求;

  3. 为接受的套接字新建一个 WSAOVERLAPPED 结构,并为该结构分配一个事件对象句柄,同时将该事件对象句柄分配给一个事件数组,以便稍后由 WSAWaitForMultipleEvents 函数使用。

  4. 在套接字上投递一个异步 WSARecv 请求,指定参数为 WSAOVERLAPPED 结构。

  5. 使用步骤 3)的事件数组,调用 WSAWaitForMultipleEvents 函数,并等待与重叠调用关联在一起的事件进入“已传信”状态(换言之,等待那个事件的“触发”)

  6. WSAWaitForMultipleEvents 函数返回后,针对“已传信”状态的事件,调用 WSAResetEvent(重设事件)函数,从而重设事件对象,并对完成的重叠请求进行处理

  7. 使用 WSAGetOverlappedResult 函数,判断重叠调用的返回状态是什么

  8. 在套接字上投递另一个重叠 WSARecv 请求

  9. 重复步骤 5)~8)

重叠结构自然是重叠模型里的核心,其实真正需要操作的函数它是这么定义的

创建套接字的时候,假如使用的是 socket 函数,而非 WSASocket 函数,那么会默认设置 WSA_FLAG_OVERLAPPED 标志。

使用 WSASocket 函数创建套接字则需要 dwFlags 参数设置为 WSA_FLAG_OVERLAPPED

创建服务器的代码,以函数的形式进行封装,代码还是以前课程的代码

type  
    {服务器对象封装}  
    TScoketRecord = record  
    ServerAddr: TSockAddrIn;  
    Scoket: TSocket;  
  end;  
  
type  
  
  { 网络异常 }  
  TWinSocketException = class(Exception)  
  
    { 构造方法 }  
    constructor Create(const Msg: string);  
  end;  
  
{初始化网络库}  
function InitSocket: Integer;  
{初始化}  
var  
  WSAData: TWSAData;  
begin  
  // 定义当前使用网络库版本  
  if WSAStartup(WINSOCK_VERSION, WSAData) <> 0 then begin  
    Result := -1;  
    raise TWinSocketException.Create('网络库初始化失败');  
  end;  
  Result := 0;  
end;  
  
  
  
{启动服务器}  
function CreateServerSocket(Address: string; Port: Integer): TScoketRecord;  
var  
  ServerAddr: TSockAddrIn;  
var  
  ScoketRecord: TScoketRecord;  
begin  
  // 创建服务器端对象。在套接字上使用重叠 I/O 模型,必须使用 WSA_FLAG_OVERLAPPED 标志创建套接字  
  var Server := WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, nil, 0, WSA_FLAG_OVERLAPPED);  
  
  // 创建是否成功  
  if (Server = INVALID_SOCKET) then begin  
    raise TWinSocketException.Create('服务器创建失败');  
    Exit;  
  end;  
  
  // 给服务器指定 IP 和端口 ,组装信息  
  with ServerAddr do begin  
    sin_family := PF_INET;  
    // 端口号  
    sin_port := Port;  
    // 本机所有有可能的 IP 作为服务器端的 IP  
    sin_addr.S_addr := inet_addr(PAnsiChar(AnsiString(Address)));  
  end;  
  
  ScoketRecord.ServerAddr := ServerAddr;  
  
  if bind(Server, TSockAddr(ServerAddr), SizeOf(ServerAddr)) = SOCKET_ERROR then begin  
  
    raise TWinSocketException.Create('端口号被占用');  
    Exit;  
  end;  
  if listen(Server, SOMAXCONN) = SOCKET_ERROR then begin  
  
    raise TWinSocketException.Create('监听失败');  
    Exit;  
  end;  
  ScoketRecord.Scoket := Server;  
  Result := ScoketRecord;  
end;  

核心代码,主要分为两个部分

  • 无限监听新客户端连接并绑定重叠结构对象(发送或者接收数据)以线程的方式书写

  • 无限监听重叠结构的事件信号,WSAWaitForMultipleEvents 就是遍历是否有信号发生,同时将数据填充到缓冲区(以线程的方式书写)

此处为了便于 IO 完成时,需要读取缓冲区的数据,获取之前连接成功的客户端对象和重叠结构,所以我封装了新的结构体

//在读取数据时需要一一对应,所以封装成结构体比较方便  
TOverlappedDomain = record  
    //事件数组  
    Events: array[0..WSA_MAXIMUM_WAIT_EVENTS] of TWSAEVENT;  
    //客户端数组  
    Clients: array[0..WSA_MAXIMUM_WAIT_EVENTS] of TSocket;  
    //重叠结构数组  
    Overlappeds: array[0..WSA_MAXIMUM_WAIT_EVENTS] of TWSAOVERLAPPED;  
end;  

为了便于测试,当前项目是以控制台应用书写,并在 DelphiXE10.4.2 下测试通过

var  
  //缓冲区  
  buf: array[0..1023] of Char;  
  //记录事件总数  
  EventTotal: Integer = 0;  
  
var  
  wBuf: WSABUF;  
var  
  //封装的结构体变量  
  OverlappedDomain: TOverlappedDomain;  
  
begin  
  wBuf.len := 1024;  
  wBuf.buf := @buf;  
  
  if (InitSocket() <> 0) then begin  
    Writeln('初始化失败');  
    Readln;  
    Exit;  
  end;  
  //无线侦听客户端连接  
  TThread.CreateAnonymousThread(  
    procedure  
    begin  
      //1) 创建一个套接字,开始在指定的端口上监听连接请求;  
      var ScoketRecord := CreateServerSocket('127.0.0.1', 10086);  
      Writeln('服务器启动完成,等待客户端连接');  
      while true do begin  
        //2) 接受一个客户端进入的连接请求;  
        var AddrSize := sizeof(ScoketRecord.ServerAddr);  
        var ClientSocket := WSAAccept(ScoketRecord.Scoket, @ScoketRecord.ServerAddr, @AddrSize, nil, 0);  
        //新客户端加入列表  
        OverlappedDomain.Clients[EventTotal] := ClientSocket;  
        // 当客户端连接成功时,显示一下客户端的 IP  
        var CustomWinSocket := TCustomWinSocket.Create(ClientSocket);  
        Writeln('客户端 IP:' + CustomWinSocket.RemoteAddress);  
  
        //3)为接受的套接字新建一个 WSAOVERLAPPED 结构,并为该结构分配一个事件对象句柄  
//        Events[0] := WSACreateEvent();  
        //创建事件对象并加入列表  
        OverlappedDomain.Events[EventTotal] := WSACreateEvent();  
        //重叠结构绑定事件  
        OverlappedDomain.Overlappeds[EventTotal].hEvent := OverlappedDomain.Events[EventTotal];  
  
        Writeln('事件对象:' + IntToStr(OverlappedDomain.Overlappeds[EventTotal].hEvent));  
  
        //4) 在套接字上投递一个异步 WSARecv(接收数据) 请求,指定参数为 WSAOVERLAPPED 结构 ,投递一个的请求  
        var rec := 0;  
        var flg := 0;  
  
        var nRecv := WSARecv(ClientSocket, @wBuf, 1, Cardinal(rec), Cardinal(flg), @OverlappedDomain.Overlappeds[EventTotal], nil);  
        //10014—WSAEFAULT 地址无效。传给 Winsock 函数的指针地址无效。若指定的缓冲区太小,也会产生这个错误  
        writeln('WSARecv 函数:' + IntToStr(WSAGetLastError()) + ',' + IntToStr(nRecv));  
        EventTotal := EventTotal + 1;  
      end;  
  
    end).Start;  
  
  //无线侦听信号  
  TThread.CreateAnonymousThread(  
    procedure()  
    begin  
      while True do begin  
  
        //5) 使用步骤 3)的事件数组,调用 WSAWaitForMultipleEvents 函数,并等待与重叠调用关联在一起的事件进入“已传信”状态  
        var nRes := WSAWaitForMultipleEvents(EventTotal, @OverlappedDomain.Events, FALSE, WSA_INFINITE, FALSE);  
         //WSAWaitForMultipleEvents 的返回值,减去预定义的值 WSA_WAIT_EVENT_0,得到具体的引用值(即索引位置)。  
        if (WSA_WAIT_FAILED = nRes) or (WSA_WAIT_IO_COMPLETION = nRes) then begin  
          continue;  
        end;  
        writeln('WSAWaitForMultipleEvents 函数:' + IntToStr(nRes) + ',' + IntToStr(nRes - WSA_WAIT_EVENT_0));  
        // 信号重置  
        WSAResetEvent(OverlappedDomain.Events[nRes - WSA_WAIT_EVENT_0]);  
  
        //7) 使用 WSAGetOverlappedResult 函数,判断重叠调用的返回状态是什么  
  
        var flag := 0;  
        var bytes: Cardinal;  
        WSAGetOverlappedResult(OverlappedDomain.Clients[nRes - WSA_WAIT_EVENT_0], @OverlappedDomain.Overlappeds[nRes - WSA_WAIT_EVENT_0], bytes, True, Cardinal(flag));  
        //判断接收到的字节数  
        if (bytes = 0) then begin  
  
          closesocket(OverlappedDomain.Clients[nRes - WSA_WAIT_EVENT_0]);  
  
          WSACloseEvent(OverlappedDomain.Events[nRes - WSA_WAIT_EVENT_0]);  
          //数据已经不可读取(客户端发生异常)  
          EventTotal := EventTotal - 1;  
  
          continue;  
        end;  
        //读取数据  
        if (bytes > 0) then  
          Writeln('客户端序号:' + IntToStr(nRes - WSA_WAIT_EVENT_0) + ',内容:', PChar(wBuf.buf));  
  
       //8) 在套接字上投递另一个重叠 WSARecv 请求;  
            var rec := 0;  
        var flg := 0;  
        WSARecv(OverlappedDomain.Clients[nRes - WSA_WAIT_EVENT_0], @wBuf, 1, Cardinal(rec), Cardinal(flg), @OverlappedDomain.Overlappeds[nRes - WSA_WAIT_EVENT_0], nil);  
      end;  
    end).Start;  
  
//       WSACleanup();  
  Readln;  
end.  

需要说一下以下几个函数的区别

  1. accept、WSAAccept 是同步操作,AcceptEx 是异步操作

  2. WSAAccept 函数在 accept 函数基础上添加了条件函数判断是否接受客户端连接

  3. AcceptEx 是异步的,可以同时发出多个 AcceptEx 请求

说一下引用的单元

uses  
  System.Generics.Collections,  
  System.Classes,  
  System.Win.ScktComp,  
  Windows,  
  WinSock2,  
  WinSock,  
  Sysutils;  

客户端的代码就不放了,随便搞一个发送消息的就可以,同时当前只有读(也就是接收),并没有写。

至此基于事件通知方式的重叠 IO 完成,当然限于篇幅当前文章的代码可能有遗漏,可以给我发消息或者等稍后的视频······

在此特别感谢群里的无为,起初我不知道 WSABUF 类型的定义,直到看了他的C++代码

放一张运行效果图吧

推一下另一个公众号,有兴趣的朋友可以去看看

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2