IOCP使用小结

        很多知识,只有自己亲自实践了才知道坑在哪里。
socket是内核对象句柄,每次对socket执行操作,需要用户对象到内核对象的转换,执行完成返回结果,需要内核对象到用户对象的转换。
IOCP的中文名称是完成端口,目前是Windows下最高效的网络模型。特点:半异步,非阻塞。(我理解的完全异步是回调式,不需要人工参与,但是IOCP的异步需要轮询)。

        其他模型的缺点:

1)select模型:最低效,每次检索对长度有限制(默认是64个链接),可以通过修改头文件的方式修改上限,需要手动循环查询是否有操作可执行,所以很低效;

2)WSAEvent,事件模型,缺点也是有上限,每次最多监听64个事件,在收到事件通知后,去手动recv数据,效率比select高许多,因为操作是系统消息通知的,可以实现异步;
3)完成例程模型,是对事件模型的改进,去掉了64个事件的上限
以上模型还有个缺点,就是每次有操作可执行时,需要手动去执行recv或者accept等操作,涉及到内核对象<->用户对象的两次切换(订制获取消息时一次,recv/accept操作一次),而且对于accept来说,每次手动调用,都会产生一个socket,当大量accept来到时,产生socket的过程会非常耗时。
        知道其他模型的缺点,就知道了完成端口的优点:1)没有监听上限;2)对于accept来说,socket是提前建立准备好的,收到连接时直接返回之前传入的socket;3)只涉及到一次内核对象<->用户对象切换(订制消息时一次),因为在订制消息的时候,已经把数据缓存地址给了内核对象,内核对象在收到数据、写入缓存后,才切换回用户对象,让用户拿走数据。总的来说,完成端口是proactor模型,其他的是reactor模型。
下面是完成端口的基本步骤:
0)头文件:winsock2.h 
加载库:ws2_32.lib

初始化网络环境:WSAStartup

1)创建完成端口

HANDLE m_hIO = CreateIoCompletionPort ( INVALID_HANDLE_VALUE, 0, 0, 0 );

说明:①一般来说全局一个完成端口就够用,可以参考boost.asio在Windows下的实现,采用的就是完成端口,全局只需要一个io_server,而且是proactor模式。

2)初始化socket

SOCKET sokListen = WSASocket ( AF_INET, SOCK_STREAM, 0, nullptr, 0, WSA_FLAG_OVERLAPPED );

SOCKET sockAccept = WSASocket ( AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED );

说明:①必须用WSASocket来初始化socket,因为只有它有参数设定socket是重叠端口

②sokListen:监听socket,sockAccept:预定义的连接成功socket,可以根据需要预定义几万个或者SOMAXCONN个(猜想)

3)把socket和完成端口绑定

class PER_IO_CONTEXT{
public:
OVERLAPPED ol;
SOCKET sokAccept;
WSABUF wsaBuf;
char buf[4096];
int len = 4096;
OPERATION_TYPE opType = NULL_POSTED;
PER_IO_CONTEXT ( )
{
ZeroMemory ( &ol, sizeof ( OVERLAPPED ) );
ZeroMemory ( buf, 4096 );
wsaBuf.buf = buf;
wsaBuf.len = 4096;
}
virtual void t ( ){};};

PER_IO_CONTEXT *perIOCtx = new PER_IO_CONTEXT;
CreateIoCompletionPort ( (HANDLE)sockListen, m_hIO, (ULONG_PTR)perIOCtx, 0 );

 说明:①PER_IO_CONTEXT是自定义数据结构,用来和socket绑定,作用类似于创建线程时传入的用户参数,在获取完成端口数据时会传入进来,所以可以在该结构体中添加数据缓存、socket标识、操作类型等关键信息;

②绑定端口的API和创建完成端口一样,区别是传入的参数不一样,绑定时第一个参数是要绑定的socket,第二个是完成端口实例,第三个是绑定socket的数据。

4)绑定端口,开始监听

sockaddr_in addr{ 0 };
addr.sin_family = AF_INET;
addr.sin_port = htons ( 11111 );
addr.sin_addr.S_un.S_addr = INADDR_ANY;
sockListen = WSASocket ( AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED );
::bind ( sockListen, (sockaddr*)&addr, sizeof ( addr ) );
::listen ( sockListen , SOMAXCONN);

5)获取WSAAcceptEx函数,并执行

LPFN_ACCEPTEX acceptex;
GUID guidAcceptex = WSAID_ACCEPTEX;
DWORD dwBytes = 0;
WSAIoctl ( sockListen, SIO_GET_EXTENSION_FUNCTION_POINTER, &guidAcceptex, sizeof ( guidAcceptex ), &acceptex,
sizeof ( acceptex ), &dwBytes, nullptr, nullptr );
SOCKET sockAccept = WSASocket ( AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED );
printf ( "%d ==\n", WSAGetLastError ( ) );
DWORD len = 0;
perIOCtx->sokAccept = sockAccept; 
// acceptex ( sockListen, sockAccept, buf, 4096 - sizeof ( sockaddr_in ) * 2, sizeof ( sockaddr_in ),
// sizeof ( sockaddr_in ), &len, (LPOVERLAPPED)&perIOCtx);
ZeroMemory ( &perIOCtx->ol, sizeof ( OVERLAPPED ) );
acceptex ( sockListen, sockAccept, perIOCtx->buf, 0/*perIOCtx->len - 2*(sizeof(sockaddr_in)+16)*/, 
sizeof ( sockaddr_in ) + 16, sizeof ( sockaddr_in ) + 16, &len, &perIOCtx->ol );//

说明:①WSAAcceptEx函数作用是投递accept操作到完成端口内核,只有该函数可以完成此功能 

②WSAAcceptEx是扩展函数,本身没有定义在winsock2中,所以需要类似动态加载的方式获取函数指针,Windows提供了函数WSAIoctl和参数SIO_GET_EXTENSION_FUNCTION_POINTER来获取函数指针,宏WSAID_ACCEPTEX是该函数对应的GUID号,完成端口的其他几个功能函数也是用这种方式取得,WSAloctl的第一个参数是一个有效socket,不是特定socket。

③acceptex 参数说明:1)监听socket;2)预先定义的连接socket,等同于其他模式accept函数返回的socket,但是这里的socket是提前申请好的,所以在高并发连接时不会有新建socket的开销,提供了性能;3)该参数为0,表示连接即返回,不为0时传入一个缓存,连接并收到第一份数据时返回,缓存内是收到的数据,高并发时设置缓存,可以等到真实数据发送过来时返回,降低完成端口处理的并发数;4)缓存长度,因为收到数据时,会把远端地址信息和本地地址信息写入缓存,所以长度要减去两个地址信息的长度,也即参数5)+6)的和;5)和6):远端地址、本地地址信息长度,16是预留的16个字节,目前内核没有用到,但是空间要保留,所以必须加16;7)实际接收的数据,没用;8)很关键,后文详细说明(见注①)

6)此时监听socket的accept操作就被投递给了完成端口内核,接下来就是不断询问完成端口是否有操作抵达

DWORD ret = 0;
PER_IO_CONTEXT *pc = NULL;
OVERLAPPED *ov = NULL;
while (1)
{
if (GetQueuedCompletionStatus ( m_hIO, &ret, (PULONG_PTR)&pc, &ov, INFINITE ))

……

说明:①参数说明:①全局完成端口对象;②返回数据长度,用不大;③返回用户数据,就是监听socket和完成端口对象绑定时传入的用户数据结构体指针;④返回操作对应的数据,就是WSAAcceptex第八个参数,或者WSARecv最后一个参数;⑤超时时间,INFINITE代表永不超时一直等待直到有可用操作到达为止。

7)对对应操作进行响应:从6)我们可以看到,完成端口并不会告诉你当前返回的是哪个操作,所以在投递操作前,应该把操作类型放在用户数据里面,在操作返回时进行响应,具体响应过程按业务需要,比如可以定义初始操作、ACCEPT操作、RECV操作枚举,然后赋值到结构体属性,如PER_IO_CONTEXT的枚举属性OPERATION_TYPE 。

8)响应accept时,要注意,每次响应时,都要重新投递accept,否则监听socket不会再接收连接请求

9)响应recv时,也要处理完数据后,调用WSARecv,再次投递recv请求。

DWORD rlen = 0;
WSAOVERLAPPED oovv{ 0 };
DWORD dwFlags = 0;
WSARecv ( pc->sokAccept, &newpc->wsaBuf, 1, &rlen, &dwFlags, &oovv, nullptr );

说明:①一定要用WSARecv,而不是WSARecvEx,后者作用和原来的recv函数一样。

②WSARecv参数说明:①要接收数据的socket;②数据保存的WSABuf结构体,可以有多个组成数组,写入数据时会按顺序写入;③WSABuf的个数,即参数②的个数;④返回接收的数据长度;⑤标志位,没大用;⑥和接收socket绑定的用户数据,和Accepte中最后一个参数一样;⑦回调函数,完成实例模式的关键,完成端口没用。

10)如果对GetQueuedCompletionStatus设定了一直等待,线程会被阻塞,在需要退出的时候,可以通过调用函数PostQueuedCompletionStatus,手动发一个操作,让它响应,在具体响应代码中退出,函数原型:

BOOL PostQueuedCompletionStatus(HANDLE CompletlonPort,DW0RD dwNumberOfBytesTrlansferred,DWORD dwCompletlonKey,LPOVERLAPPED lpoverlapped);
11)退出后,一定要关闭socket,关闭完成端口:CancelIo(m_hIO).
12)一个有用的获取远端、本地地址的函数:
获取办法:
WSAIoctl(m_pListenContext->m_Socket, 
SIO_GET_EXTENSION_FUNCTION_POINTER, 
&GuidGetAcceptExSockAddrs,
sizeof(GuidGetAcceptExSockAddrs), 
&m_lpfnGetAcceptExSockAddrs, 
sizeof(m_lpfnGetAcceptExSockAddrs),  
&dwBytes, 
NULL, 
NULL)) ;

函数原型:

VOID
(PASCAL FAR * LPFN_GETACCEPTEXSOCKADDRS)(
    _In_reads_bytes_(dwReceiveDataLength+dwLocalAddressLength+dwRemoteAddressLength) PVOID lpOutputBuffer,//数据缓存
    _In_ DWORD dwReceiveDataLength,//缓存长度-(地址+16)*2
    _In_ DWORD dwLocalAddressLength,//本地地址长度+16
    _In_ DWORD dwRemoteAddressLength,//远程地址长度+16
    _Outptr_result_bytebuffer_(*LocalSockaddrLength) struct sockaddr **LocalSockaddr,//本地地址
    _Out_ LPINT LocalSockaddrLength,//获取的本地地址长度
    _Outptr_result_bytebuffer_(*RemoteSockaddrLength) struct sockaddr **RemoteSockaddr,//远程地址
    _Out_ LPINT RemoteSockaddrLength);//获取的远程地址长度

注①: 这个参数要的是OVERLAPPED指针,实际可以传入用户数据,只要用户数据内还有OVERLAPPED类型的属性,并把OVERLAPPED属性地址在这里传入即可。在完成端口内核中,这个指针的内容会被赋值,并在返回完成端口操作时返回给用户,所以这个参数可以绑定关键的用户数据,比如和前面传入的预定义的接收返回socket绑定。我这里传入的就是之前定义的 PER_IO_CONTEXT 结构体。有很多网文说这个结构体第一个参数必须是OVERLAPPED类型,这是不准确的,因为这个结构体内其他地址,完成端口是不会动的,所以只要在 GetQueuedCompletionStatus取得OVERLAPPED的地址,就能推导出原结构体的起始地址,然后取出关键数据。Windows提供了推导起始地址的宏: CONTAINING_RECORD(address, type, field),第一个参数是已知地址,这里是 GetQueuedCompletionStatus返回的OVERLAPPED的地址,第二个参数是要获取的结构体类型,这里是 PER_IO_CONTEXT ,第三个参数是第一个参数对应的结构体中属性的名称,这里是ol,举例: CONTAINING_RECORD ( ov, PER_IO_CONTEXT, ol),ov是 GetQueuedCompletionStatus返回的OVERLAPPED的地址。在向 acceptex 传入OVERLAPPED指针前,这个结构体一定要 初始化 ,可以初始化为0,否则会报错。
注②:UDP也可以使用IOCP,只要使用对应的WSASendTo和WSARecvFrom即可。组播时,发送端可以不绑定端口,直接发往组播IP,接收端必须绑定端口,实现和组播端口的映射,才能接收数据。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值