IOCP进阶文4篇

注:以下IOCP系列博文版权归 网易博客 GameBaby 所有,转载请按如下方式显示标明原创作者及原文出处,以示尊重!!

原创作者:GameBaby

原文出处:http://gamebabyrocksun.blog.163.com/

 

IOCP加Windows线程池打造高伸缩性高性能的服务器应用

 

       对于IOCP,搞Windows服务器编程的都不会陌生,它所表现出来的性能是其他各种WinSock模型难望其项背的。撰写本文的目的就是为让大家能够在深入理解IOCP的基础上,再来深入的挖掘Windows系统的性能。此处假设读者对IOCP模型已经深刻理解,并对Windows线程、线程池有一定的了解。如果对此还不熟悉,限于篇幅的原因,请您先学习理解这些内容后再来阅读本文。
       在IOCP模型编程中,我们经常需要考虑的就是创建多少个线程来作为完成执行线程,很多时候这是个非常需要技巧和经验的决策性问题。大多数情况下,我们采取的策略是看服务器上有多少CPU然后假定每个CPU最多执行两个线程,然后我们创建的线程数量就是CPU数*2。这看起来很合理,但是实际上在复杂的服务器应用环境中,这样做的效果并不尽如人意,很多时候我们希望得到一种更加动态灵活的方案。
       有些有经验的程序员就自己编写线程池库,来实现这种动态灵活的管理方式,从而还可以实现一定的扩展性,比如系统动态的添加了一些CPU的资源,或者系统负担比较重的时候,或者CPU因为频繁切换线程场景而导致效率低下时,线程池的动态性就发挥出来了。

       索性的是,在Windows2000以上的平台上,已经为我们提供了线程池的接口,虽然这些接口有时候看起来还有些简陋,比如有名的QueueUserWorkItem函数,这些接口简陋到你连当前线程池中有多少活动线程等信息都无法知道,你只能通过其它的工具来动态观察和猜测。但这样的简单性也为我们带来了调用方便的实惠。当然到了Windows2008以上的平台时,线程池的函数总算是被大大加强了,你可以控制更多东西了,关于Windows2008线程池的内容请看我的另一篇博客拙作《Windows2008线程池前瞻》。
       在结合IOCP和线程池这方面Windows系统也想到了程序员面临的这个困难,Windows系统干脆直接就在系统内部捆绑了IOCP和线程池,提供了一个带IOCP功能的线程池函数——BindIoCompletionCallback。
此函数的原形如下:


需要的完成过程(实际也就是IOCP线程的过程)Function的原形需要你定义成如下的样子:

        熟悉IOCP的各位可能已经兴奋得血管暴胀了吧?
       从BindIoCompletionCallback函数的参数你应该已经能够猜到这个函数的用法了,第一个句柄就是你需要捆绑的文件句柄或者SOCKET套接字句柄,甚至是其他I/O设备的句柄。第二个函数的指针就是你的完成例程的指针,这个函数完全由你实现和控制,最后一个Flags参数当前所有的Windows系统中都必须赋予0值,这个参数实际上还没有被起用。
       这么简单?真是难以置信,代表IOCP的句柄上哪去了?其实哪个什么IOCP的句柄,以及创建多少个线程什么的都不需要你考虑了,你唯一需要操心的就是如何编写完成例程以及如何将一个I/O句柄和完成例程捆绑起来。以前需要n多行代码才能完成的事情,一个BindIoCompletionCallback函数就彻底搞定了,甚至我们不需要再考虑线程的动态性问题了。这一切现在都有Windows系统综合考虑了,而你就被解放出来了。
还愣着干嘛?快去写你的高可用性,高可扩展、高动态性的IOCP大型服务应用去了!

 

IOCP编程之“双节棍”

 

在我博客之前的一些文章中,我讨论了关于使用BindIoCompletionCallback函数编写IOCP服务器的话题,在之后的一段时间中我也用此函数展开了伟大的服务器编程实践活动,在实际的应用中,我发现这个函数的很多怪脾气,今天我觉得有必要为大家澄清一下关于此函数的种种诽谤和传闻。

其实这是一个非常非常好用的函数,直接利用了Windows系统所有的优秀特性于一身——多线程、线程池、IOCP等等,我们要做的就是实现一个原型如下的CallBack函数:

他的参数中最有用的就是那个dwErrorCode,这里要特别注意的就是此参数并不像MSDN中说的那么简单的只是一些SOCKET错误码,在用SOCKET句柄代替文件句柄调用BindIoCompletionCallback函数的时候,dwErrorCode参数通常会返回一些0xC开头的系统内部状态码,比如常见的0xC000023F,表示目标地址不可到达,通常是目标UDP地址不可达或者目标UDP端口没有开放,这些错误码都可以在Windows的头文件中找到,通常在Ntstatus.h文件中。

通常我们可以通过调用RtlNtStatusToDosError函数来将这些内部的NTStatus码,翻译为所谓的“DOS”错误码,其实这个函数的名字很诡异,它实际完成就是从系统Ring0层错误码转换为Ring3层错误码,跟DOS其实没有关系。当然这个函数的调用也要通过LoadLibrary调入NtDll.Dll之后,再调用 GetProcAddress函数得到它的地址,然后调用之。

同时,还要特别注意的就是传说中BindIoCompletionCallback失败时,调用GetLastError函数得到错误码时,往往也需要用RtlNtStatusToDosError函数翻译一下,当然我没有遇到过BindIoCompletionCallback函数调用失败的情况,但是如果遇到了也不要慌张,这里已经告诉你了如何看懂这个诡异的错误码。

错误码搞清楚了,那么就可以从容不迫的处理关于回调FileIOCompletionRoutine发生的各种错误,从而让我们的服务器更健康。

当然今天我们讨论的不只是这个函数的错误码这么简单,我们要说的是“双节棍”,大家都知道“双节棍”,是有两根棍子用铁链链接起来才能发挥威力,这里说的BindIoCompletionCallback只是一根棍子,另一根棍子就是QueueUserWorkItem这个最简单的线程池函数。

两根棍子有了,链接到一起的方法就是在FileIOCompletionRoutine回调函数中根据操作类型,来调用QueueUserWorkItem将对应操作的WorkItem放入线程池,通常操作无非就是接收和发送数据,对应的需要编写“接收完成的WorkItem”和“发送完成的WorkItem”。这其实就是用普通线程池接力IOCP本身的线程池。为什么要这么啰嗦的做呢?

这是因为,在实践过程中,我发现,通常我们在接收到数据后,会做一些“慢速”的操作,最常见的就是访问数据库,虽然我用上了OLEDB,但是对于网络速度来说,似乎这还是慢的,尤其是有大量用户朝服务器发送数据请求时,我发现服务器响应很不尽人意,甚至有时出现了频繁掉线的情况。如果直接使用BindIoCompletionCallback通过在FileIOCompletionRoutine中直接调用这些慢速操作时,服务器的响应很低效,此时实际只有IOCP的线程池在工作,而我隐约感觉到这个IOCP的线程池是个很保守的线程池,它假设回调函数FileIOCompletionRoutine很快就会完成,否则就不再去响应其它的完成请求,基于这样的理解,我就在接收完成或发送完成操作之后直接调用QueueUserWorkItem再启动线程池,专门来处理数据,结果性能一下子有了质的飞跃,而且掉线的情况也成了凤毛麟角。在我实际用一个双核的破PC做测试时,服务器的响应一下子由500-1000连接飙升到了6000以上,大家不要笑哈,我说的这个成绩可是普通的破PC装的可是“菜羊”CPU,硬盘只是一块破SATA2盘,内存只有可怜的1G,这个成绩还是比较满意的。最后这个服务程序在上了8G内存双至强带Raid阵列的专业服务器之后,性能简直让我自己都瞠目结舌。

这里要特别提示的就是,在使用这个双节棍的结构中,内存管理就是核心的问题,尤其是要注意申请和释放,而要极力避免的就是共享内存,即尽量不要在线程池中使用并发控制来访问共享内存,对于一些应用来说这似乎又是无法避免的,我的策略是使用数据库来控制共享的状态,因为数据库的并发控制可以用“完美”这个词来形容,当然代价就是性能,但是只要你用了RAID,数据库还是不会让你失望的,至少这样我们省去了自己去做该死的并发控制的麻烦,而且不用担心服务器宕机,因为所有关键的状态都在数据库中,重启服务器自动就可以还原到原状态。

最后我需要澄清的一个事实就是,很多人说BindIoCompletionCallback函数不会启动超过一个线程以上的更多线程,我觉得这是个严重的诽谤,因为在单核单CPU机器上确实是这样的,但是在多核多CPU平台上,只要你没有BT的并发控制,线程数量一般是和CPU数目成正比的,我的服务器在双核机器上一般都会跑到10多个线程,因为有两个线程池,抛开调试时VS环境附加的几个线程,线程池的净线程数还是比较高的。另外要注意的一个问题就是似乎我们这里讨论的两类线程池函数都没有利用Intel的超线程技术,在单CPU超线程的机器上,你不会看到多余一个线程的壮观场面,而在多核CPU上,多个线程是可以被看到的。这也是我们选用IOCP线程池函数的初衷,因为它自动就可以适应多CPU的环境,带有先天的自动适应性。

当然目前Windows的线程池还很弱,除了2008平台以上,2003的服务器中线程池函数还是很弱,我们没法控制线程的亲缘性,没法控制线程的数量,没法处理线程的异常终止等等,我在BindIoCompletionCallback时就遇到过这类问题,因为回调函数的错误导致线程池罢工,服务器死锁等等情况,但是只要有了健壮的异常处理这类事故还是可以避免的,但是这都不是我们拒绝使用这根“双节棍”的理由,所谓“艺高人胆大,胆大艺更高”,只有不断的实践总结经验才是正确的道路。任何的道听途说,以讹传讹都不足为信。

在不久的将来,我将步入研究Windows2008平台“双节棍”的征程当中,因为2008的线程池比之2003的线程池那不是一般的强大。

谨以此文献给那些过去、或者现在、或者将来奋战在服务器开发一线的程序员兄弟们,提前祝大家2010年春节快乐!

............

什么刀枪跟棍棒 我都耍的有模有样

 什么兵器最喜欢 双节棍柔中带刚

 想要去河南嵩山 学少林跟武当

 我只用双节棍 哼哼哈兮

 快使用双节棍 哼哼哈兮

 习武之人切记 仁者无敌

 是谁在练太极 风生水起

 我只用双节棍 哼

 漂亮的旋风踢 一身正气 哼

 快使用双节棍 哼

 我用手刀防御 哼

 漂亮的回旋踢

...............................

 

WinSock2编程之打造完整的SOCKET池

 

在Winodows平台上,网络编程的主要接口就是WinSock,目前大多数的Windows平台上的WinSock平台已经升级到2.0版,简称为WinSock2。在WinSock2中扩展了很多很有用的Windows味很浓的SOCKET专用API,为Windows平台用户提供高性能的网络编程支持。这些函数中的大多数已经不再是标准的“Berkeley”套接字模型的API了。使用这些函数的代价就是你不能再将你的网络程序轻松的移植到“尤里平台”(我给Unix +Linux平台的简称)下,反过来因为Windows平台支持标准的“Berkeley”套接字模型,所以你可以将大多数尤里平台下的网络应用移植到Windows平台下。

如果不考虑可移植性(或者所谓的跨平台性),而是着重于应用的性能时,尤其是注重服务器性能时,对于Windows的程序,都鼓励使用WinSock2扩展的一些API,更鼓励使用IOCP模型,因为这个模型是目前Windows平台上比较完美的一个高性能IO编程模型,它不但适用于SOCKET编程,还适用于读写硬盘文件,读写和管理命名管道、邮槽等等。如果再结合Windows线程池,IOCP几乎可以利用当今硬件所有可能的新特性(比如多核,DMA,高速总线等等),本身具有先天的扩展性和可用性。

今天讨论的重点就是SOCKET池。很多VC程序员也许对SOCKET池很陌生,也有些可能很熟悉,那么这里就先讨论下这个概念。

在Windows平台上SOCKET实际上被视作一个内核对象的句柄,很多Windows API在支持传统的HANDLE参数的同时也支持SOCKET,比如有名的CreateIoCompletionPort就支持将SOCKET句柄代替HANDLE参数传入并调用。熟悉Windows内核原理的读者,立刻就会发现,这样的话,我们创建和销毁一个SOCKET句柄,实际就是在系统内部创建了一个内核对象,对于Windows来说这牵扯到从Ring3层到Ring0层的耗时操作,再加上复杂的安全审核机制,实际创建和销毁一个SOCKET内核对象的成本还是蛮高的。尤其对于一些面向连接的SOCKET应用,服务端往往要管理n多个代表客户端通信的SOCKET对象,而且因为客户的变动性,主要面临的大量操作除了一般的收发数据,剩下的就是不断创建和销毁SOCKET句柄,对于一个频繁接入和断开的服务器应用来说,创建和销毁SOCKET的性能代价立刻就会体现出来,典型的例如WEB服务器程序,就是一个需要频繁创建和销毁SOCKET句柄的SOCKET应用。这种情况下我们通常都希望对于断开的SOCKET对象,不是简单的“销毁”了之(很多时候“断开”的含义不一定就等价于“销毁”,可以仔细思考一下),更多时候希望能够重用这个SOCKET对象,这样我们甚至可以事先创建一批SOCKET对象组成一个“池”,在需要的时候“重用”其中的SOCKET对象,不需要的时候将SOCKET对象重新丢入池中即可,这样就省去了频繁创建销毁SOCKET对象的性能损失。在原始的“Berkeley”套接字模型中,想做到这点是没有什么办法的。而幸运的是在Windows平台上,尤其是支持WinSock2的平台上,已经提供了一套完整的API接口用于支持SOCKET池。

对于符合以上要求的SOCKET池,首先需要做到的就是对SOCKET句柄的“回收”,因为创建函数无论在那个平台上都是现成的,而最早能够实现这个功能的WinSock函数就是TransmitFile,如果代替closesocket函数像下面这样调用就可以“回收”一个SOCKET句柄,而不是销毁:(注意“回收”这个功能对于TransmitFile函数来说只是个“副业”。)

TransmitFile(hSocket,NULL,0,0,NULL,NULL,TF_DISCONNECT | TF_REUSE_SOCKET );

注意上面函数的最后一个参数,使用了标志TF_DISCONNECT和TF_REUSE_SOCKET,第一个值表示断开,第二个值则明确的表示“重用”实际上也就是回收这个SOCKET,经过这个处理的SOCKET句柄,就可以直接再用于connect等操作,但是此时我们会发现,这个回收来的SOCKET似乎没什么用,因为其他套接字函数没法直接利用这个回收来的SOCKET句柄。

这时就要WinSock2的一组专用API上场了。我将它们按传统意义上的服务端和客户端分为两组:

一、         服务端:

二、         客户端:

注意观察这些函数,似乎和传统的“Berkeley”套接字模型中的一些函数“大同小异”,其实仔细观察他们的参数,就已经可以发现一些调用他们的“玄机”了。

首先我们来看AcceptEx函数,与accept函数不同,它需要两个SOCKET句柄作为参数,头一个参数的含义与accept函数的相同,而第二个参数的意思就是accept函数返回的那个代表与客户端通信的SOCKET句柄,在传统的accept内部,实际在返回那个代表客户端的SOCKET时,是在内部调用了一个SOCKET的创建动作,先创建这个SOCKET然后再“accept”让它变成代表客户端连接的SOCKET,而AcceptEx函数就在这里“扩展”(实际上是“阉割”才对)accept函数,省去了内部那个明显的创建SOCKET的动作,而将这个创建动作交给最终的调用者自己来实现。AcceptEx要求调用者创建好那个sAcceptSocket句柄然后传进去,这时我们立刻发现,我们回收的那个SOCKET是不是也可以传入呢?答案是肯定的,我们就是可以利用这个函数传入那个“回收”来的SOCKET句柄,最终实现服务端的SOCKET重用。

这里需要注意的就是,AcceptEx函数必须工作在非阻塞的IOCP模型下,同时即使AcceptEx函数返回了,也不代表客户端连接进来或者连接成功了,我们必须依靠它的“完成通知”才能知道这个事实,这也是AcceptEx函数区别于accept这个阻塞方式函数的最大之处。通常可以利用AcceptEx的非阻塞特性和IOCP模型的优点,一次可以“预先”发出成千上万个AcceptEx调用,“等待”客户端的连接。对于习惯了accept阻塞方式的程序员来说,理解AcceptEx的工作方式还是需要费一些周折的。下面的例子就演示了如何一次调用多个AcceptEx:

以上的例子只是简单的演示了AcceptEx的调用,还没有涉及到真正的“回收重用”这个主题,那么下面的例子就演示了如何重用一个SOCKET句柄:

  

至此回收重用SOCKET的工作也就结束了,以上的过程实际理解了IOCP之后就比较好理解了,例子的最后我们使用了BindIoCompletionCallback函数重新将SOCKET丢进了IOCP线程池中,实际还可以再次使用CreateIoCompletionPort函数达到同样的效果,这里列出这一步就是告诉大家,不要忘了再次绑定一下完成端口和SOCKET。

    对于客户端来说,可以使用ConnectEx函数来代替connect函数,与AcceptEx函数相同,ConnectEx函数也是以非阻塞的IOCP方式工作的,唯一要注意的就是在WSASocket调用之后,在ConnectEx之前要调用一下bind函数,将SOCKET提前绑定到一个本地地址端口上,当然回收重用之后,就无需再次绑定了,这也是ConnectEx较之connect函数高效的地方之一。

   与AcceptEx函数类似,也可以一次发出成千上万个ConnectEx函数的调用,可以连接到不同的服务器,也可以连接到相同的服务器,连接到不同的服务器时,只需提供不同的sockaddr即可。

    通过上面的例子和讲解,大家应该对SOCKET池概念以及实际的应用有个大概的了解了,当然核心仍然是理解了IOCP模型,否则还是寸步难行。

在上面的例子中,回收SOCKET句柄主要使用了DisconnectEx函数,而不是之前介绍的TransmitFile函数,为什么呢?因为TransmitFile函数在一些情况下会造成死锁,无法正常回收SOCKET,毕竟不是专业的回收重用SOCKET函数,我就遇到过好几次死锁,最后偶然的发现了DisconnectEx函数这个专用的回收函数,调用之后发现比TransmitFile专业多了,而且不管怎样都不会死锁。

最后需要补充的就是这几个函数的调用方式,不能像传统的SOCKET API那样直接调用它们,而需要使用一种间接的方式来调用,尤其是AcceptEx和DisconnectEx函数,下面给出了一个例子类,用于演示如何动态载入这些函数并调用之:

这个类的使用非常简单,只需要声明一个类的对象,然后调用其成员AcceptEx、DisconnectEx函数等即可,参数与这些函数的MSDN声明方式完全相同,除了本文中介绍的这些函数外,这个类还包含了很多其他的Winsock2函数,那么都应该按照这个类中演示的这样来动态载入后再行调用,如果无法载入通常说明你的环境中没有Winsock2函数库,或者是你初始化的不是2.0版的Winsock环境。

这个类是本人完整类库的一部分,如要使用需要自行修改一些地方,如果不知如何修改或遇到什么问题,可以直接跟帖说明,我会不定期回答大家的问题,这个类可以免费使用、分发、修改,可以用于任何商业目的,但是对于使用后引起的任何问题,本人概不负责,有问题请跟帖。关于AcceptEx以及其他一些函数,包括本文中没有介绍到得函数,我会在后续的一些专题文章中进行详细深入的介绍,敬请期待。如果你有什么疑问,或者想要了解什么也请跟帖说明,我会在后面的文章中尽量说明。IOCP+WinSock2新函数打造高性能SOCKET池

 

在前一篇文章《WinSock2编程之打造完整的SOCKET池 》中,介绍了WinSock2的一些新函数,并重点详细介绍了什么是SOCKET池,有了这个概念,现在就接着展开更深入的讨论。

首先这里要重点重申一下就是,SOCKET池主要指的是使用面向连接的协议的情况下,最常用的就是需要管理大量的TCP连接的时候。常见的就是Web服务器、FTP服务器等。

下面就分步骤的详细介绍如何最终实现SOCKET池。

 

一、WinSock2环境的初始化:

 

要使用WinSock2就需要先初始化Socket2.0的环境,不废话,上代码:

最后再不使用WinSock之后都要记得调用一下WSACleanup()这个函数;

 

二、装载WinSock2函数:

 

上一篇文章中给出了一个装载WinSock2函数的类,这里分解介绍下装载的具体过程,要提醒的就是,凡是类里面演示了动态装载的函数,最好都像那样动态载入,然后再调用。以免出现上网发帖跪求高手赐教为什么AcceptEx函数无法编译通过等问题。看完这篇文章详细你不会再去发帖找答案了,呵呵呵,好了,上代码:

  以上是一个简单的演示,如何动态载入一个WinSock2扩展函数,并调用之,其它函数的详细例子可以看前一篇文章中CGRSMsSockFun类的实现部分。如果使用CGRSMsSockFun 类的话当然更简单,像下面这样调用即可:

如果要使用这个类,那么需要一些修改,主要是异常处理部分,自己注释掉,或者用其它异常代替掉即可,这个对于有基础的读者来说不是什么难事。

 

三、定义OVERLAPPED结构:

 

要想“IOCP”就要自定义OVERLAPPED,这是彻底玩转IOCP的不二法门,可以这么说:“江湖上有多少种自定义的OVERLAPPED派生结构体,就有多少种IOCP的封装!”

OVERLAPPED本身是Windows IOCP机制内部需要的一个结构体,主要用于记录每个IO操作的“完成状态”,其内容对于调用者来说是没有意义的,但是很多时候我们把它当做一个“火车头”,因为它可以方便的把每个IO操作的相关数据简单的“从调用处运输到完成回调函数中”,这是一个非常有用的特性,哪么如何让这个火车头发挥运输的作用呢?其实很简单:让它成为一个自定义的更大结构体的第一个成员。然后用强制类型转换,将自定义的结构体转换成OVERLAPPED指针即可。当然不一定非要是新结构体的第一个成员,也可以是任何第n个成员,这时使用VC头文件中预定义的一个宏CONTAINING_RECORD再反转回来即可。

说到这里一些C++基础差一点的读者估计已经很头晕了,更不知道我再说什么,那么我就将好人做到底吧,来解释下这个来龙去脉。

首先就以我们将要使用的AcceptEx函数为例子看看它的原型吧(知道孙悟空的火眼金睛用来干嘛的吗?就是用来看原型的,哈哈哈):

注意最后一个参数,是一个OVERLAPPED结构体的指针(LP的意思是Long Pointer,即指向32位地址长指针,注意不是“老婆”拼音的缩写),本身这个参数的意思就是分配一块OVERLAPPED大小的内存,在IOCP调用方式下传递给AcceptEx函数用,调用者不用去关心里面的任何内容,而在完成过程中(很多时候是另一个线程中的事情了),通常调用GetQueuedCompletionStatus函数后,会再次得到这个指针,接着让我们也看看它的原型:

注意这里的LPOVERLAPPED多了一个*变成了指针的指针,并且前面的说明很清楚Out!很明白了吧,不明白就真的Out了。这里就可以重新得到调用AcceptEx传入的LPOVERLAPPED指针,也就是得到了这个“火车头”,因为只是一个指针,并没有详细的限定能有多大,所以可以在火车头的后面放很多东西。

再仔细观察GetQueuedCompletionStatus函数的参数,会发现,这时只能知道一个IO操作结束了,但是究竟是哪个操作结束了,或者是哪个SOCKET句柄上的操作结束了,并没有办法知道。通常这个信息非常重要,因为只有在IO操作实际完成之后才能释放发送或接收等操作的缓冲区。

这些信息可以定义成如下的一个扩展OVERLAPPED结构:

使用时:

  

 在完成过程回调线程函数中,这样使用:

 

  

至此,关于这个“火车头”如何使用,应该是看明白了,其实就是从函数传入,又由函数返回。只不过其间可能已经转换了线程环境,是不同的线程了。

这里再补充一个AcceptEx容易被遗漏的一个细节问题,那就是在AcceptEx完成返回之后,如下在那个连入的客户端SOCKET上调用一下:

这样才可以继续在这个代表客户端连接的pOL->m_skClient上继续调用WSARecv和WSASend。

另外,在AcceptEx完成之后,通常可以用:

这样来得到连入的客户端地址,以及连入的服务端地址,通常这个地址可以和这个客户端的SOCKET绑定在一起用map或hash表保存,方便查询,就不用再调用那个getpeername得到客户端的地址了。要注意的是GetAcceptExSockaddrs也是一个WinSock2扩展函数,专门配合AcceptEx使用的,需要像AcceptEx那样动态载入一下,然后再调用,详情请见前一篇文章中的CGRSMsSockFun类。

至此AcceptEx算讨论完整了,OVERLAPPED的派生定义也讲完了,让我们继续下一步。

 

四、编写线程池回调函数:

 

在讨论扩展定义OVERLAPPED结构体时,给出了非线程池版的线程函数的大概框架,也就是传统IOCP使用的自建线程使用方式,这种方式要自己创建完成端口句柄,自己将SOCKET句柄绑定到完成端口,这里就不在赘述,主要介绍下调用BindIoCompletionCallback函数时,应如何编写这个线程池的回调函数,其实它与前面那个线程函数是很类似的。先来看看回调函数长个什么样子:

第一个参数就是一个错误码,如果是0恭喜你,操作一切ok,如果有错也不要慌张,前一篇文章中已经介绍了如何翻译和看懂这个错误码。照着做就是了。

第二个参数就是说这次IO操作一共完成了多少字节的数据传输任务,这个字段有个特殊含义,如果你发现一个Recv操作结束了,并且这个参数为0,那么就是说,客户端断开了连接(注意针对的是TCP方式,整个SOCKET池就是为TCP方式设计的)。如果这个情况发生了,在SOCKET池中就该回收这个SOCKET句柄。

第三个参数现在不用多说了,立刻就知道怎么用它了。跟刚才调用GetQueuedCompletionStatus函数得到的指针是一个含义。

下面就来看一个实现这个回调的例子:

看起来很简单吧?好像少了什么?对了那个该死的循环,这里不用了,因为这个是由线程池回调的一个函数而已,线程的活动状态完全由系统内部控制,只管认为只要有IO操作完成了,此函数就会被调用。这里关注的焦点就完全的放到了完成之后的操作上,而什么线程啊,完成端口句柄啊什么的就都不需要了(甚至可以忘记)。

这里要注意一个问题,正如在《IOCP编程之“双节棍”》中提到的,这个函数执行时间不要过长,否则会出现掉线啊,连接不进来啊等等奇怪的事情。

另一个要注意的问题就是,这个函数最好套上结构化异常处理,尽可能的多拦截和处理异常,防止系统线程池的线程因为你糟糕的回调函数而壮烈牺牲,如果加入了并发控制,还要注意防止死锁,不然你的服务器会“死”的很难看。

理论上来说,你尽可以把这个函数看做一个与线程池函数等价的函数,只是他要尽可能的“短”(指执行时间)而紧凑(结构清晰少出错)。

最后,回调函数定义好了,就可以调用BindIoCompletionCallback函数,将一个SOCKET句柄丢进完成端口的线程池了:

注意最后一个参数到目前为止,你就传入0吧。这个函数的神奇就是不见了CreateIoCompletionPort的调用,不见了CreateThread的调用,不见了GetQueuedCompletionStatus等等的调用,省去了n多繁琐且容易出错的步骤,一个函数就全部搞定了。

 

五、服务端调用:

 

以上的所有步骤在完全理解后,最终让我们看看SOCKET池如何实现之。

1、按照传统,要先监听到某个IP的指定端口上:

  

2、就是发出一大堆的AcceptEx调用:

这样就有1000个AcceptEx在提前等着客户端的连接了,即使1000个并发连接也不怕了,当然如果再BT点那么就放1w个,什么你要放2w个?那就要看看你的这个IP段的端口还够不够了,还有你的系统内存够不够用。一定要注意同一个IP地址上理论上端口最大值是65535,也就是6w多个,这个要合理的分派,如果并发管理超过6w个以上的连接时,怎么办呢?那就再插块网卡租个新的IP,然后再朝那个IP端绑定并监听即可。因为使用了INADDR_ANY,所以一监听就是所有本地IP的相同端口,如果服务器的IP有内外网之分,为了安全和区别起见可以明确指定监听哪个IP,单IP时就要注意本IP空闲端口的数量问题了。

 

3、AcceptEx返回后,也就是线程函数中,判定是AcceptEx操作返回后,首先需要的调用就是:

之后就可以WSASend或者WSARecv了。

       4、这些调用完后,就可以在这个m_skClient上收发数据了,如果收发数据结束或者IO错误,那么就回收SOCKET进入SOCKET池:

       5、当DisconnectEx函数完成操作之后,在回调的线程函数中,像下面这样重新让这个SOCKET进入监听状态,等待下一个用户连接进来,至此组建SOCKET池的目的就真正达到了:

       至此服务端的线程池就算搭建完成了,这个SOCKET池也就是围绕AcceptEx和DisconnectEx展开的,而创建操作就全部都在服务启动的瞬间完成,一次性投递一定数量的SOCKET进入SOCKET池即可,这个数量也就是通常所说的最大并发连接数,你喜欢多少就设置多少吧,如果连接多数量就大些,如果IO操作多,连接断开请求不多就少点,剩下就是调试了。

 

六、客户端调用:

 

1、  主要是围绕利用ConnectEx开始调用:

如果高兴就可以把上面的过程放到循环里面去,pRemoteAddr就是远程服务器的IP和端口,你可以重复连接n多个,然后疯狂下载东西(别说我告诉你的哈,人家的服务器宕机了找你负责)。注意那个绑定一定要有,不然调用会失败的。

2、  接下来就在线程函数中判定是ConnectEx操作,通过判定m_iOpType == 2就可以知道,然后这样做:

然后就是自由的按照需要调用WSASend或者WSARecv。

3、  最后使用和服务端相似的逻辑调用DisconnectEx函数,收回SOCKET并直接再次调用ConnectEx连接到另一服务器或相同的同一服务器即可。

至此客户端的SOCKET池也搭建完成了,创建SOCKET的工作也是在一开始的一次性就完成了,后面都是利用ConnectEx和DisconnectEx函数不断的连接-收发数据-回收-再连接来进行的。客户端的这个SOCKET池可以用于HTTP下载文件的客户端或者FTP下载的服务端(反向服务端)或者客户端,甚至可以用作一个网游的机器人系统,也可以作为一个压力测试的客户端核心的模型。

 

七、总结和提高:

以上就是比较完整的如何具体实现SOCKET池的全部内容,因为篇幅的原因就不贴全部的代码了,我相信各位看客看完之后心中应该有个大概的框架,并且也可以进行实际的代码编写工作了。可以用纯c来实现也可以用C++来实现。但是这里要说明一点就是DisconnectEx函数和ConnectEx函数似乎只能在XP SP2以上和2003Server以上的平台上使用,对于服务端来说这不是什么问题,但是对于客户端来说,使用SOCKET池时还要考虑一个兼容性问题,不得已还是要放弃在客户端使用SOCKET池。

SOCKET池的全部精髓就在于提前创建一批SOCKET,然后就是不断的重复回收再利用,比起传统的非SOCKET池方式,节省了大量的不断创建和销毁SOCKET对象的内核操作,同时借用IOCP函数AcceptEx、ConnectEx和DisconnectEx等的异步IO完成特性提升了整体性能,非常适合用于一些需要大规模TCP连接管理的场景,如:HTTP Server FTP Server和游戏服务器等。

SOCKET池的本质就是充分的利用了IOCP模型的几乎所有优势,因此要用好SOCKET池就要深入的理解IOCP模型,这是前提。有问题请跟帖讨论。

<div class="post-text" itemprop="text"> <p>I am writing a simple go web application with redis (trying out redis for the first time) on windows. I'm using the go-redis package to connect to redis.</p> <pre><code>package main import ( "fmt" "net/http" "text/template" "github.com/go-redis/redis" "github.com/gorilla/mux" ) var client *redis.Client var tmpl *template.Template func init() { client = redis.NewClient(&redis.Options{ Addr: "localhost:6397", Password: "", DB: 0, }) tmpl = template.Must(template.ParseGlob("./templates/*.gohtml")) pong, err := client.Ping().Result() fmt.Println(pong, err) } func main() { router := mux.NewRouter() router.HandleFunc("/", indexHandler).Methods("GET") http.Handle("/", router) http.ListenAndServe(":1234", nil) } func indexHandler(w http.ResponseWriter, r *http.Request) { comments, err := client.LRange("comments", 0, 10).Result() check(err) tmpl.ExecuteTemplate(w, "index.gohtml", comments) } func check(err error) { if err != nil { fmt.Println(err) return } } </code></pre> <p>But when I run this code, I get "dial tcp [::1]:6397: connectex: No connection could be made because the target machine actively refused it."</p> <p>The only answer that I could find was to "start the redis server". My redis server is up and running (Checked it by using the "PING" command in redis client).I have also tried running it as administrator, but no luck. <a href="https://i.stack.imgur.com/GdjoK.jpg" rel="nofollow noreferrer">Screenshot attached</a></p> <p>Any help would be appreciated. Thanks in advanced! </p> </div>
©️2020 CSDN 皮肤主题: 创作都市 设计师:CSDN官方博客 返回首页