IOCP Input/Output Completion Port IO完成端口

参考

https://blog.csdn.net/piggyxp/article/details/6922277

https://blog.csdn.net/ithzhang/article/details/8525306

https://www.cnblogs.com/persistentsnail/p/3862433.html

https://yq.aliyun.com/articles/50006

https://www.cnblogs.com/ldcsaa/archive/2012/02/25/2367409.html

https://www.cnblogs.com/alazalazalaz/p/4285958.html

https://blog.csdn.net/neicole/article/details/7549497

https://www.codeproject.com/Articles/10330/A-simple-IOCP-Server-Client-Class

 

1. 前言

I/O completion ports provide an efficient threading model for processing multiple asynchronous I/O requests on a multiprocessor system. When a process creates an I/O completion port, the system creates an associated queue object for requests whose sole purpose is to service these requests. Processes that handle many concurrent asynchronous I/O requests can do so more quickly and efficiently by using I/O completion ports in conjunction with a pre-allocated thread pool than by creating threads at the time they receive an I/O request.

https://docs.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports

IO完成端口,是微软提供的用来处理IO高并发的技术。IO完成端口并不单单是为了网络高并发。它的名字和意义就是IO高并发。与IO相关的,比如读写文件,都可以用这个。不过一般都用在网络上。

 

2. 选择的原因

Windows下有多种网络技术,都有部分缺陷:

  • 最原始的自己创建线程处理的,性能太低
  • 然后微软提出了一系列的方法,都有连接的数量限制,有的还有一些其他的限制,比如WSAAsyncSelect,必须要有一个窗口。
  • 所以Windows下如果要做网络,按照现在的互联网环境,已经不像以前那样,只有很大的公司才有高并发,随便做一些东西,都会有很大的并发问题,直接研究学习IO完成端口就好了。

 

3. IOCP的优点

  • 连接数量没有限制
  • 理论上比epoll更高级的模型,因为IOCP通知你的时候,数据已经接收完了,直接使用就可以,epoll通知的时候是数据到了,需要自己再接收一下。
  • 高并发,因为发送数据,等待接收数据,这些都是内核完成的,程序只需处理发送和接收后的逻辑,不用轮询,节省了资源

 

4. IOCP的缺点

  • 实现复杂,用IOCP,不管并发有多高,实现起步的复杂度是固定的
  • 必须有一个缓冲区,好处是缓冲区提前申请,每次利用,提高了性能,坏处是动态的,超过缓冲区的消息发送,比较麻烦

 

5. 函数介绍

 

5.1 创建IOCP

创建或绑定一个完成端口,一个程序只需创建一个就可以。(ps 微软有时候喜欢一个api多个用法,通过很多参数去区分,感觉上非常不好,比如以前的多字节和宽字节转换的api,很乱,不明确)

HANDLE WINAPI CreateIoCompletionPort(
  _In_     HANDLE    FileHandle,
  _In_opt_ HANDLE    ExistingCompletionPort,
  _In_     ULONG_PTR CompletionKey,
  _In_     DWORD     NumberOfConcurrentThreads
);

Parameters

FileHandle [in]

An open file handle or INVALID_HANDLE_VALUE.

The handle must be to an object that supports overlapped I/O.

If a handle is provided, it has to have been opened for overlapped I/O completion. For example, you must specify the FILE_FLAG_OVERLAPPED flag when using the CreateFile function to obtain the handle.

If INVALID_HANDLE_VALUE is specified, the function creates an I/O completion port without associating it with a file handle. In this case, the ExistingCompletionPort parameter must be NULL and the CompletionKey parameter is ignored.

一个handle,可以是socket,可以是文件,实际上socket属于文件的一种,这里描述就是一个文件handle。或是INVALID_HANDLE_VALUE。如果是INVALID_HANDLE_VALUE,就是创建一个IOCP。

ExistingCompletionPort [in, optional]

A handle to an existing I/O completion port or NULL.

If this parameter specifies an existing I/O completion port, the function associates it with the handle specified by the FileHandle parameter. The function returns the handle of the existing I/O completion port if successful; it does not create a new I/O completion port.

If this parameter is NULL, the function creates a new I/O completion port and, if the FileHandle parameter is valid, associates it with the new I/O completion port. Otherwise no file handle association occurs. The function returns the handle to the new I/O completion port if successful.

已经存在的一个IOCP的handle,如果是NULL,表示创建一个。

CompletionKey [in]

The per-handle user-defined completion key that is included in every I/O completion packet for the specified file handle. For more information, see the Remarks section.

绑定的时候传递的参数。

NumberOfConcurrentThreads [in]

The maximum number of threads that the operating system can allow to concurrently process I/O completion packets for the I/O completion port. This parameter is ignored if the ExistingCompletionPort parameter is not NULL.

If this parameter is zero, the system allows as many concurrently running threads as there are processors in the system.

创建的时候的参数,告诉IOCP用多少cpu,如果是0,就是默认自己控制。一般是0。

Return value

If the function succeeds, the return value is the handle to an I/O completion port:

  • If the ExistingCompletionPort parameter was NULL, the return value is a new handle.

  • If the ExistingCompletionPort parameter was a valid I/O completion port handle, the return value is that same handle.

  • If the FileHandle parameter was a valid handle, that file handle is now associated with the returned I/O completion port.

If the function fails, the return value is NULL. To get extended error information, call the GetLastError function.

如果返回是NULL,表示出错了。

 

创建一个IOCP

HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

绑定一个IOCP,第一个参数是你需要操作的socket,第二个参数是iocp的handle,第三个参数是iocp回调过来后会传递进来的参数,与创建线程类似,一般传递一个结构体指针,保存所有我们需要操作的数据,这里就是封装了一个class,里面有所有网络操作的数据,第四个参数会被忽略,默认设置为0.

if (NULL == CreateIoCompletionPort((HANDLE)psc->m_socket, hiocp, (ULONG_PTR)psc, 0))
{
    //error       
}

 

 

5.2 创建socket

创建完IOCP之后,需要创建socket用于监听连接

SOCKET WSAAPI WSASocketW(
  int                 af,
  int                 type,
  int                 protocol,
  LPWSAPROTOCOL_INFOW lpProtocolInfo,
  GROUP               g,
  DWORD               dwFlags
);

WSASocketA不推荐使用了,所以用WSASocketW,对程序没任何影响。

Parameters

af

The address family specification. Possible values for the address family are defined in the Winsock2.h header file.

On the Windows SDK released for Windows Vista and later, the organization of header files has changed and the possible values for the address family are defined in the Ws2def.h header file. Note that the Ws2def.h header file is automatically included in Winsock2.h, and should never be used directly.

The values currently supported are AF_INET or AF_INET6, which are the Internet address family formats for IPv4 and IPv6. Other options for address family (AF_NETBIOS for use with NetBIOS, for example) are supported if a Windows Sockets service provider for the address family is installed. Note that the values for the AF_ address family and PF_ protocol family constants are identical (for example, AF_INET and PF_INET), so either constant can be used.

创建socket的协议,正常都是AF_INET,其他的可以去了解一下。

type

The type specification for the new socket.

Possible values for the socket type are defined in the Winsock2.h header file.

socket的类型,正常都是SOCK_STREAM,流类型,就是TCP。

protocol

The protocol to be used. The possible options for the protocol parameter are specific to the address family and socket type specified. Possible values for the protocol are defined are defined in the Winsock2.h and Wsrm.h header files.

On the Windows SDK released for Windows Vista and later,, the organization of header files has changed and this parameter can be one of the values from the IPPROTO enumeration type defined in the Ws2def.h header file. Note that the Ws2def.h header file is automatically included in Winsock2.h, and should never be used directly.

If a value of 0 is specified, the caller does not wish to specify a protocol and the service provider will choose the protocol to use.

When the af parameter is AF_INET or AF_INET6 and the type is SOCK_RAW, the value specified for the protocol is set in the protocol field of the IPv6 or IPv4 packet header.

设置协议的参数,指定用什么协议,设置为0默认即可。

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.

对socket更详细的设置,设置为nullptr就好了。

g

An existing socket group ID or an appropriate action to take when creating a new socket and a new socket group.

设置socket的组,为0,默认即可。

dwFlags

A set of flags used to specify additional socket attributes.

设置socket的额外属性,这里要填WSA_FLAG_OVERLAPPED。

Return Value

If no error occurs, WSASocket returns a descriptor referencing the new socket. Otherwise, a value of INVALID_SOCKET is returned, and a specific error code can be retrieved by calling WSAGetLastError.

成功返回一个socket,失败返回INVALID_SOCKET。

WSASocketW(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

 

 

5.3 接收

int WSAAPI WSARecv(
  SOCKET                             s,
  LPWSABUF                           lpBuffers,
  DWORD                              dwBufferCount,
  LPDWORD                            lpNumberOfBytesRecvd,
  LPDWORD                            lpFlags,
  LPWSAOVERLAPPED                    lpOverlapped,
  LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

向IOCP投递一个接收请求,如果有数据到达,IOCP就会自动回调你的函数。不投递是不会主动回调的。

Parameters

s

A descriptor identifying a connected socket.

需要投递接收数据请求的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.

数据接收到哪里,这是一个指针,告诉IOCP把数据存到里面。

dwBufferCount

The number of WSABUF structures in the lpBuffers array.

这里是指上面的参数中有多少个buff,一般上面是一个,这里是1。

lpNumberOfBytesRecvd

A pointer to the number, in bytes, of data received by this call if the receive operation completes immediately.

Use NULL for this parameter if the lpOverlapped parameter is not NULL to avoid potentially erroneous results. This parameter can be NULL only if the lpOverlapped parameter is not NULL.

如果是立即返回的话,接收到多少数据,一般没有作用。

lpFlags

A pointer to flags used to modify the behavior of the WSARecv function call. For more information, see the Remarks section.

设置接收行为的flag,默认是0就可以,不去设置。

lpOverlapped

A pointer to a WSAOVERLAPPED structure (ignored for nonoverlapped sockets).

这次投递接收数据的请求,必须要传递一个参数,用来回调。每一个socket对应一个overlapped变量。我们创建socket的时候也指定了WSA_FLAG_OVERLAPPED参数,因为我们把数据投递到系统后,就等待回调,系统需要知道是哪一个socket,这个用来内核调用,我们不需要处理。

lpCompletionRoutine

A pointer to the completion routine called when the receive operation has been completed (ignored for nonoverlapped sockets).

设置为NULL即可。

if (WSARecv(
        m_iosocket,
        &m_wsabuf,
        1,
        &dwbytes,
        &dwflags,
        &m_overlapped,
        NULL) == SOCKET_ERROR)
    {
        int err = WSAGetLastError();
        if (WSA_IO_PENDING != err)
        {
            ret = false;
        }
    }

 

 

5.4 发送

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

Parameters

  • s
    [in] Descriptor identifying a connected socket.
    需要发送数据的socket。
  • lpBuffers
    [in] Pointer to an array of WSABUF structures. Each WSABUF structure contains a pointer to a buffer and the length of the buffer. This array must remain valid for the duration of the send operation.
    需要发送的数据缓冲。
  • dwBufferCount
    [in] Number of WSABUF structures in the lpBuffers array.
    上面缓冲buff的个数,一般是一个。
  • lpNumberOfBytesSent
    [out] Pointer to the number of bytes sent by this call if the I/O operation completes immediately.
    如果发送立马结束,一共发送了多少字节。一般没有作用。
  • dwFlags
    [in] Flags used to modify the behavior of the WSASend function call
    修改发送函数行为的标志,不用设置。
  • lpOverlapped
    [in] Pointer to a WSAOVERLAPPED structure. This parameter is ignored for nonoverlapped sockets.
    socket对应的overlapped。
  • lpCompletionRoutine
    [in] Pointer to the completion routine called when the send operation has been completed. This parameter is ignored for nonoverlapped sockets.
    设置为NULL。

Return Value

If no error occurs and the send operation has completed immediately, this function returns zero. The overlapped structures are updated with the receive results, and the associated event is signalled.

if (WSASend(
        m_iosocket,
        &m_wsabuf,
        1,
        &dwbytes,
        dwflags,
        &m_overlapped,
        NULL) == SOCKET_ERROR)
    {
        int err = WSAGetLastError();
        if (err != WSA_IO_PENDING)
        {
            ret = false;
        }
    }

 

 

5.5 WSAOVERLAPPED structure

typedef struct _WSAOVERLAPPED {
  DWORD    Internal;
  DWORD    InternalHigh;
  DWORD    Offset;
  DWORD    OffsetHigh;
  WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;

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.

这个overlapped结构,提供了初始化的I/O操作与后续的其他发送接收直接的一个关联。

Members

Internal

Type: ULONG_PTR

Reserved for internal use. The Internal member is used internally by the entity that implements overlapped I/O. For service providers that create sockets as installable file system (IFS) handles, this parameter is used by the underlying operating system. Other service providers (non-IFS providers) are free to use this parameter as necessary.

InternalHigh

Type: ULONG_PTR

Reserved. Used internally by the entity that implements overlapped I/O. For service providers that create sockets as IFS handles, this parameter is used by the underlying operating system. NonIFS providers are free to use this parameter as necessary.

Offset

Type: DWORD

Reserved for use by service providers.

OffsetHigh

Type: DWORD

Reserved for use by service providers.

hEvent

Type: HANDLE

If an overlapped I/O operation is issued without an I/O completion routine (the operation's lpCompletionRoutine parameter is set to null), then this parameter should either contain a valid handle to a WSAEVENT object or be null. If the lpCompletionRoutine parameter of the call is non-null then applications are free to use this parameter as necessary.

这个参数我们不用管,只需要记住,每次操作之前,需要初始化一下。也就是接受,发送等socket操作之前,需要重置一下。

 

 

5. 7 AcceptEx

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

既然用了IOCP,那么我们就尽量用微软提供的一套工具。这里微软提供了AcceptEx代替accept。AcceptEx的好处就是,与IOCP接收发送数据一样的模型,投递,在线程等待连接。提前申请一批接收的socket,连接成功的时候,这个socket就可以直接用了。可以设置第一次数据接收到之后AcceptEx才返回,减少操作次数。

不过对于提前申请接收的socket,感觉上并没有减少接收的时候的操作时间。accept是接收到请求,创建socket,AcceptEx是提前申请,接收到之后,socket赋值也好了,但是还是需要创建一个socket进一步投递,等待下一次连接。

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.

 用于监听的socket。

sAcceptSocket

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

用于接收的socket。

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.

用于接收socket第一个数据,并且会接收socket的网络信息。数据放到起始位置,地址信息放到数据后面。

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.

用于接收数据的buff长度。不能包含地址信息的长度。如果是0,函数立马返回,并不等待接收第一个数据,可以用来避免连接不发送数据的攻击。

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.

用于接收本地地址信息的长度,最少比最大的地址空间多16位。

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.

用于接收远程地址信息的长度,最少比最大的地址空间多16位。

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.

与接收socket对应的overlapped结构体。

Return Value

If no error occurs, the AcceptEx function completed successfully and a value of TRUE is returned.

 如果投递接收请求成功后,就返回TRUE。

Remarks

The AcceptEx function combines several socket functions into a single API/kernel transition. The AcceptEx function, when successful, performs three tasks:

AcceptEx把多个功能绑定到了一起,当这个函数执行成功的时候,就完成了三个任务:

  • A new connection is accepted.
  • 一个新的连接已经到达
  • Both the local and remote addresses for the connection are returned.
  • 本地和远程的地址已经获得并返回
  • The first block of data sent by the remote is received.
  • 第一个数据已经接收
 
Note  The function pointer for the AcceptEx function must be obtained at run time by making a call to the WSAIoctl function with the SIO_GET_EXTENSION_FUNCTION_POINTER opcode specified. The input buffer passed to the WSAIoctl function must contain WSAID_ACCEPTEX, a globally unique identifier (GUID) whose value identifies the AcceptEx extension function. On success, the output returned by the WSAIoctl function contains a pointer to the AcceptEx function. The WSAID_ACCEPTEX GUID is defined in the Mswsock.h header file.
 这个函数每次调用都需要额外的库加载,所以为了性能,建议初始化时,获取到函数指针并保存,以后直接使用。

A program can make a connection to a socket more quickly using AcceptEx instead of the accept function.

A single output buffer receives the data, the local socket address (the server), and the remote socket address (the client).

Using a single buffer improves performance. When using AcceptEx, the GetAcceptExSockaddrs function must be called to parse the buffer into its three distinct parts (data, local socket address, and remote socket address). On Windows XP and later, once the AcceptEx function completes and the SO_UPDATE_ACCEPT_CONTEXT option is set on the accepted socket, the local address associated with the accepted socket can also be retrieved using the getsockname function. Likewise, the remote address associated with the accepted socket can be retrieved using the getpeername function.

可以通过GetAcceptExSockaddrs这个函数解析第一个数据包,把数据,本地地址信息,远程地址信息解析出来。

The buffer size for the local and remote address must be 16 bytes more than the size of the sockaddr structure for the transport protocol in use because the addresses are written in an internal format. For example, the size of a sockaddr_in (the address structure for TCP/IP) is 16 bytes. Therefore, a buffer size of at least 32 bytes must be specified for the local and remote addresses.

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.

AcceptEx函数与accept的一个很大的区别是AcceptEx调用的时候,就需要把接收的socket创建好,accept是接收到请求再创建一个对应的socket。这样可以增加效率,减少客户端连接等待的时间。

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

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.

使用AcceptEx时,如果设置了dwReceiveDataLength,会导致有被攻击的风险,就是连接了请求,但是并没有发送第一个数据,这样监听的socket就被占用了。可以是用getsockopt获取连接之后过了多久时间,来确实什么时候超时。

        if (false == AcceptEx(
            m_listensocket,
            m_iosocket,
            m_wsabuf.buf,
            m_wsabuf.len - (sizeof(SOCKADDR_IN) + 16) * 2,
            sizeof(SOCKADDR_IN) + 16,
            sizeof(SOCKADDR_IN) + 16,
            &dwbytes,
            &m_overlapped
            ))
        {
            if (WSA_IO_PENDING != WSAGetLastError())
            {
                ret = false;
            }
        }

 

 

5.8 Function Extension Mechanism in the SPI

Because the Winsock DLL itself is no longer supplied by each individual stack vendor, it is not possible for a stack vendor to offer extended functionality by adding entry points to the Winsock DLL. To overcome this limitation, Winsock takes advantage of the new WSAIoctl function to accommodate service providers who wish to offer provider-specific functionality extensions. This mechanism presupposes that an application is aware of a particular extension and understands both the semantics and syntax involved. Such information would typically be supplied by the service provider vendor.

To invoke an extension function, the application must first ask for a pointer to the desired function. This is done through the WSAIoctl function using the SIO_GET_EXTENSION_FUNCTION_POINTER command code. The input buffer to the WSAIoctl function contains an identifier for the desired extension function and the output buffer will contain the function pointer itself. The application can then invoke the extension function directly without passing through the Ws2_32.dll.

The identifiers assigned to extension functions are globally unique identifiers (GUIDs) that are allocated by service provider vendors. Vendors who create extension functions are urged to publish full details about the function including the syntax of the function prototype. This facilitates common and/or popular extension functions to be offered by multiple service providers. An application can obtain the function pointer and use the function without needing to know anything about the particular service provider that implements the function.

这里说明了,因为一些限制,导致有些函数不能放到Winsock DLL中了,所以新增加了一个DLL(Ws2_32.dll),AcceptEx就属于这一类,所以要想使用AcceptEX,就需要解析Ws2_32.dll库,获取对应的模块。这里建议一次性获取,保存下来使用。不然的话每次读取解析动态库,浪费资源。

 

 

5.9 WSAIoctl function

The WSAIoctl function controls the mode of a socket.

这个函数是用来控制socket的,这里还可以用来获取我们需要的函数。

int WSAAPI WSAIoctl(
  SOCKET                             s,
  DWORD                              dwIoControlCode,
  LPVOID                             lpvInBuffer,
  DWORD                              cbInBuffer,
  LPVOID                             lpvOutBuffer,
  DWORD                              cbOutBuffer,
  LPDWORD                            lpcbBytesReturned,
  LPWSAOVERLAPPED                    lpOverlapped,
  LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

Parameters

s

A descriptor identifying a socket.

要控制的socket,这里随便写一个就好了,因为我们不需要控制。

dwIoControlCode

The control code of operation to perform.

要控制的code,填写SIO_GET_EXTENSION_FUNCTION_POINTER

lpvInBuffer

A pointer to the input buffer.

传入的参数,获取AcceptEx,就传入(WSAID_ACCEPTEX)。

cbInBuffer

The size, in bytes, of the input buffer.

传入参数的长度。

lpvOutBuffer

A pointer to the output buffer.

输出的buffer,就是我们的函数指针。AcceptEx的函数指针模型是(LPFN_ACCEPTEX)。

cbOutBuffer

The size, in bytes, of the output buffer.

输出的buffer的长度。

lpcbBytesReturned

A pointer to actual number of bytes of output.

实际输出数据的长度。

lpOverlapped

A pointer to a WSAOVERLAPPED structure (ignored for non-overlapped sockets).

lpCompletionRoutine

LPFN_ACCEPTEX acceptExF;
GUID guidacceptex = WSAID_ACCEPTEX;
DWORD dwbytes = 0;
        if (SOCKET_ERROR == WSAIoctl(
            m_listensocket,
            SIO_GET_EXTENSION_FUNCTION_POINTER,
            &guidacceptex,
            sizeof(guidacceptex),
            &acceptExF,
            sizeof(acceptExF),
            &dwbytes,
            NULL,
            NULL
            ))
        {
            ret = false;
        }

这是获取AcceptEx的方法,后面直接使用acceptExF代替就可以。

 

 

5.10 GetAcceptExSockaddrs function

The GetAcceptExSockaddrs function parses the data obtained from a call to the AcceptEx function and passes the local and remote addresses to a sockaddr structure.

这个函数就是用来解析AcceptEx获取的第一个数据,包含地址信息。它可以把数据和地址信息解析出来。

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

这个函数与AcceptEx一样,是Windows扩展的实现,需要上面的方法获取函数指针。

void GetAcceptExSockaddrs(
  PVOID    lpOutputBuffer,
  DWORD    dwReceiveDataLength,
  DWORD    dwLocalAddressLength,
  DWORD    dwRemoteAddressLength,
  sockaddr **LocalSockaddr,
  LPINT    LocalSockaddrLength,
  sockaddr **RemoteSockaddr,
  LPINT    RemoteSockaddrLength
);

Parameters

lpOutputBuffer

A pointer to a buffer that receives the first block of data sent on a connection resulting from an AcceptEx call. Must be the same lpOutputBuffer parameter that was passed to the AcceptEx function.

AcceptEx调用的时候传入的buff指针,必须是AcceptEx传入的buff,因为第一块数据与地址信息都放在这个里面,这个函数就是解析这一块数据,如果传入的不是AcceptEx的buff,会有无法预测的问题。

dwReceiveDataLength

The number of bytes in the buffer used for receiving the first data. This value must be equal to the dwReceiveDataLength parameter that was passed to the AcceptEx function.

这个是AcceptEx传入的用来接收数据的长度,必须与AcceptEx传入的数据一样。这样函数才能正确解析数据,而不会解析出错。

dwLocalAddressLength

The number of bytes reserved for the local address information. This value must be equal to the dwLocalAddressLength parameter that was passed to the AcceptEx function.

用来接收本地地址信息的长度,必须与AcceptEx传入的一样。

dwRemoteAddressLength

The number of bytes reserved for the remote address information. This value must be equal to the dwRemoteAddressLength parameter that was passed to the AcceptEx function.

用来接收远程地址信息的长度,必须与AcceptEx传入的一样。

LocalSockaddr

A pointer to the sockaddr structure that receives the local address of the connection (the same information that would be returned by the getsockname function). This parameter must be specified.

传入一个用来接收本地地址信息指针的指针,也就是我们不需要申请空间,声明一个指针,然后把这个指针变量的指针传递过去,系统创建赋值。

LocalSockaddrLength

The size, in bytes, of the local address. This parameter must be specified.

返回获取的系统地址信息的长度。

RemoteSockaddr

A pointer to the sockaddr structure that receives the remote address of the connection (the same information that would be returned by the getpeername function). This parameter must be specified.

获得远程地址信息的指针,与上面一样。

RemoteSockaddrLength

The size, in bytes, of the local address. This parameter must be specified.

获取远程地址信息的长度。

Remarks

The GetAcceptExSockaddrs function is used exclusively with the AcceptEx function to parse the first data that the socket receives into local and remote addresses. The AcceptEx function returns local and remote address information in an internal format. Application developers need to use the GetAcceptExSockaddrs function if there is a need for the sockaddr structures containing the local or remote addresses.

Note  The function pointer for the GetAcceptExSockaddrs function must be obtained at run time by making a call to the WSAIoctl function with the SIO_GET_EXTENSION_FUNCTION_POINTER opcode specified. The input buffer passed to the WSAIoctl function must contain WSAID_GETACCEPTEXSOCKADDRS, a globally unique identifier (GUID) whose value identifies the GetAcceptExSockaddrs extension function. On success, the output returned by the WSAIoctl function contains a pointer to the GetAcceptExSockaddrs function. The WSAID_GETACCEPTEXSOCKADDRS GUID is defined in the Mswsock.h header file.

 

 

5.11 GetQueuedCompletionStatus function

Attempts to dequeue an I/O completion packet from the specified I/O completion port. If there is no completion packet queued, the function waits for a pending I/O operation associated with the completion port to complete.

To dequeue multiple I/O completion packets at once, use the GetQueuedCompletionStatusEx function.

试图从IOCP队列中取出一个消息包。如果没有,则阻塞在这里等待。如果像一次性获取当前的所有数据,可以使用GetQueuedCompletionStatusEx。

BOOL GetQueuedCompletionStatus(
  HANDLE       CompletionPort,
  LPDWORD      lpNumberOfBytesTransferred,
  PULONG_PTR   lpCompletionKey,
  LPOVERLAPPED *lpOverlapped,
  DWORD        dwMilliseconds
);

Parameters

CompletionPort

A handle to the completion port. To create a completion port, use the CreateIoCompletionPort function.

监听获取数据的IOCP,就是我们原来创建的那一个。

lpNumberOfBytesTransferred

TBD

获取了多少数据。

lpCompletionKey

A pointer to a variable that receives the completion key value associated with the file handle whose I/O operation has completed. A completion key is a per-file key that is specified in a call to CreateIoCompletionPort.

调用CreateIoCompletionPort传递的key,一般是一个结构体的指针。

lpOverlapped

A pointer to a variable that receives the address of the OVERLAPPED structure that was specified when the completed I/O operation was started.

与这次数据请求返回对应的overlapped结构体。

Even if you have passed the function a file handle associated with a completion port and a valid OVERLAPPED structure, an application can prevent completion port notification. This is done by specifying a valid event handle for the hEvent member of the OVERLAPPED structure, and setting its low-order bit. A valid event handle whose low-order bit is set keeps I/O completion from being queued to the completion port.

dwMilliseconds

The number of milliseconds that the caller is willing to wait for a completion packet to appear at the completion port. If a completion packet does not appear within the specified time, the function times out, returns FALSE, and sets *lpOverlapped to NULL.

If dwMilliseconds is INFINITE, the function will never time out. If dwMilliseconds is zero and there is no I/O operation to dequeue, the function will time out immediately.

超时时间。

Return Value

Returns nonzero (TRUE) if successful or zero (FALSE) otherwise.

To get extended error information, call GetLastError.

For more information, see the Remarks section.

Remarks

This function associates a thread with the specified completion port. A thread can be associated with at most one completion port.

If a call to GetQueuedCompletionStatus fails because the completion port handle associated with it is closed while the call is outstanding, the function returns FALSE, *lpOverlapped will be NULL, and GetLastError will return ERROR_ABANDONED_WAIT_0.

Windows Server 2003 and Windows XP:  Closing the completion port handle while a call is outstanding will not result in the previously stated behavior. The function will continue to wait until an entry is removed from the port or until a time-out occurs, if specified as a value other than INFINITE.

If theGetQueuedCompletionStatus function succeeds, it dequeued a completion packet for a successful I/O operation from the completion port and has stored information in the variables pointed to by the following parameters: lpNumberOfBytes, lpCompletionKey, and lpOverlapped. Upon failure (the return value is FALSE), those same parameters can contain particular value combinations as follows:

  • If *lpOverlapped is NULL, the function did not dequeue a completion packet from the completion port. In this case, the function does not store information in the variables pointed to by the lpNumberOfBytes and lpCompletionKey parameters, and their values are indeterminate.
  • If *lpOverlapped is not NULL and the function dequeues a completion packet for a failed I/O operation from the completion port, the function stores information about the failed operation in the variables pointed to by lpNumberOfBytes, lpCompletionKey, and lpOverlapped. To get extended error information, call GetLastError.

 

 

5.12 CONTAINING_RECORD macro

The CONTAINING_RECORD macro returns the base address of an instance of a structure given the type of the structure and the address of a field within the containing structure.

根据结构体中一个成员的地址获取这个结构体的指针。

Syntax

PCHAR CONTAINING_RECORD(
  [in] PCHAR Address,
  [in] TYPE  Type,
  [in] PCHAR Field
);

Parameters

Address [in]

A pointer to a field in an instance of a structure of type Type.

这个变量的指针。

Type [in]

The name of the type of the structure whose base address is to be returned.

结构体的类型。

Field [in]

The name of the field pointed to by Address and which is contained in a structure of type Type.

第一个参数对应在结构体中的名字。

Return value

Returns the address of the base of the structure containing Field.

 

到这里,IOCP相关的函数介绍完了。下面介绍整体的流程和注意事项。

 

6. 流程

 

6.1 创建完成端口

hiocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

把这个句柄保存下来。

 

6.2 创建处理线程

::CreateThread(0, 0, WorkerThreadProc, (void*)this, 0, 0);

把当前类的指针传递过去,当收到消息后好处理,因为处理消息都是我们类里面的方法,这个线程指示用来接收数据,所以把当前类的指针传递过去,当收到数据时,进行处理。要注意多线程问题,WorkerThreadProc中与当前创建的地方不是同一个线程。

 

6.3 创建listen socket对应的类

listensocket = WSASocketW(AF_INET, SOCK_STREAM, 0, nullptr, 0, WSA_FLAG_OVERLAPPED);

每一个socket都对应一个类,这个类就表示一个客户端的连接。里面包含了所有相关的信息,socket,地址,接收消息列表,发送消息列表,处理消息函数等。上面是创建这个类对应的socket

 

6.4 把listen socket与IOCP绑定

if (NULL == CreateIoCompletionPort(
            (HANDLE)listensocket, hiocp, (ULONG_PTR)listenSocketContext, 0))
        {
            ret = false;
        }

然后把当前socket对应的类传递过去,因为接收发送消息,都是与当前socket有关系,所以肯定要传递过去,接收到数据后,好进一步处理。

 

6.5 初始化绑定listen

ZeroMemory(&serveraddr, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
        serveraddr.sin_port = htons(port);
        if (SOCKET_ERROR == ::bind(listensocket, (sockaddr *)&serveraddr, sizeof(serveraddr)))
        {
            ret = false;
        }

 

6.6 发起监听

if (SOCKET_ERROR == listen(listensocket, SOMAXCONN))
        {
            ret = false;
        }

 

6.7 获取AcceptEx与GetAcceptExSockaddrs句柄

 

6.8 投递一批基于listen socket的AcceptEx的请求

申请listen的iocontext,因为AcceptEx也是一个请求,IOCP的模式就是,所有的操作,接收连接,接收数据,发送数据,都是IO请求,都需要一个iocontext。

    DWORD dwbytes = 0;
    iotype = IOOPT_ACCEPT;
    iosocket = WSASocketW(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
    if (INVALID_SOCKET == iosocket)
    {
        ret = false;
    }
    if (ret)
    {
        if (false == acceptExFunc(
            listensocket,
            iosocket,
            wsabuf.buf,
            wsabuf.len - (sizeof(SOCKADDR_IN) + 16) * 2,
            sizeof(SOCKADDR_IN) + 16,
            sizeof(SOCKADDR_IN) + 16,
            &dwbytes,
            &overlapped
            ))
        {
            if (WSA_IO_PENDING != WSAGetLastError())
            {
                ret = false;
            }
        }
    }

 

 

6.9 线程处理

bool bret = GetQueuedCompletionStatus(
            iocp,
            &dwbytes,
            (PULONG_PTR)&socketcontext,
            &ol,
            INFINITE
        );

 

 

6.10 获取每一次IO操作的结构体

每一次socket的操作都是向IOCP投递一个IO请求,这个请求必须传递一个overlapped,每次GetQueuedCompletionStatus返回的时候,会把这个overlapped传递过来,它的作用对于我们来说,就是获取这次IO操作传递的数据。

iocontext = CONTAINING_RECORD(ol, IOContext, overlapped);

每次IO投递的时候,我们都会设置这次投递的是什么请求的状态。这里接收过来后,通过状态判断是请求,接收还是发送,进行下一步处理。

 

6.11 AcceptEx请求处理

  • 获取AcceptEx返回的地址信息和数据
getAcceptExSockFunc(
        iocxt->m_wsabuf.buf,
        iocxt->m_wsabuf.len - ((sizeof(SOCKADDR_IN) + 16) * 2),
        sizeof(SOCKADDR_IN) + 16,
        sizeof(SOCKADDR_IN) + 16,
        (LPSOCKADDR*)&localaddr,
        &localaddrlen,
        (LPSOCKADDR*)&clientaddr,
        &clientaddrlen
    );
  • 把获取的socket信息赋值到一个socketcontext,这就是一个新的连接。
  • 把获取的数据发送到这个socketcontext类的消息处理列表中。
  • 把这个socket与IOCP绑定,这样就可以通过IOCP收发消息了。
  • 对于这个socketcontext投递一个接收数据的iocontext。
  • 把这次使用的用来监听赋值的socketcontext(注意不是listen监听的socket,是AcceptEx调用时,传递进去的一个socket),重新初始化,创建socket投递。因为原来的socket已经被使用并赋值到其他socketcontext了。

 

6.12 接收数据

如果是接收数据,我们就把iocontext放到消息处理列表中,把数据取出,处理,然后再初始化,投递新的接收数据的请求。

 

7. 结尾

剩下的就是接收数据,处理数据,发送数据,再投递新的接收请求。

posted @ 2019-07-11 13:01  秋来叶黄  阅读(787)  评论(0编辑  收藏  举报