IOCP是一整套高性能的IO操作异步模型,可以用在文件操作也可以用在网络SOCKET操作上面。当用在网络SOCKET上时,在服务器端主要配合AceeptEx WSASend WSASendto来使用,在客户机端主要配合ConnectEx WSARecv和WSARecvFrom来使用。这几天用IOCP模型模仿IPMSG软件时有一些感触,分享如下:(这里没有具体的使用常识,这部分请参考《Windows网络编程2nd》或者相关网路资料)
一、单句柄数据和单IO数据
这部分的术语不是很明白如何而来,只是根据Windows网络编程一书的中文翻译而来。
单句柄数据是跟随你丢给IOCP的相关句柄的,而IO数据则是根据你每次IO操作时丢给相关API函数的OVERLAPPED参数的指针。具体来说,如果你要把某个句柄上的操作用IOCP来完成,那么你会调用一次(注意,仅需一次,以前我会在每次IO操作时丢调用,这是错误的示范!)CreateIoCompletionPort时把他的指针赋值给CompletionKey这个参数,而这块堆上内存将会跟随你的句柄直到句柄被Close,而且中间不允许更换,所以说单句柄数据应该而且必须是与你的IO句柄相关的数据比如说socket跟状态等等。
而单IO数据是在调用WSARecv等等的API函数时的OVERLAPPED参数指向的堆上内存,这部分的数据结构最简单的做法是把OVERLAPPED作为数据结构的第一个字段,然后后面跟上跟此次IO操作相关的一些数据,比如说指向缓冲区的指针和表明缓冲区长度的DWORD值等等。这部分的数据只跟每次调用API函数进行的IO操作相关。
二、AcceptEx函数
我在这个函数上卡壳了很长时间,他第三个函数表示一个完成AcceptEx操作后用来接收数据的一个缓冲,第四个参数表示一个缓冲的大小,然后四个函数分别表示本地、远程地址结构的长度。如果你只想做Accept操作而不想在这里做接收数据的动作那么把第四个参数设为0即可。但是容易在这里犯错的是,如果你认为既然不要接收数据那么把第三个参数设定为NULL那么这次投递永远不可能完成,并且所有的返回值WSAGetLastError都会看上去非常正确,这很不幸。即使你不想接收任何数据你也不能把表示缓冲区的参数设为0,而要至少设置一个长度为两个地址结构长度加上32的长度才行,如果不到那个长度那么等着在delete的时候报运行时错误吧!后面两个表示地址结构长度的参数都必须设置成地址结构长度加上16字节。如果你打算从缓冲里取出那两个地址结构,那么切记在每个地址结构后面都有16字节的数据块,这两块数据到底是什么我也不知道,也没有任何资料给我解释包括MSDN,相当崩溃!
三、ConnectEx函数
基本上这个函数至少从表面上没有AcceptEx函数那些龟毛和诡异的东西,但是你认为这跟WSARecv之类API一样直接简单你就又错了。你会发现按照普通的方法调用以后调用WSAGetLastError返回的是10022错误,而不是WSA_IO_PENDING,又崩溃了吧?还好,这次MSDN给了你一小行解释,说The parameter s is an unbound or a listening socket,还是诡异两个字connect操作干嘛要绑定?不知道,没人给解释,那绑定就对了,那么绑哪个?最好把你的地址结构像下面这样设置
SOCKADDR_IN temp;
temp.sin_family = AF_INET;
temp.sin_port = htons(0);
temp.sin_addr.s_addr = htonl(ADDR_ANY);
为什么端口这个地方用0,原因很简单,你去查查MSDN,这样表示他会在1000-4000这个范围(可能记错,想了解的话去查MSDN)找一个没被用到的port,这样的话最大程度保证你bind的成功,然后再把socket句柄丢给IOCP,然后调用ConnectEx这样就会看到熟悉的WSA_IO_PENDING了!
四、WSARecvFrom和WSASendTo
这两个函数没什么诡异的地方,只有一个细节,由于这两个函数都是在UDP里用,所以有个地址结构参数,WSARecvFrom的地址结构API会自己抓取可以在堆栈上分配,而WSASendTo的地址结构API不会自己抓取所以需要你用new在堆上分配,在完成以后再delete掉。
另外还有就是基于UDP的IOCP在WIN2K上可能有些问题,这个在google大神上很容易找到,比如说你打个WSARecvFrom就能在第一页看到,在WINXP上则没有什么问题。
仔细玩了两天IOCP以后发现,细节很重要,无论是看书还是MSDN等等英文资料,不要错过任何一个单词,每错过一个单词就多一个可能让你在某个地方多调试一个小时甚至更多~