IOCP以其高效的性能受到服务器开发者的青睐,本人有幸在当前的项目中使用了该异步模型,修改调试之余,总结出开发过程中的经验若干,供大家借鉴。
首先是需要注意的是OVERLAPPED结构。想必该结构大多数人都是自定义新的结构体,将OVERLAPPED成员放置在第一位,然后后置其他成员。
在函数 WSASend, WSARecv, PostQueuedCompletionStatus 以及GetQueuedCompletionStatus 中都有LPOVERLAPPED的参数,其中在前面三个函数中是输入参数,后面一个函数中是输出参数。对于输入参数可以传入强制转换的自定义结构体指针(不需要取地址),也可以传入自定义结构体中OVERLAPPED成员的地址(需要取地址);对于输出参数,将要传出的是前三个函数中输入参数的地址。在开发过程中,对于该结构地址的操作需要细心。
形如以下的代码都是正确的:
WSASend(pClientData->IoSocket, &(pPerIoData->WsaSendDataBuff), 1, &dwSendBytes, 0, (LPOVERLAPPED)pPerIoData, NULL);
WSASend(pClientData->IoSocket, &(pPerIoData->WsaSendDataBuff), 1, &dwSendBytes, 0, &(pPerIoData->overlaped), NULL);
GetQueuedCompletionStatus(pThis->m_hIOCP, &dwBytesTransferred,(LPDWORD)&pPerHandleData, (LPOVERLAPPED *)&pPerIoData, INFINITE);
其次需要注意的是PostQueuedCompletionStatus 函数。该函数向IOCP发送三个参数(DWORD dwNumberOfBytesTransferred, ULONG_PTR dwCompletionKey, LPOVERLAPPED lpOverlapped),GetQueuedCompletionStatus 函数将接收到这三个参数。IOCP将不会对这三个参数做任何操作。
在实际应用中,该函数一般用于控制IOCP接收线程的退出。其实,该函数的用法远不止于此,它还可以作为消息来使用。通过定义特定的dwNumberOfBytesTransferred消息值,然后通过PostQueuedCompletionStatus函数向IOCP中POST该消息,GetQueuedCompletionStatus 函数就可以捕获该消息。自定义的dwNumberOfBytesTransferred消息值一定要大于接收BUFFER和发送BUFFER的最大长度,否则作为消息就没有意义了。
还有一个需要注意的是WSARecv函数。在IOCP中多次调用该函数是有后果的,严重的会导致接收缓冲区被塞满,就算没有塞满接收缓冲区,如果客户端意外断开连接,GetQueuedCompletionStatus 函数会接到与调用次数一样多次数的返回错误,想必大家一定都不希望这些情况发生。要避免这种问题一定要谨慎的调用WSARecv函数,最好在GetQueuedCompletionStatus 函数接收到数据后再考虑再次调用WSARecv。
今天又测出一个潜在的BUG,先贴代码:
首先是定义:
typedef enum _IO_OPERATION
{
IoRecv, //WSARecv
IoSend, //WSASend
IoQuit
}IO_OPERATION, *PIO_OPERATION;
typedef struct _PER_IO_CONTEXT
{
WSAOVERLAPPED ol;
WSABUF WsaRecvDataBuff;
WSABUF WsaSendDataBuff;
char strRecvBuffer[DATA_MAX_BUFFERSIZE]; //接收BUFFER
char strSendBuffer[DATA_MAX_BUFFERSIZE]; //发送BUFFER
IO_OPERATION IoType;
}PER_IO_CONTEXT, *LPPER_IO_CONTEXT;
这里的IO_OPERATION定义了三种类型,分别表示接收、发送和退出,GetQueuedCompletionStatus函数在接收到消息时,可以通过检测IoType的类型来判断IOCP刚刚完成的操作是接收操作还是发送操作亦或是退出操作。
在一次操作中(比如接收到数据后的操作),先后调用WSASend和WSARecv,来实现发送数据,然后继续Recv的动作,这样做可行吗?答案是否定的。分析:调用WSASend之前,设置IoType为IoSend,标志本次操作是发送操作;然后在调用WSARecv前,设置IoType为IoRecv,标志本次操作是接收操作。IOCP在处理消息队列时,首先应该接收到的是发送操作,由于IoType已经被设置成了IoRecv,在判断时就会将这次操作判断成接收操作,去检测接收BUFFER,这样显然就出错了;然后会接收到接收操作,此时IoType是IoRecv,仍然判断为接收操作,此时检测接收BUFFER,是正确的。这样做的直观表现就是接收事件明显变多了。
以上的这个例子,说明处理IOCP时一定要细心,要注意那些变量是易变的,那些是不易变的。可能上面的这个看起来很明显,但是如果程序复杂了,这种BUG就不易察觉。