http://h-lm.spaces.live.com/blog/cns!C523F565A10E3B66!824.entry
2008/11/11
当前许多资料都是介绍TCP的IOCP的实现,UDP的较少。
1.很多人在讨论UDP是否需要IOCP。
借http://jlbookworm.spaces.live.com/blog/cns!ef3e777c24481c39!240.entry的文章来说明一下
总算是没有白费时间,搞定了UDP+IOCP。原来的程序采用的是多线程+阻塞的变种,效率一直不如意。本来想一开始就用IOCP的,但是在网上查阅资料的时候看到讨论说UDP不需要用IOCP的,自己想想也觉得有点道理。于是就用最简单的多线程+阻塞来实现了数据采集。后来测试的时候效率一直不如意,更改为IOCP模型时思路错了,一直更改不成功。昨晚突然想到了问题所在,一下就把UDP+IOCP实现了。呵呵。
总算是小有收获,工作线程更改用户界面的问题也解决了,数据采集效率也还过得去了,相关测试程序都可以勉强运转了,又恢复了些许信心了。感觉是度过了一个黎明前的黑夜了,只是不知道还有多少个更黑的夜晚需要经历。
我的实现还没有进行测试,之后再附上。
2.TCP的IOCP是在Accept之后,将Accept创建的套接字与完成端口绑定,而在UDP中,则是把WSASocket或Socket创建的套接字与完成端口绑定。
在实现UDP IOCP时,可以参考已有的TCP IOCP代码,例如http://www.cppblog.com/niewenlong/archive/2007/08/17/30224.html
另外http://www.codeproject.com/KB/IP/iocp-multicast-udp.aspx可供下的源码中的客户端代码是UDP IOCP实现
3.以下数据结构非常重要。
typedef struct _PER_IO_OPERATION_DATA
{
OVERLAPPED Overlapped;
WSABUF DataBuff;
char Buff[24];
BOOL OperationType;
}PER_IO_OPERATION_DATA,* LPPER_IO_OPERATION_DATA;
因为在UDP中每次RecvFrom获WSARecvFrom会传回UDP数据来源的IP,因此可以将以上数据结构修改成:
typedef struct _PER_IO_OPERATION_DATA
{
OVERLAPPED Overlapped;
WSABUF DataBuff;
char Buff[24];
unsigned long recvBytes; //存储接收到的字节数
SOCKADDR_IN remoteAddr; //存储数据来源IP地址
int remoteAddrLen; //存储数据来源IP地址长度
}PER_IO_OPERATION_DATA,* LPPER_IO_OPERATION_DATA;
4.实现过程
创建LPPER_IO_OPERATION_DATA数据结构并进行初始化(初始化很重要)
LPPER_IO_OPERATION_DATA ioperdata;
ioperdata = (LPPER_IO_OPERATION_DATA)malloc(sizeof(PER_IO_OPERATION_DATA));
memset(&(ioperdata->Overlapped), 0, sizeof(OVERLAPPED));
(ioperdata->DataBuff).len = 24;
(ioperdata->DataBuff).buf = ioperdata->Buff;
ioperdata->recvBytes = 24;
ioperdata->remoteAddrLen = sizeof(ioperdata->remoteAddr);
创建完成端口
hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
创建UDP socket
udpSocket = WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_IP, NULL, 0, WSA_FLAG_OVERLAPPED);
绑定UDP Socket
bind(udpSocket, (SOCKADDR*) & addr, sizeof(SOCKADDR_IN));
将完成端口与UDP Socket绑定
CreateIoCompletionPort(
(HANDLE)udpSocket,
hCompletionPort,
(DWORD)udpSocket,
5
);
根据CPU数量*2+2的原则创建工作者线程------- ------- --------------一直循环
CreateWorkers(m_dwThreads)
接收数据 获得当前完成状态 GetQueuedCompletionStatus(
WSARecvFrom( ComPort,
udpSocket, &BytesTransferred,
&(ioperdata->DataBuff), (LPDWORD) & nSocket,
1, (LPOVERLAPPED *) & PerIoData,
&(ioperdata->recvBytes), INFINITE );
&flags,
(SOCKADDR*) & (ioperdata->remoteAddr),
&(ioperdata->remoteAddrLen),
&(ioperdata->Overlapped),
NULL); 处理接收到的数据
继续执行投递操作(类同接收数据操作)
5.WSAGetLastError错误代码
通过WSAGetLastError的信息来测试程序中出现的问题,常见的错误有10055、10014、6等,最主要的是变量的初始化。
另附上http://www.cppblog.com/johndragon/archive/2008/09/16/21845.html的总结
1- 不要为每个小数据包发送一个IOCP请求,这样很容易耗尽IOCP的内部队列.....从而产生10055错误.
2- 不要试图在发送出IOCP请求之后,收到完成通知之前修改请求中使用的数据缓冲的内容,因为在这段时间,系统可能会来读取这些缓冲.
3- 为了避免内存拷贝,可以尝试关闭SOCKET的发送和接收缓冲区,不过代价是,你需要更多的接收请求POST到一个数据流量比较大的SOCKET,从而保证系统一直可以找到BUFFER来收取到来的数据.
4- 在发出多个接收请求的时候,如果你的WORKTHREAD不止一个,一定要使用一些手段来保证接收完成的数据按照发送接收请求的顺序处理,否则,你会遇到数据包用混乱的顺序排列在你的处理队列里.....
5- 说起工作线程, 最好要根据MS的建议, 开 CPU个数*2+2 个, 如果你不了解IOCP的工作原理的话.
6- IOCP的工作线程是系统优化和调度的, 自己就不需要进行额外的工作了.如果您自信您的智慧和经验超过MS的工程师, 那你还需要IOCP么....
7-发出一个Send请求之后,就不需要再去检测是否发送完整,因为iocp会帮你做这件事情,有些人说iocp没有做这件事情,这和iocp的高效能是相悖的,并且我做过的无数次测试表明,Iocp要么断开连接,要么就帮你把每个发送请求都发送完整。
8- 出现数据错乱的时候,不要慌,要从多线程的角度检查你的解析和发送数据包的代码,看看是不是有顺序上的问题。
9- 当遇到奇怪的内存问题时,逐渐的减少工作线程的数量,可以帮你更快的锁定问题发生的潜在位置。
10-同样是遇到内存问题时,请先去检查你的客户端在服务器端内部映射对象的释放是否有问题。而且要小心的编写iocp完成失败的处理代码,防止引用一个错误的内部映射对象的地址。
11- overlapped对象一定要保存在持久的位置,并且不到操作完成(不管成功还是失败)不要释放,否则可能会引发各种奇怪的问题。
12- IOCP的所有工作都是在获取完成状态的那个函数内部进行调度和完成的,所以除了注意工作线程的数量之外,还要注意,尽量保持足够多的工作线程处在获取完成状态的那个等待里面,这样做就需要减少工作线程的负担,确保工作线程内部要处理费时的工作。(我的建议是工作线程和逻辑线程彻底区分开)
13- 刚刚想起来,overlapped对象要为每次的send和recv操作都准备一个全新的,不能图方便重复利用。
14- 尽量保持send和recv的缓冲的大小是系统页面大小的倍数,因为系统发送或者接收数据的时候,会锁用户内存的,比页面小的缓冲会浪费掉整个一个页面。(作为第一条的补充,建议把小包合并成大包发送)