己明白,Worker线程的工作一点也不比主线程少,相反还要更复杂一些,并且具体的通信工作全部都是Worker线程来完成的,Worker线程反而还觉得主线程是在旁边看热闹,只知道发号施令而已,但是大家终究还是谁也离不开谁,这也就和公司里老板和员工的微妙关系是一样的吧……
【第五步】我们再来看看Worker线程都做了些什么
_Worker线程的工作都是涉及到具体的通信事务问题,主要完成了如下的几个工作,让我们一步一步的来看。
(1) 使用 GetQueuedCompletionStatus() 监控完成端口
首先这个工作所要做的工作大家也能猜到,无非就是几个Worker线程哥几个一起排好队队来监视完成端口的队列中是否有完成的网络操作就好了,代码大体如下: [cpp] view plaincopyprint?
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 plaincopyprint?
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 plaincopyprint?
1.PER_IO_CONTEXT* pIoContext = CONTAINING_RECORD(lpOverlapped, PE
R_IO_CONTEXT, m_Overlapped);
这个宏的含义,就是去传入的lpOverlapped变量里,找到和结构体中PER_IO_CONTEXT中m_Overlapped成员相关的数据。 你仔细想想,其实真的很神奇……
但是要做到这种神奇的效果,应该确保我们在结构体PER_IO_CONTEXT定义的时候,把Overlapped变量,定义为结构体中的第一个成员。
只要各位能弄清楚这个GetQueuedCompletionStatus()中各种奇怪的参数,那我们就离成功不远了。
既然我们可以获得PER_IO_CONTEXT结构体,那么我们就自然可以根据其中的m_OpType参数,得知这次收到的这个完成通知,是关于哪个Socket上的哪个I/O操作的,这样就分别进行对应处理就好了。
在我的示例代码里,在有AcceptEx请求完成的时候,我是执行的_DoAccept()函数,在有WSARecv请求完成的时候,执行的是_DoRecv()函数,下面我就分别讲解一下这两个函数的执行流程。
【第六步】当收到Accept通知时 _DoAccept()
在用户收到AcceptEx的完成通知时,需要后续代码并不多,但却是逻辑最为混乱,最容易出错的地方,这也是很多用户为什么宁愿用效率低下的accept()也不愿意去用AcceptEx的原因吧。
和普通的Socket通讯方式一样,在有客户端连入的时候,我们需要做三件事情: (1) 为这个新连入的连接分配一个Socket;
(2) 在这个Socket上投递第一个异步的发送/接收请求; (3) 继续监听。
其实都是一些很简单的事情但是由于“单句柄数据”和“单IO数据”的加入,事情就变得比较乱。因为是这样的,让我们一起缕一缕啊,最好是配合代码一起看,否则太抽象了…… (1) 首先,_Worker线程通过GetQueuedCompletionStatus()里会收到一个
lpCompletionKey,这个也就是PER_SOCKET_CONTEXT,里面保存了与这个I/O相关的Socket和Overlapped还有客户端发来的第一组数据等等,对吧?但是这里得注意,这个SOCKET的上下文数据,是关于监听Socket的,而不是新连入的这个客户端Socket的,千万别弄混了……
(2) 所以,AcceptEx不是给咱们新连入的这个Socket早就建好了一个Socket吗?所以这里,我们需要再用这个新Socket重新为新客户端建立一个PER_SOCKET_CONTEXT,以及下面一系列的新PER_IO_CONTEXT,千万不要去动传入的这个Listen Socket上的PER_SOCKET_CONTEXT,也不要用传入的这个Overlapped信息,因为这个是属于AcceptEx I/O操作的,也不是属于你投递的那个Recv I/O操作的……,要不你下次继续监听的时候就悲剧了……
(3) 等到新的Socket准备完毕了,我们就赶紧还是用传入的这个Listen Socket上的PER_SOCKET_CONTEXT和PER_IO_CONTEXT去继续投递下一个AcceptEx,循环起来,留在这里太危险了,早晚得被人给改了……
(4) 而我们新的Socket的上下文数据和I/O操作数据都准备好了之后,我们要做两件事情:一件事情是把这个新的Socket和我们唯一的那个完成端口绑定,这个就不用细说了,和前面绑定监听Socket是一样的;然后就是在这个Socket上投递第一个I/O操作请求,在我的示例代码里投递的是WSARecv()。因为后续的WSARecv,就不是在这里投递的了,这里只负责第一个请求。
相关推荐: