这篇主要说一些其他不重要的,然后慢慢更新。
如何给IOCP工作线程发送自定义消息并处理
除了前面讲的投递三种IOContext都会从完成队列中返回(GetQueuedCompletionStatus),咱也可以投递自己的数据,让工作线程从队列中取出去处理,一般也没什么让工作线程去处理的,但要完美的关闭IOCP时还得用一下。
给完成队列投递事件用PostQueuedCompletionStatus,参数和GetQueuedCompletionStatus的参数一一对应。具体可以看小猪的例程。有一点,小猪的例程里认为GetQueuedCompletionStatus有可能会出问题而多消耗一个退出事件(hShutdownEvent),所以在循环时用一个hShuntdownEvent来确保工作线程退出,但我个人觉得不好,因为这样会导致队列中剩余事件没有被梳理,所以只要咱把程序流程做对了,就不会有问题。
系统从队列中取到我们投递的IOContext时,GetQueuedCompletionStatus的第2、3、4参数都有值,第2个参数是dwBytesTransfered,表示传输的字节数;第3个参数是我们在给Socket绑定完成端口时传递的参数,也就是SocketContext指针,在CreateIoCompletionPort的第3个参数时传递进去的;第4个参数就是我们投递的IOContext时都会携带一个overlapped,比如WSARecv的第6个参数,包含了IOContext结构体。所以我们在PostQueuedCompletionStatus时,只要让第3个或第4个值为NULL即可,表示这是我们自定义的事件(个人推荐还是让第3个参数为NULL),其他两个参数表示具体的自定义事件类型和值就好了。
另外特别注意,咱多次调用PostQueuedCompletionStatus时是将事件发送到了完成端口队列中,取出时也是按队列方式先进先出的,但因为线程切换的不确定性,任务的执行仍然时不确定的,当初我就遇到过这种问题,这也是为什么不能在一个Socket上投递多个WSARecv来接收数据的重要原因。
关于Accpet时客户端附带第一组数据的说明
我们在使用AcceptEx时,第4个参数如果不为0,那么建立连接时还得等待客户端发送第一组数据,AcceptIOContext才能从完成端口队列中返回。而且对应的,你还得在GetAcceptExSockAddrs的第2个参数也要修改,两个值要设置为一样的。
有个问题是,咱设置的这个值,只是表示最大能接受的字节数,你真正发送数据时不能超过这个值,否则会覆盖掉后续的数据(有客户机的IP和端口数据),而且!!!这个函数不会返回真正收到数据的长度,这就麻烦了,首先你得发送一个固定格式或固定长度的数据才行,还有,你得确定这个数据没有分批发送,要一次性收到,因为AcceptEx也不是等数据满了才返回,哪怕你发送1个字节,它也会返回的。
基于上述分析,个人推荐还是关闭为好(置为0),建立连接后AcceptEx立即返回。
关于更优雅的关闭IOCP
就像小猪说的,一定要优雅的关闭IOCP,别退出时弹出一个无响应或者报错,会显得很low,更何况如果暴力关闭,会导致很多存在于队列中的IOContext、业务数据都没有处理,这也会导致一些无法预期的问题。
首先是IOCP工作线程的关闭,小猪的文章也说的比较全,但是小猪因为在GetQueuedCompletionStatus前使用了hShutdownEvent,可能工作线程在队列中还有数据没处理时就退出了,所以我补充一下:Set关闭事件后,如果有Watch线程(心跳线程),先等待Watch线程结束;然后Close所有的Socket,让所有投递到队列中的Post和Recv的IOContext都返回(这些返回的IOContext会给业务线程投递关闭事件),然后给完成队列中投递退出事件(自定义事件)并等待所有线程退出(这样才会保证退出事件排在所有事件的后面),等所有工作线程返回后,就说明完成队列中的任务就真的空了。
但是这样还没完!完成队列空了,业务线程并没有空,它还有一堆事件需要处理(比如所有的客户Close事件),说不定还得写数据库,不能这样随便退出,所以还得在最后,给业务线程发送一个退出事件,排在所有业务事件的最后(我用Qt实现方式是QMetaObject::invokeMethod调用Stop函数,而Stop函数用来关闭数据库、日志等),所有业务处理完毕后,最后再释放所有资源(包括释放Watch线程句柄、工作线程句柄、完成端口句柄、关闭ListenSocket、释放所有SocketContext和IOContext资源等)。
真麻烦啊,我还是直接exit(0)吧,哈哈哈。