重叠(Overlapped)I/O模型

时间:2022-05-12 07:54:13

    与介绍过的其他模型相比,重叠I/O模型提供了更好的系统性能。这个模型的基本设计思想是允许应用程序使用重叠数据结构一次投递一个或者多个异步I/O请求(即所谓的重叠I/O)。提交的I/O请求完成之后,与之关联的重叠数据结构中的事件对象受信,应用程序便可使用WSAGetOverlappedResult函数获取重叠操作结果。这和使用重叠结构调用ReadFile和WriteFile函数操作文件类似。

    1  重叠I/O函数

     为了使用重叠I/O模型,必须调用特定的重叠I/O函数创建套接字,在套接字上传输数据,这些函数有Winsock2.h中新添的函数,如WSASend、WSARecv等,也有一些I/O扩展函数,如AcceptEx等,下面分别介绍:

    (1)创建套接字

     要使用重叠I/O模型,在创建套接字时,必须使用WSASocket函数,设置重叠标志。

  

The WSASocket function creates a socket that is bound to a specific transport-service provider.

SOCKET WSASocket(
  __in          int af,
  __in          int type,
  __in          int protocol,//前三个参数与socket函数相同
  __in          LPWSAPROTOCOL_INFO lpProtocolInfo,  //指定下层服务提供者   ,可以是NULL
  __in          GROUP g,    //保留
  __in          DWORD dwFlags //指定套接字属性。要使用重叠I/O模型,必须指定WSA_FLAG_OVERLAPPED
);
lpProtocolInfo

A pointer to a WSAPROTOCOL_INFO structure that defines the characteristics of the socket to be created. If this parameter is not NULL, the socket will be bound to the provider associated with the indicated WSAPROTOCOL_INFO structure.


例如:可以使用如下代码创建监听套接字:

The following example demonstrates the use of the WSASocket function.

#include <winsock2.h>
#include <stdio.h>

int main() {
  WSADATA wsaData;
  SOCKET RecvSocket;

  int iResult;

  //-----------------------------------------------
  // Initialize Winsock
  iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
  if (iResult != 0) {
      printf("WSAStartup failed: %d\n", iResult);
      return 1;
  }


  //-----------------------------------------------
  // Create a socket 
  RecvSocket = WSASocket(AF_INET, 
    SOCK_DGRAM, 
    IPPROTO_UDP, 
    NULL, 
    0, 
    WSA_FLAG_OVERLAPPED);

  if (RecvSocket == INVALID_SOCKET) {
      printf("WSASocket call failed with error: %ld\n", WSAGetLastError());
      WSACleanup();
      return 1;
  }

}
2    传输数据

     在重叠I/O模型中,传输数据的函数是WSASend\WSARecv(TCP)和WSASendTo、WSARecvFrom等,下面是WSASend的定义:

    

The WSASend function sends data on a connected socket.

int WSASend(
  __in          SOCKET s,
  __in          LPWSABUF lpBuffers,
  __in          DWORD dwBufferCount,
  __out         LPDWORD lpNumberOfBytesSent,
  __in          DWORD dwFlags,
  __in          LPWSAOVERLAPPED lpOverlapped,
  __in          LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

Parameters

s

A descriptor that identifies a connected socket.

lpBuffers

A pointer to an array of WSABUF structures. Each WSABUF structure contains a pointer to a buffer and the length, in bytes, of the buffer. This array must remain valid for the duration of the send operation.

dwBufferCount

The number of WSABUF structures in the lpBuffers array.

lpNumberOfBytesSent

A pointer to the number, in bytes, sent by this call if the I/O operation completes immediately. If the lpOverlapped parameter is non-NULL, this parameter is optional and can be set to NULL.

dwFlags

The flags used to modify the behavior of the WSASend function call. For more information, see Using dwFlags in the Remarks section.

lpOverlapped

A pointer to a WSAOVERLAPPED structure. This parameter is ignored for nonoverlapped sockets.

lpCompletionRoutine

A pointer to the completion routine called when the send operation has been completed. This parameter is ignored for nonoverlapped sockets.

I/O操作函数都接收了一个WSAOVERLAPPED结构类型的参数。这些函数被调用之后立即返回,它们依靠应用程序传递的WSAOVERLAPPED结构管理I/O请求的完成。应用程序有两种方法可以接收到重叠I/O请求操作完成的通知:

(1)在与WSAOVERLAPPED结构关联的事件对象上等待,I/O操作完成后,此事件对象受信,这是最经常使用的方法。

(2)使用lpCompletionRoutine指向的完成例程,完成例程是一个自定义的函数,I/O操作完成后,Winsock便去调用它,这种方法很少用,将lpCompletionRoutine设为NULL即可。

下面讨论使用第一种方法接收网络事件通知:

   3   接收连接

   可以异步接收连接请求的函数是AcceptEX。这是一个Mincrosoft扩展函数,它接受一个新的连接,返回本地和远程地址,取得客户程序发送的第一块数据,函数定义如下:

The AcceptEx function accepts a new connection, returns the local and remote address, and receives the first block of data sent by the client application.

Note  This function is a Microsoft-specific extension to the Windows Sockets specification.

BOOL AcceptEx(
  __in          SOCKET sListenSocket,
  __in          SOCKET sAcceptSocket,
  __in          PVOID lpOutputBuffer,
  __in          DWORD dwReceiveDataLength,
  __in          DWORD dwLocalAddressLength,
  __in          DWORD dwRemoteAddressLength,
  __out         LPDWORD lpdwBytesReceived,
  __in          LPOVERLAPPED lpOverlapped
);

Parameters

sListenSocket

A descriptor identifying a socket that has already been called with the listen function. A server application waits for attempts to connect on this socket.

sAcceptSocket

A descriptor identifying a socket on which to accept an incoming connection. This socket must not be bound or connected.

lpOutputBuffer

A pointer to a buffer that receives the first block of data sent on a new connection, the local address of the server, and the remote address of the client. The receive data is written to the first part of the buffer starting at offset zero, while the addresses are written to the latter part of the buffer. This parameter must be specified.

dwReceiveDataLength

The number of bytes in lpOutputBuffer that will be used for actual receive data at the beginning of the buffer. This size should not include the size of the local address of the server, nor the remote address of the client; they are appended to the output buffer. If dwReceiveDataLength is zero, accepting the connection will not result in a receive operation. Instead, AcceptEx completes as soon as a connection arrives, without waiting for any data.

dwLocalAddressLength

The number of bytes reserved for the local address information. This value must be at least 16 bytes more than the maximum address length for the transport protocol in use.

dwRemoteAddressLength

The number of bytes reserved for the remote address information. This value must be at least 16 bytes more than the maximum address length for the transport protocol in use. Cannot be zero.

lpdwBytesReceived

A pointer to a DWORD that receives the count of bytes received. This parameter is set only if the operation completes synchronously. If it returns ERROR_IO_PENDING and is completed later, then this DWORD is never set and you must obtain the number of bytes read from the completion notification mechanism.

lpOverlapped

An OVERLAPPED structure that is used to process the request. This parameter must be specified; it cannot be NULL.

The AcceptEx function uses overlapped I/O, unlike the accept function. If your application uses AcceptEx, it can service a large number of clients with a relatively small number of threads. As with all overlapped Windows functions, either Windows events or completion ports can be used as a completion notification mechanism.

Another key difference between the AcceptEx function and the accept function is that AcceptEx requires the caller to already have two sockets:

  • One that specifies the socket on which to listen.
  • One that specifies the socket on which to accept the connection.

The sAcceptSocket parameter must be an open socket that is neither bound nor connected.

The lpNumberOfBytesTransferred parameter of the GetQueuedCompletionStatus function or the GetOverlappedResult function indicates the number of bytes received in the request.

When this operation is successfully completed, sAcceptSocket can be passed, but to the following functions only:

ReadFile
WriteFile
send
WSASend
recv
WSARecv
TransmitFile
closesocket
setsockopt (only for SO_UPDATE_ACCEPT_CONTEXT)

When the AcceptEx function returns, the socket sAcceptSocket is in the default state for a connected socket. The socket sAcceptSocket does not inherit the properties of the socket associated with sListenSocket parameter until SO_UPDATE_ACCEPT_CONTEXT is set on the socket. Use the setsockopt function to set the SO_UPDATE_ACCEPT_CONTEXT option, specifying sAcceptSocket as the socket handle and sListenSocket as the option value.

For example:

err = setsockopt( sAcceptSocket, 
    SOL_SOCKET, 
    SO_UPDATE_ACCEPT_CONTEXT, 
    (char *)&sListenSocket, 
    sizeof(sListenSocket) );

If a receive buffer is provided, the overlapped operation will not complete until a connection is accepted and data is read. Use the getsockopt function with the SO_CONNECT_TIME option to check whether a connection has been accepted. If it has been accepted, you can determine how long the connection has been established. The return value is the number of seconds that the socket has been connected. If the socket is not connected, the getsockopt returns 0xFFFFFFFF. Applications that check whether the overlapped operation has completed, in combination with the SO_CONNECT_TIME option, can determine that a connection has been accepted but no data has been received. Scrutinizing a connection in this manner enables an application to determine whether connections that have been established for a while have received no data. It is recommended such connections be terminated by closing the accepted socket, which forces the AcceptEx function call to complete with an error.

For example:

INT seconds;
INT bytes = sizeof(seconds);
err = getsockopt( sAcceptSocket, SOL_SOCKET, SO_CONNECT_TIME,
                      (char *)&seconds, (PINT)&bytes );
if ( err != NO_ERROR ) {
    printf( "getsockopt(SO_CONNECT_TIME) failed: %ld\n", WSAGetLastError( ) );
    exit(1);
}

Example Code

The following example uses the AcceptEx function using overlapped I/O and completion ports.

#include <stdio.h>
#include "winsock2.h"
#include "mswsock.h"

void main() {
  //----------------------------------------
  // Declare and initialize variables
  WSADATA wsaData;
  HANDLE hCompPort;
  LPFN_ACCEPTEX lpfnAcceptEx = NULL;
  GUID GuidAcceptEx = WSAID_ACCEPTEX;
  WSAOVERLAPPED olOverlap;
  
  SOCKET ListenSocket, AcceptSocket;
  sockaddr_in service;
  char lpOutputBuf[1024];
  int outBufLen = 1024;
  DWORD dwBytes;

  //----------------------------------------
  // Initialize Winsock
  int iResult = WSAStartup( MAKEWORD(2,2), &wsaData );
  if( iResult != NO_ERROR )
    printf("Error at WSAStartup\n");

  //----------------------------------------
  // Create a handle for the completion port
  hCompPort = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, (u_long)0, 0 );

  //----------------------------------------
  // Create a listening socket
  ListenSocket = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
  if (ListenSocket == INVALID_SOCKET) {
    printf("Error at socket(): ListenSocket\n");
    WSACleanup();
    return;
  }

  //----------------------------------------
  // Associate the listening socket with the completion port
  CreateIoCompletionPort((HANDLE)ListenSocket, hCompPort, (u_long)0, 0);

  //----------------------------------------
  // Bind the listening socket to the local IP address
  // and port 27015
  hostent* thisHost;
  char* ip;
  u_short port;
  port = 27015;
  thisHost = gethostbyname("");
  ip = inet_ntoa (*(struct in_addr *)*thisHost->h_addr_list);

  service.sin_family = AF_INET;
  service.sin_addr.s_addr = inet_addr(ip);  service.sin_port = htons(port);

  if ( bind( ListenSocket,(SOCKADDR*) &service, sizeof(service) )  == SOCKET_ERROR ) {
    printf("bind failed\n");
    closesocket(ListenSocket);
    return;
  }

  //----------------------------------------
  // Start listening on the listening socket
  if (listen( ListenSocket, 100 ) == SOCKET_ERROR) {
    printf("error listening\n");
  } 
  printf("Listening on address: %s:%d\n", ip, port);

  //----------------------------------------
  // Load the AcceptEx function into memory using WSAIoctl.
  // The WSAIoctl function is an extension of the ioctlsocket()
  // function that can use overlapped I/O. The function's 3rd
  // through 6th parameters are input and output buffers where
  // we pass the pointer to our AcceptEx function. This is used
  // so that we can call the AcceptEx function directly, rather
  // than refer to the Mswsock.lib library.
  WSAIoctl(ListenSocket, 
    SIO_GET_EXTENSION_FUNCTION_POINTER, 
    &GuidAcceptEx, 
    sizeof(GuidAcceptEx),
    &lpfnAcceptEx, 
    sizeof(lpfnAcceptEx), 
    &dwBytes, 
    NULL, 
    NULL);

  //----------------------------------------
  // Create an accepting socket
  AcceptSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (AcceptSocket == INVALID_SOCKET) {
    printf("Error creating accept socket.\n");
    WSACleanup();
    return;
  }

  //----------------------------------------
  // Empty our overlapped structure and accept connections.
  memset(&olOverlap, 0, sizeof(olOverlap));

  lpfnAcceptEx(ListenSocket, 
    AcceptSocket,
    lpOutputBuf, 
    outBufLen - ((sizeof(sockaddr_in) + 16) * 2),
    sizeof(sockaddr_in) + 16, 
    sizeof(sockaddr_in) + 16, 
    &dwBytes, 
    &olOverlap);

  //----------------------------------------
  // Associate the accept socket with the completion port
  CreateIoCompletionPort((HANDLE)AcceptSocket, hCompPort, (u_long)0, 0);

  //----------------------------------------
  // Continue on to use send, recv, TransmitFile(), etc.,.
  ...

}

AcceptEX函数将几个套接字函数的功能集合在一起。如果它投递的请求成功完成,则执行了如下3个操作:

(1)接受了新的连接

(2)新连接的本地地址和远程地址都会返回

(3)接收到了远程主机发来的第一块数据

AcceptEX和大家熟悉的accept函数有很大的不同就是AcceptEX函数需要调用者提供两个套接字,一个指定了在哪个套接字上监听,另一个指定了在哪个套接字上接受连接,也就是说,AcceptEX不会像accept函数一样为新的连接创建套接字。

   如果提供了新的缓冲区,AcceptEX投递的重叠操作直到接受到连接并且读到数据之后才会返回。以SO_CONNECT_TIME为参数调用getsockopt函数可以检查到是否接受了连接,如果接受了连接,这个调用还可以取得连接已经建立了多长时间。

  AcceptEX函数是从Mswsock.lib库中导出的,为了能够直接调用它,而不用链接到Mswsock.lib库,需要使用WSAIoctl函数将AcceptEX函数加载到内存,WSAIoctl函数是ioctlsocket函数的扩展,它可以使用重叠I/O。函数的第3个到第6个参数是输入和输出缓冲区,在这里传递AcceptEX函数的指针,具体加载代码如下:

   //加载扩展函数AcceptEX

   GUID   GuidAcceptEx   =WSAID_ACCEPTEX;

  DWORD   dwBytes;

  WSAIoctl(pListen->s,SIO_GET_EXTENSION_FUNCTION_POINTER,&GuidAcceptEx,sizeof(GuidAcceptEx),&pListen->lpfnAcceptEx,sizeof(pListen->lpfnAcceptEx),&dwBytes,NULL,NULL);

   3   事件通知方式

    为了使用重叠I/O,每个I/O函数都要接收一个WSAOVERLAPPED结构类型的参数,这个结构在Winsock2.h文件中定义如下:

The WSAOVERLAPPED structure provides a communication medium between the initiation of an overlapped I/O operation and its subsequent completion. The WSAOVERLAPPED structure is compatible with the Windows OVERLAPPED structure.

typedef struct _WSAOVERLAPPED {
  ULONG_PTR Internal;
  ULONG_PTR InternalHigh;
  union {
    struct {
      DWORD Offset;
      DWORD OffsetHigh;
    };
    PVOID Pointer;
  };
  HANDLE hEvent;
} WSAOVERLAPPED, 
 *LPWSAOVERLAPPED;
当重叠I/O请求最终完成以后,以之关联的事件对象受信,等待函数返回,应用程序可以使用WSAGetOverlappedResult函数取得重叠操作的结果,函数用法如下:

The WSAGetOverlappedResult function retrieves the results of an overlapped operation on the specified socket.

BOOL WSAAPI WSAGetOverlappedResult(
  __in          SOCKET s,
  __in          LPWSAOVERLAPPED lpOverlapped,
  __out         LPDWORD lpcbTransfer,
  __in          BOOL fWait,
  __out         LPDWORD lpdwFlags
);