3. OVERLAPPED m_Overlapped; // 每一个重叠I/O网络操作都要有一
个
4. SOCKET m_sockAccept; // 这个I/O操作所使用的Socket,每个连接
的都是一样的
5. WSABUF m_wsaBuf; // 存储数据的缓冲区,用来给重叠操作传递参
数的,关于WSABUF后面还会讲
6. char m_szBuffer[MAX_BUFFER_LEN]; // 对应WSABUF里的缓冲区 7. OPERATION_TYPE m_OpType; // 标志这个重叠I/O操作是做什么的,例
如Accept/Recv等 8.
9. } PER_IO_CONTEXT, *PPER_IO_CONTEXT;
这个结构体的成员当然是我们随便定义的,里面的成员你可以随意修改(除了OVERLAPPED那个之外……)。
但是AcceptEx不是普通的accept,buffer不是普通的buffer,那么这个结构体当然也不能是普通的结构体了……
在完成端口的世界里,这个结构体有个专属的名字“单IO数据”,是什么意思呢?也就是说每一个重叠I/O都要对应的这么一组参数,至于这个结构体怎么定义无所谓,而且这个结构体也不是必须要定义的,但是没它……还真是不行,我们可以把它理解为线程参数,就好比你使用线程的时候,线程参数也不是必须的,但是不传还真是不行……
除此以外,我们也还会想到,既然每一个I/O操作都有对应的PER_IO_CONTEXT结构体,而在每一个Socket上,我们会投递多个I/O请求的,例如我们就可以在监听Socket上投递多个AcceptEx请求,所以同样的,我们也还需要一个“单句柄数据”来管理这个句柄上所有的I/O请求,这里的“句柄”当然就是指的Socket了,我在代码中是这样定义的:
[cpp] view plaincopy
1.
2. typedef struct _PER_SOCKET_CONTEXT 3. {
4. SOCKET m_Socket; // 每一个客户端连接的
Socket
5. SOCKADDR_IN m_ClientAddr; // 这个客户端的地址 6. CArray<_PER_IO_CONTEXT*> m_arrayIoContext; // 数组,所有客户端IO操作的参
数,
7. // 也就是说对于每一个
客户端Socket
8. // 是可以在上面同时投递
多个IO请求的
9. } PER_SOCKET_CONTEXT, *PPER_SOCKET_CONTEXT;
这也是比较好理解的,也就是说我们需要在一个Socket句柄上,管理在这个Socket上投递的每一个IO请求的_PER_IO_CONTEXT。
当然,同样的,各位对于这些也可以按照自己的想法来随便定义,只要能起到管理每一个IO请求上需要传递的网络参数的目的就好了,关键就是需要跟踪这些参数的状态,在必要的时候释放这些资源,不要造成内存泄漏,因为作为Server总是需要长时间运行的,所以如果有内存泄露的情况那是非常可怕的,一定要杜绝一丝一毫的内存泄漏。 至于具体这两个结构体参数是如何在Worker线程里大发神威的,我们后面再看。 以上就是我们全部的准备工作了,具体的实现各位可以配合我的流程图再看一下示例代码,相信应该会理解得比较快。
完成端口初始化的工作比起其他的模型来讲是要更复杂一些,所以说对于主线程来讲,它总觉得自己付出了很多,总觉得Worker线程是坐享其成,但是Worker自己的苦只有自己明白,Worker线程的工作一点也不比主线程少,相反还要更复杂一些,并且具体的通信工作全部都是Worker线程来完成的,Worker线程反而还觉得主线程是在旁边看热闹,只知道发号施令而已,但是大家终究还是谁也离不开谁,这也就和公司里老板和员工的微妙关系是一样的吧……
【第五步】我们再来看看Worker线程都做了些什么
_Worker线程的工作都是涉及到具体的通信事务问题,主要完成了如下的几个工作,让我们一步一步的来看。
(1) 使用 GetQueuedCompletionStatus() 监控完成端口
首先这个工作所要做的工作大家也能猜到,无非就是几个Worker线程哥几个一起排好队队来监视完成端口的队列中是否有完成的网络操作就好了,代码大体如下:
[cpp] view plaincopy
1.
2. void *lpContext = NULL;
3. OVERLAPPED *pOverlapped = NULL;
4. DWORD dwBytesTransfered = 0; 5.
6. BOOL bReturn = GetQueuedCompletionStatus(
7. pIOCPModel->m_hIOCompletionPort, 8. &dwBytesTransfered, 9. (LPDWORD)&lpContext, 10. &pOverlapped, 11. INFINITE );
各位留意到其中的GetQueuedCompletionStatus()函数了吗?这个就是Worker线程里第一件也是最重要的一件事了,这个函数的作用就是我在前面提到的,会让Worker线程进入不占用CPU的睡眠状态,直到完成端口上出现了需要处理的网络操作或者超出了等待的时间限制为止。
一旦完成端口上出现了已完成的I/O请求,那么等待的线程会被立刻唤醒,然后继续执行后续的代码。
至于这个神奇的函数,原型是这样的:
[cpp] view plaincopy
1.
2. BOOL WINAPI GetQueuedCompletionStatus(
3. __in HANDLE CompletionPort, // 这个就是我们建立的那个唯一的完
成端口
4. __out LPDWORD lpNumberOfBytes, //这个是操作完成后返回的字节数 5. __out PULONG_PTR lpCompletionKey, // 这个是我们建立完成端口的时候绑
定的那个自定义结构体参数
6. __out LPOVERLAPPED *lpOverlapped, // 这个是我们在连入Socket的时候
一起建立的那个重叠结构
7. __in DWORD dwMilliseconds // 等待完成端口的超时时间,如果线
程不需要做其他的事情,那就INFINITE就行了 8. );
所以,如果这个函数突然返回了,那就说明有需要处理的网络操作了 --- 当然,在没有出现错误的情况下。
然后switch()一下,根据需要处理的操作类型,那我们来进行相应的处理。
但是如何知道操作是什么类型的呢?这就需要用到从外部传递进来的loContext参数,也就是我们封装的那个参数结构体,这个参数结构体里面会带有我们一开始投递这个操作的时候设置的操作类型,然后我们根据这个操作再来进行对应的处理。
但是还有问题,这个参数究竟是从哪里传进来的呢?传进来的时候内容都有些什么? 这个问题问得好!
首先,我们要知道两个关键点:
(1) 这个参数,是在你绑定Socket到一个完成端口的时候,用的
CreateIoCompletionPort()函数,传入的那个CompletionKey参数,要是忘了的话,就翻到文档的“第三步”看看相关的内容;我们在这里传入的是定义的PER_SOCKET_CONTEXT,也就是说“单句柄数据”,因为我们绑定的是一个Socket,这里自然也就需要传入Socket相关的上下文,你是怎么传过去的,这里收到的就会是什么样子,也就是说这个
lpCompletionKey就是我们的PER_SOCKET_CONTEXT,直接把里面的数据拿出来用就可以了。
(2) 另外还有一个很神奇的地方,里面的那个lpOverlapped参数,里面就带有我们的PER_IO_CONTEXT。这个参数是从哪里来的呢?我们去看看前面投递AcceptEx请求的时候,是不是传了一个重叠参数进去?这里就是它了,并且,我们可以使用一个很神奇的宏,把和它存储在一起的其他的变量,全部都读取出来,例如:
[cpp] view plaincopy
1. PER_IO_CONTEXT* pIoContext = CONTAINING_RECORD(lpOverlapped, PER_IO_CONTEXT,
m_Overlapped);
这个宏的含义,就是去传入的lpOverlapped变量里,找到和结构体中PER_IO_CONTEXT中m_Overlapped成员相关的数据。 你仔细想想,其实真的很神奇……
但是要做到这种神奇的效果,应该确保我们在结构体PER_IO_CONTEXT定义的时候,把Overlapped变量,定义为结构体中的第一个成员。
只要各位能弄清楚这个GetQueuedCompletionStatus()中各种奇怪的参数,那我们就离成功不远了。
相关推荐: