帮帮忙,想要详细解说步骤跟编程程序。谢谢

   手把手叫你玩转网络编程系列之彡

        本系列里完毕port的代码在两年前就已经写好了可是因为许久没有写东西了,不知该怎样提笔所以这篇文档总是在酝酿之中……酝酿了兩年之后,最终决定開始动笔了但愿还不算晚…..

这篇文档我很具体而且图文并茂的介绍了关于网络编程模型中完毕port的方方面面的信息,從API的使用方法到使用的步骤从完毕port的实现机理到实际使用的注意事项,都有所涉及而且为了让朋友们更直观的体会完毕port的使用方法,夲文附带了有详尽凝视的使用MFC编写的图形界面的演示样例代码

        我的初衷是希望写一份互联网上能找到的最详尽的关于完毕port的教学文档,並且让对Socket编程略有了解的人都可以看得懂都能学会怎样来使用完毕port这么优异的网络编程模型,可是因为本人水平所限不知道我的初衷昰否实现了,但还是希望各位须要的朋友可以喜欢

        因为篇幅原因,本文如果你已经熟悉了利用Socket进行TCP/IP编程的基本原理而且也熟练的掌握叻多线程编程技术,太主要的概念我这里就略过不提了网上的资料应该遍地都是。

        本文档凝聚着笔者心血如要转载,请指明原作者及絀处谢谢!只是代码没有版权,可以随便散播使用欢迎改进,特别是很欢迎可以帮助我发现Bug的朋友以更好的造福大家。^_^

(里面的代码包含VC++2008/VC++2010编写的完毕portserver端和client的代码还包含一个对server端进行压力測试的client,都是经过我精心调试过而且带有很详尽的代码凝视的。当然作为教学玳码,为了可以使得代码结构清晰明了我还是对代码有所简化,假设想要用于产品开发不妨须要自己再完好一下,另外我的project是用2010编写嘚附带的2008project不知道有没有问题,可是当中代码都是一样的暂未測试)

        忘了叮嘱一下了,文章篇幅非常长非常长基本涉及到了与完毕port有关嘚方方面面,一次看不完能够分好几次中间注意歇息,好身体才是咱们程序猿最大的本钱!

       对了还忘了叮嘱一下,由于本人的水平有限尽管我重复修正了数遍,但文章和演示样例代码里肯定还有我没发现的错误和纰漏希望各位一定要指出来,拍砖、喷我我都能Hold住,可是一定要指出来我会及时修正,由于我不想让文中的错误传遍互联网祸害大家。

2. 完毕port程序的执行演示

3. 完毕port的相关概念

4. 完毕port嘚基本流程

5. 完毕port的使用具体解释

6. 实际应用中应该要注意的地方

一. 完毕port的长处

我想仅仅要是写过或者想要写C/S模式网络server端的朋友都应該或多或少的听过完毕port的大名吧,完毕port会充分利用Windows内核来进行I/O的调度是用于C/S通信模式中性能最好的网络通信模型,没有之中的一个;甚臸连和它性能接近的通信模型都没有

首先,假设使用“同步”的方式来通信的话这里说的同步的方式就是说全部的操作都在一个线程內顺序运行完毕,这么做缺点是非常明显的:由于同步的通信操作会堵塞住来自同一个线程的不论什么其它操作仅仅有这个操作完毕了の后,兴许的操作才干够完毕;一个最明显的样例就是咱们在MFC的界面代码中直接使用堵塞Socket调用的代码,整个界面都会因此而堵塞住没有響应!所以我们不得不为每个通信的Socket都要建立一个线程多麻烦?这不坑爹呢么所以要写高性能的server程序,要求通信一定要是异步的

各位读者肯定知道,能够使用使用“同步通信(堵塞通信)+多线程”的方式来改善(1)的情况那么好,想一下我们好不easy实现了让server端在每个client连入之後,都要启动一个新的Thread和client进行通信有多少个client,就须要启动多少个线程对吧;可是由于这些线程都是处于执行状态,所以系统不得不在铨部可执行的线程之间进行上下文的切换我们自己是没啥感觉,可是CPU却痛苦不堪了由于线程切换是相当浪费CPU时间的,假设client的连入线程過多这就会弄得CPU都忙着去切换线程了,根本没有多少时间去执行线程体了所以效率是很低下的,承认坑爹了不

而微软提出完毕port模型嘚初衷,就是为了解决这样的"one-thread-per-client"的缺点的它充分利用内核对象的调度,仅仅使用少量的几个线程来处理和客户端的全部通信消除了无谓嘚线程上下文切换,最大限度的提高了网络通信的性能这样的奇妙的效果详细是怎样实现的请看下文。

二. 完毕port程序的执行演示

16GB内存峩以这台PC作为server,简单的进行了例如以下的測试通过Client生成3万个并发线程同一时候连接至Server,然后每一个线程每隔3秒钟发送一次数据一共发送3次,然后观察server端的CPU和内存的占用情况

3.82%,整个执行过程中的峰值也没有超过4%是相当气定神闲的……哦,对了这还是在Debug环境下执行的凊况,假设採用Release方式执行性能肯定还会更高一些,除此以外在UI上显示信息也非常大成都上影响了性能。

         事实上不管是哪种网络操模型对于内存占用都是差点儿相同的,真正的区别就在于CPU的占用其它的网络模型都须要很多其它的CPU动力来支撑相同的连接数据。

         尽管这远遠算不上server极限压力測试可是从中也能够看出来完毕port的实力,并且这样的方式比纯粹靠多线程的方式实现并发资源占用率要低得多

三. 唍毕port的相关概念

在開始编码之前,我们先来讨论一下和完毕port相关的一些概念假设你没有耐心看完这段大段的文字的话,也能够跳过这一節直接去看下下一节的详细实现部分可是这一节中涉及到的基本概念你还是有必要了解一下的,并且你也更能知道为什么有那么多的网絡编程模式不用非得要用这么又复杂又难以理解的完毕port呢?也会坚定你继续学习下去的信心^_^

异步通信就是在咱们与外部的I/O设备进行打茭道的时候,我们都知道外部设备的I/O和CPU比起来简直是龟速比方硬盘读写、网络通信等等,我们没有必要在咱们自己的线程里面等待着I/O操莋完毕再运行兴许的代码而是将这个请求交给设备的驱动程序自己去处理,我们的线程能够继续做其它更重要的事情大体的流程例如鉯下图所看到的:

        我能够从图中看到一个非常明显的并行操作的过程,而“同步”的通信方式是在进行网络操作的时候主线程就挂起了,主线程要等待网络操作完毕之后才干继续运行兴许的代码,就是说要末运行主线程要末运行网络操作,是没法这样并行的;

        “异步”方式无疑比 “堵塞模式+多线程”的方式效率要高的多这也是前者为什么叫“异步”,后者为什么叫“同步”的原因了由于不须要等待網络操作完毕再运行别的操作。

而在Windows中实现异步的机制相同有好几种而这当中的差别,关键就在于图1中的最后一步“通知应用程序处理網络数据”上了由于实现操作系统调用设备驱动程序去接收数据的操作都是一样的,关键就是在于怎样去通知应用程序来拿数据它们の间的详细差别我这里多讲几点,文字有点多假设没兴趣深入研究的朋友能够跳过下一面的这一段,不影响的:)

设备内核对象使用设备內核对象来协调数据的发送请求和接收数据协调,也就是说通过设置设备内核对象的状态在设备接收数据完毕后,立即触发这个内核对潒然后让接收数据的线程收到通知,可是这样的方式太原始了接收数据的线程为了可以知道内核对象是否被触发了,还是得不停的挂起等待这简直是根本就没实用嘛,太低级了有木有?所以在这里就略过不提了各位读者要是没明确是怎么回事也不用深究了,总之沒有什么用

事件内核对象,利用事件内核对象来实现I/O操作完毕的通知事实上这样的方式事实上就是我曾经写文章的时候提到的《基于倳件通知的重叠I/O模型》,这样的机制就先进得多,能够同一时候等待多个I/O操作的完毕实现真正的异步,可是缺点也是非常明显的既嘫用WaitForMultipleObjects()来等待Event的话,就会受到64个Event等待上限的限制可是这可不是说我们仅仅能处理来自于64个client的Socket,而是这是属于在一个设备内核对象上等待的64個事件内核对象也就是说,我们在一个线程内能够同一时候监控64个重叠I/O操作的完毕状态,当然我们相同能够使用多个线程的方式来满足无限多个重叠I/O的需求比方假设想要支持3万个连接,就得须要500多个线程…用起来太麻烦让人感觉不爽;

Call异步过程调用)来完毕,这个也僦是我曾经在文章里提到的《基于完毕例程的重叠I/O模型》,这样的方式的优点就是在于摆脱了基于事件通知方式的64个事件上限的限制鈳是缺点也是有的,就是发出请求的线程必须得要自己去处理接收请求哪怕是这个线程发出了非常多发送或者接收数据的请求,可是其咜的线程都闲着…这个线程也还是得自己来处理自己发出去的这些请求,没有人来帮忙…这就有一个负载均衡问题显然性能没有达到朂优化。

完毕port不用说大家也知道了,最后的压轴戏就是使用完毕port对照上面几种机制,完毕port的做法是这种:事先开好几个线程你有几個CPU我就开几个,首先是避免了线程的上下文切换由于线程想要运行的时候,总有CPU资源可用然后让这几个线程等着,等到实用户请求来箌的时候就把这些请求都增加到一个公共消息队列中去,然后这几个开好的线程就排队逐一去从消息队列中取出消息并加以处理这种方式就非常优雅的实现了异步通信和负载均衡的问题,因为它提供了一种机制来使用几个线程“公平的”处理来自于多个client的输入/输出而苴线程假设没事干的时候也会被系统挂起,不会占用CPU周期挺完美的一个解决方式,不是吗哦,对了这个关键的作为交换的消息队列,就是完毕port

比較完毕之后,熟悉网络编程的朋友可能会问到为什么没有提到WSAAsyncSelect或者是WSAEventSelect这两个异步模型呢,对于这两个模型我不知道其內部是怎样实现的,可是这当中一定没实用到Overlapped机制就不能算作是真正的异步,可能是其内部自己在维护一个消息队列吧总之这两个模式尽管实现了异步的接收,可是却不能进行异步的发送这就非常明显说明问题了,我想其内部的实现一定和完毕port是迥异的而且,完毕port非常厚道由于它是先把用户数据接收回来之后再通知用户直接来取就好了,而WSAAsyncSelect和WSAEventSelect之流仅仅是会接收到数据到达的通知而仅仅能由应用程序自己再另外去recv数据,性能上的差距就更明显了

        最后,我的建议是想要使用 基于事件通知的重叠I/O和基于完毕例程的重叠I/O的朋友,假設不是特别必要就不要去使用了,由于这两种方式不仅使用和理解起来也不算简单并且还有性能上的明显瓶颈,何不就再努力一下使鼡完毕port呢

Richter的解释是由于“运行I/O请求的时间与线程运行其它任务的时间是重叠(overlapped)的”,从这个名字我们也可能看得出来重叠结构发明的初衷叻对于重叠结构的内部细节我这里就只是多的解释了,就把它当成和其它内核对象一样不须要深究事实上现机制,仅仅要会使用就能夠了想要了解很多其它重叠结构内部的朋友,请去翻阅Jeffrey Richter的《Windows via C/C++》 5th 的292页假设没有机会的话,也能够随便翻翻我曾经写的Overlapped的东西只是写得仳較浅显……

这里我想要解释的是,这个重叠结构是异步通信机制实现的一个核心数据结构由于你看到后面的代码你会发现,差点儿全蔀的网络操作比如发送/接收之类的都会用WSASend()和WSARecv()取代,參数里面都会附带一个重叠结构这是为什么呢?由于重叠结构我们就能够理解成为昰一个网络操作的ID号也就是说我们要利用重叠I/O提供的异步机制的话,每个网络操作都要有一个唯一的ID号由于进了系统内核,里面黑灯瞎火的也不了解上面出了什么状况,一看到有重叠I/O的调用进来了就会使用其异步机制,而且操作系统就仅仅能靠这个重叠结构带有的ID號来区分是哪一个网络操作了然后内核里面处理完成之后,依据这个ID号把相应的数据传上去。

        对于完毕port这个概念我一直不知道为什麼它的名字是叫“完毕port”,我个人的感觉应该叫它“完毕队列”似乎更合适一些总之这个“port”和我们寻常所说的用于网络通信的“port”全嘫不是一个东西,我们不要混淆了

首先,它之所以叫“完毕”port就是说系统会在网络I/O操作“完毕”之后才会通知我们,也就是说我们茬接到系统的通知的时候,事实上网络操作已经完毕了就是比方说在系统通知我们的时候,并不是是有数据从网络上到来而是来自于網络上的数据已经接收完毕了;或者是client的连入请求已经被系统接入完毕了等等,我们仅仅须要处理后面的事情就好了

        各位朋友可能会非瑺开心,什么已经处理完成了才通知我们,那岂不是非常爽事实上也没什么爽的,那是由于我们在之前给系统分派工作的时候都叮囑好了,我们会通过代码告诉系统“你给我做这个做那个等待做完了再通知我”,仅仅是这些工作是做在之前还是之后的差别而已

Richter恐嚇我们说:“完毕port可能是最为复杂的内核对象了”,可是我们也不用去管他由于它详细的内部怎样实现的和我们无关,仅仅要我们可以學会用它相关的API把这个完毕port的框架搭建起来就行了我们临时仅仅用把它大体理解为一个容纳网络通信操作的队列就好了,它会把网络操莋完毕的通知都放在这个队列里面,咱们仅仅用从这个队列里面取就行了取走一个就少一个…。

关于完毕port内核对象的具体很多其它内蔀细节我会在后面的“完毕port的基本原理”一节更具体的和朋友们一起来研究当然,要是你们在文章中没有看到这一节的话就是说明我叒犯懒了没写…在兴许的文章里我会补上。这里就临时说这么多了到时候我们也能够看到它的机制也并不是有那么的复杂,可能仅仅是甴于操作系统其它的内核对象相比較而言实现起来太easy了吧^_^

四. 使用完毕port的基本流程

        (2) 依据系统中有多少个处理器就建立多少个工作者(为了醒目起见,以下直接说Worker)线程这几个线程是专门用来和client进行通信的,眼下临时没什么工作;

以下就是接收连入的Socket连接了这里有两种实现方式:一是和别的编程模型一样,还须要启动一个独立的线程专门用来acceptclient的连接请求;二是用性能更高更好的异步AcceptEx()请求,由于各位对accept使用方法应该非常熟悉了并且网上资料也会非常多,所以为了更全面起见本文採用的是性能更好的AcceptEx,至于两者代码编写上的差别我接下來会具体的讲。

        至此我们事实上就已经完毕了完毕port的相关部署工作了,嗯是的,完事了后面的代码里我们就能够充分享受完毕port带给峩们的巨大优势,坐享其成了是不是非常easy呢?

       (5) 比如client连入之后,我们能够在这个Socket上提交一个网络请求比如WSARecv(),然后系统就会帮咱们乖乖嘚去运行接收数据的操作我们大能够放心的去干别的事情了;

函数在扫描完毕port的队列里是否有网络通信的请求存在(比如读取数据,发送數据等)一旦有的话,就将这个请求从完毕port的队列中取回来继续运行本线程中后面的处理代码,处理完毕之后我们再继续投递下一个網络通信的请求就OK了,如此循环

        当然,我这里如果你已经对网络编程的基本套路有了解了所以略去了非常多主要的细节,而且为了配匼朋友们更好的理解我的代码在流程图我标出了一些函数的名字,而且画得非常具体

另外须要注意的是因为对于client的连入有两种方式,┅种是普通堵塞的accept第二种是性能更好的AcceptEx,为了可以方面朋友们从别的网络编程的方式中过渡我这里画了两种方式的流程图,方便朋友們对照学习图a是使用accept的方式,当然配套的源代码我默认就不提供了假设须要的话,我倒是也可以发上来;图b是使用AcceptEx的并配有配套的源代码。

 为什么呢由于我们使用了异步的通信机制,这些琐碎反复的事情全然没有必要交给主线程自己来做了仅仅用在初始化的时候囷Worker线程交待好就能够了,用一句话来形容就是主线程永远也体会不到Worker线程有多忙,而Worker线程也永远体会不到主线程在初始化建立起这个通信框架的时候操了多少的心……

_AcceptThread()负责接入连接并把连入的Socket和完毕port绑定,另外的多个_WorkerThread()就负责监控完毕port上的情况一旦有情况了,就取出来處理假设CPU有多核的话,就能够多个线程轮着来处理完毕port上的信息非常明显效率就提高了。

图b中最明显的差别也就是AcceptEx和传统的accept之间最夶的差别,就是取消了堵塞方式的accept调用也就是说,AcceptEx也是通过完毕port来异步完毕的所以就取消了专门用于accept连接的线程,用了完毕port来进行异步的AcceptEx调用;然后在检索完毕port队列的Worker函数中依据用户投递的完毕操作的类型,再来找出当中的投递的Accept请求加以相应的处理。

(10055)了由于系統来不及为新连入的client准备资源了。

这么一行的代码就OK了可是系统内部建立一个Socket是相当耗费资源的,由于Winsock2是分层的机构体系创建一个Socket须偠到多个Provider之间进行处理,终于形成一个可用的套接字总之,系统创建一个Socket的开销是相当高的所以用accept的话,系统可能来不及为很多其它嘚并发client现场准备Socket了

这个优点是最关键的,是由于AcceptEx是在client连入之前就把client的Socket建立好了,也就是说AcceptEx是先建立的Socket,然后才发出的AcceptEx调用也就是說,在进行client的通信之前不管是否有client连入,Socket都是提前建立好了;而不须要像accept是在client连入了之后再现场去花费时间建立Socket。假设各位不清楚是怎样实现的请看后面的实现部分。

         (2) 相比accept仅仅能堵塞方式建立一个连入的入口对于大量的并发client来讲,入口实在是有点挤;而AcceptEx能够同一时候在完毕port上投递多个请求这样有client连入的时候,就很优雅并且从容不迫的边喝茶边处理连入请求了

AcceptEx另一个很体贴的长处,就是在投递AcceptEx的時候我们还能够顺便在AcceptEx的同一时候,收取client发来的第一组数据这个是同一时候进行的,也就是说在我们收到AcceptEx完毕的通知的时候,我们僦已经把这第一组数据接完毕了;可是这也意味着假设client仅仅是连入可是不发送数据的话,我们就不会收到这个AcceptEx完毕的通知……这个我们茬后面的实现部分也能够具体看到。

五. 完毕port的实现具体解释

        这里我把完毕port的具体实现步骤以及会涉及到的函数依照出现的先后步骤,都和大家具体的说明解释一下当然,文档中为了让大家便于阅读这里去掉了当中的错误处理的内容,当然这些内容在演示样例代碼中是会有的。

呵呵看到CreateIoCompletionPort()的參数不要奇怪,參数就是一个INVALID一个NULL,两个0…说白了就是一个-1,三个0……简直就和什么都没传一样可是Windows系统内部却是好一顿忙活,把完毕port相关的资源和数据结构都已经定义好了(在后面的原理部分我们会看到完毕port相关的数据结构大部分都是┅些用来协调各种网络I/O的队列),然后系统会给我们返回一个有意义的HANDLE仅仅要返回值不是NULL,就说明建立完毕port成功了就这么简单,不是吗

        至于里面各个參数的详细含义,我会放到后面的步骤中去讲反正这里仅仅要知道创建我们唯一的这个完毕port,就仅仅是须要这么几个參數

0,我这里要简单的说两句这个0可不是一个普通的0,它代表的是NumberOfConcurrentThreads也就是说,同意应用程序同一时候执行的线程数量当然,我们这裏为了避免上下文切换最理想的状态就是每一个处理器上仅仅执行一个线程了,所以我们设置为0就是说有多少个处理器,就同意同一時候多少个线程执行

        由于比方一台机器仅仅有两个CPU(或者两个核心),假设让系统同一时候执行的线程多于本机的CPU数量的话那事实上昰没有什么意义的事情,由于这样CPU就不得不在多个线程之间执行上下文切换这会浪费宝贵的CPU周期,反而减少的效率我们要牢记这个原則。

        我们前面已经提到这个Worker线程非常重要,是用来详细处理网络请求、详细和client通信的线程并且对于线程数量的设置非常有意思,要等於系统中CPU的数量那么我们就要首先获取系统中CPU的数量,这个是基本功我就不多说了,代码例如以下:

我们最好是建立CPU核心数量*2那么多嘚线程这样更能够充分利用CPU资源,由于完毕port的调度是很智能的比方我们的Worker线程有的时候可能会有Sleep()或者WaitForSingleObject()之类的情况,这样同一个CPU核心上嘚还有一个线程就能够取代这个Sleep的线程运行了;由于完毕port的目标是要使得CPU满负荷的工作

 // 依据CPU数量,建立*2的线程
// 这里需要特别注意假设偠使用重叠I/O的话,这里必需要使用WSASocket来初始化Socket // 填充地址结构信息 // 这里能够选择绑定不论什么一个可用的地址或者是自己指定的一个IP地址

listen.,所以说不用白不用咯^_^

等等!大家没认为这个函数非常眼熟么?是的这个和前面那个创建完毕port用的竟然是同一个API!可是这里这个API可不是鼡来建立完毕port的,而是用于将Socket和曾经创建的那个完毕port绑定的大家可要看准了,不要被迷惑了由于他们的參数是明显不一样的,前面那個的參数是一个-1三个0,太好记了…

// 绑定的时候把自定义的结构体指针传递 // 这样到了Worker线程中也能够使用这个 // 结构体的数据了,相当于參數的传递

        这个AcceptEx比較特别并且这个是微软专门在Windows操作系统里面提供的扩展函数,也就是说这个不是Winsock2标准里面提供的是微软为了方便咱们使用重叠I/O机制,额外提供的一些函数所以在使用之前也还是须要进行些准备工作。

实际上是存在于Winsock2结构体系之外的(由于是微软另外提供嘚)所以假设我们直接调用AcceptEx的话,首先我们的代码就仅仅能在微软的平台上用了没有办法在其它平台上调用到该平台提供的AcceptEx的版本号(假設有的话), 并且更糟糕的是我们每次调用AcceptEx时,Service Provider都得要通过WSAIoctl()获取一次该函数指针效率太低了,所以还不如我们自己直接在代码中直接去這么获取一下指针好了

 

详细实现就没什么可说的了,由于都是固定的套路那个GUID是微软给定义好的,直接拿过来用即可了WSAIoctl()就是通过这個找到AcceptEx的地址的,另外须要注意的是通过WSAIoctl获取AcceptEx函数指针时,仅仅须要随便传递给WSAIoctl()一个有效的SOCKET即可该Socket的类型不会影响获取的AcceptEx函数指针。

  • 參数2--sAcceptSocket, 用于接受连接的socket这个就是那个须要我们事先建好的,等有client连接进来直接把这个Socket拿给它用的那个是AcceptEx高性能的关键所在。

  • 这也是AcceptEx比較囿特色的地方既然AcceptEx不是普通的accpet函数,那么这个缓冲区也不是普通的缓冲区这个缓冲区包括了三个信息:一是客户端发来的第一组数据,二是server的地址三是client地址,都是精华啊…可是读取起来就会非常麻烦只是后面有一个更好的解决方式。

  • 參数4--dwReceiveDataLength前面那个參数lpOutputBuffer中用于存放數据的空间大小。假设此參数=0则Accept时将不会待数据到来,而直接返回假设此參数不为0,那么一定得等接收到数据了才会返回…… 所以通瑺当须要Accept接收数据时就须要将该參数设成为:sizeof(lpOutputBuffer) - 2*(sizeof sockaddr_in +16),也就是说总长度减去两个地址空间的长度就是了看起来复杂,事实上想明确了也没啥……

        这里面的參数倒是没什么看起来复杂,可是咱们依然能够一个一个传进去然后在相应的IO操作完毕之后,这些參数Windows内核自然就会帮咱们填满了

等我们再次见到这些个变量的时候,就已经是在Worker线程内部了由于Windows会直接把操作完毕的结果传递到Worker线程里,这样咱们在启动嘚时候投递了那么多的IO请求这从Worker线程传回来的这些结果,究竟是相应着哪个IO请求的呢。。

        聪明的你肯定想到了,是的Windows内核也帮峩们想到了:用一个标志来绑定每个IO操作,这样到了Worker线程内部的时候收到网络操作完毕的通知之后,再通过这个标志来找出这组返回的數据究竟相应的是哪个Io操作的

 
 WSABUF m_wsaBuf; // 存储数据的缓冲区,用来给重叠操作传递參数的关于WSABUF后面还会讲

在完毕port的世界里,这个结构体有个专属嘚名字“单IO数据”是什么意思呢?也就是说每个重叠I/O都要相应的这么一组參数至于这个结构体怎么定义无所谓,并且这个结构体也没必要要定义的可是没它……还真是不行,我们能够把它理解为线程參数就好比你使用线程的时候,线程參数也不是必须的可是不传還真是不行……

除此以外,我们也还会想到既然每个I/O操作都有相应的PER_IO_CONTEXT结构体,而在每个Socket上我们会投递多个I/O请求的,比如我们就能够在監听Socket上投递多个AcceptEx请求所以相同的,我们也还须要一个“单句柄数据”来管理这个句柄上全部的I/O请求这里的“句柄”当然就是指的Socket了,峩在代码中是这样定义的:

 
 // 是能够在上面同一时候投递多个IO请求的

当然相同的,各位对于这些也能够依照自己的想法来随便定义仅仅偠能起到管理每个IO请求上须要传递的网络參数的目的就好了,关键就是须要跟踪这些參数的状态在必要的时候释放这些资源,不要造成內存泄漏由于作为Server总是须要长时间执行的,所以假设有内存泄露的情况那是很可怕的一定要杜绝一丝一毫的内存泄漏。

         以上就是我们所有的准备工作了详细的实现各位能够配合我的流程图再看一下演示样例代码,相信应该会理解得比較快

完毕port初始化的工作比起其它嘚模型来讲是要更复杂一些,所以说对于主线程来讲它总认为自己付出了非常多,总认为Worker线程是坐享其成可是Worker自己的苦仅仅有自己明確,Worker线程的工作一点也不比主线程少相反还要更复杂一些,而且详细的通信工作所有都是Worker线程来完毕的Worker线程反而还认为主线程是在旁邊看热闹,仅仅知道发号施令而已可是大家终究还是谁也离不开谁,这也就和公司里老板和员工的微妙关系是一样的吧……

        首先这个工莋所要做的工作大家也能猜到无非就是几个Worker线程哥几个一起排好队队来监视完毕port的队列中是否有完毕的网络操作就好了,代码大体例如鉯下:

 

各位留意到当中的GetQueuedCompletionStatus()函数了吗这个就是Worker线程里第一件也是最重要的一件事了,这个函数的作用就是我在前面提到的会让Worker线程进入鈈占用CPU的睡眠状态,直到完毕port上出现了须要处理的网络操作或者超出了等待的时间限制为止

 

        可是怎样知道操作是什么类型的呢?这就须偠用到从外部传递进来的loContext參数也就是我们封装的那个參数结构体,这个參数结构体里面会带有我们一開始投递这个操作的时候设置的操莋类型然后我们依据这个操作再来进行相应的处理。

这个參数是在你绑定Socket到一个完毕port的时候,用的CreateIoCompletionPort()函数传入的那个CompletionKey參数,要是忘了嘚话就翻到文档的“第三步”看看相关的内容;我们在这里传入的是定义的PER_SOCKET_CONTEXT,也就是说“单句柄数据”由于我们绑定的是一个Socket,这里洎然也就须要传入Socket相关的上下文你是怎么传过去的,这里收到的就会是什么样子也就是说这个lpCompletionKey就是我们的PER_SOCKET_CONTEXT,直接把里面的数据拿出来鼡就能够了

另外另一个非常奇妙的地方,里面的那个lpOverlapped參数里面就带有我们的PER_IO_CONTEXT。这个參数是从哪里来的呢我们去看看前面投递AcceptEx请求的時候,是不是传了一个重叠參数进去这里就是它了,而且我们能够使用一个非常奇妙的宏,把和它存储在一起的其它的变量所有都讀取出来,比如:

        事实上都是一些非常easy的事情可是由于“单句柄数据”和“单IO数据”的增加事情就变得比較乱。由于是这种让我们一起缕一缕啊,最好是配合代码一起看否则太抽象了……

Socket上的PER_SOCKET_CONTEXT,也不要用传入的这个Overlapped信息由于这个是属于AcceptEx I/O操作的,也不是属于你投递的那个Recv I/O操作的……要不你下次继续监听的时候就悲剧了……

而我们新的Socket的上下文数据和I/O操作数据都准备好了之后,我们要做两件事情:一件事情是把这个新的Socket和我们唯一的那个完毕port绑定这个就不用细说了,和前面绑定监听Socket是一样的;然后就是在这个Socket上投递第一个I/O操作请求在我的演示样例代码里投递的是WSARecv()。由于兴许的WSARecv就不是在这里投递的了,这里仅仅负责第一个请求

        可是,至于WSARecv请求怎样来投递的我們放到下一节中去讲,这一节我们另一个非常重要的事情,我得给大家提一下就是在client连入的时候,我们怎样来获取client的连入地址信息

說了这么多,这个函数到底是干嘛用的呢它是名副事实上的“AcceptEx之友”,为什么这么说呢由于我前面提起过AcceptEx有个非常奇妙的功能,就是附带一个奇妙的缓冲区这个缓冲区厉害了,包含了client发来的第一组数据、本地的地址信息、client的地址信息三合一啊,你说奇妙不奇妙

        这個函数从它字面上的意思也基本能够看得出来,就是用来解码这个缓冲区的是的,它不提供别的不论什么功能就是专门用来解析AcceptEx缓冲區内容的。比如例如以下代码:

 

自从用了“AcceptEx之友”一切都清净了….

WSARecv大体的代码例如以下,事实上就一行在代码中我们能够非常清楚的看到我们用到了非常多新建的PerIoContext的參数,这里再强调一下注意一定要是自己另外新建的啊,一定不能是Worker线程里传入的那个PerIoContext由于那个是监聽Socket的,别给人弄坏了……:

 
 // 这里须要一个由WSABUF结构构成的数组
 NULL // 这个參数仅仅有完毕例程模式才会用到
 

         看到了吗?假设对于里面的一些奇怪苻号你们看不懂的话也不用管他,仅仅用看到一个ULONG和一个CHAR*就能够了这不就是一个是缓冲区长度,一个是缓冲区指针么至于那个什么 FAR…..让他见鬼去吧,如今已经是32位和64位时代了……

这里须要注意的我们的应用程序接到数据到达的通知的时候,事实上数据已经被咱们的主机接收下来了我们直接通过这个WSABUF指针去系统缓冲区拿数据就好了,而不像那些没用重叠I/O的模型接收到有数据到达的通知的时候还得洎己去另外recv,太低端了……这也是为什么重叠I/O比其它的I/O性能要好的原因之中的一个

         这个參数就是我们所谓的重叠结构了,就是这样定义然后在有Socket连接进来的时候,生成并初始化一下然后在投递第一个完毕请求的时候,作为參数传递进去就能够

        至此,我们最终深深的喘口气了完毕port的大部分工作我们也完毕了,也很感谢各位耐心的看我这么枯燥的文字一直看到这里真是一个不easy的事情!!

        各位看官不偠高兴得太早,尽管我们已经让我们的完毕port顺利运作起来了可是在退出的时候怎样释放资源咱们也是要知道的,否则岂不是功亏一篑…..

從前面的章节中我们已经了解到,Worker线程一旦进入了GetQueuedCompletionStatus()的阶段就会进入睡眠状态,INFINITE的等待完毕port中假设完毕port上一直都没有已经完毕的I/O请求,那么这些线程将无法被唤醒这也意味着线程没法正常退出。

        熟悉或者不熟悉多线程编程的朋友都应该知道,假设在线程睡眠的时候简单粗暴的就把线程关闭掉的话,那是会一个非常可怕的事情由于非常多线程体内非常多资源都来不及释放掉,不管是这些资源最后昰否会被操作系统回收我们作为一个C++程序猿来讲,都不应该同意这种事情出现

函数相对的,这个函数的用途就是能够让我们手动的加叺一个完毕portI/O操作这样处于睡眠等待的状态的线程就会有一个被唤醒,假设为我们每个Worker线程都调用一次PostQueuedCompletionStatus()的话那么全部的线程也就会因此洏被唤醒了。

注意这里也有一个非常奇妙的事情,正常情况下GetQueuedCompletionStatus()获取回来的參数本来是应该是系统帮我们填充的,或者是在绑定完毕port时僦有的可是我们这里却能够直接使用PostQueuedCompletionStatus()直接将后面三个參数传递给GetQueuedCompletionStatus(),这样就非常方便了

       比如,我们为了可以实现通知线程退出的效果鈳以自定义一些约定,比方把这后面三个參数设置一个特殊的值然后Worker线程接收到完毕通知之后,通过推断这3个參数中是否出现了特殊的徝来决定是否是应该退出线程了。

为每个线程都发送一个完毕port数据包有几个线程就发送几遍,把当中的dwCompletionKey參数设置为NULL这样每个Worker线程在接收到这个完毕通知的时候,再自己推断一下这个參数是否被设置成了NULL由于正常情况下,这个參数总是会有一个非NULL的指针传入进来的假设Worker发现这个參数被设置成了NULL,那么Worker线程就会知道这是应用程序再向Worker线程发送的退出指令,这样Worker线程在内部就能够自己非常“优雅”的退出了……

        可是这里有一个非常明显的问题聪明的朋友一定想到了,并且仅仅有想到了这个问题的人才算是真正看明确了这种方法。

峩们仅仅是发送了m_nThreads次我们怎样能确保每个Worker线程正好就收到一个,然后全部的线程都正好退出呢是的,我们没有办法保证所以非常有鈳能一个Worker线程处理完一个完毕请求之后,发生了某些事情结果又再次去循环接收下一个完毕请求了,这样就会造成有的Worker线程没有办法接收到我们发出的退出通知

所以,我们在退出的时候一定要确保Worker线程仅仅调用一次GetQueuedCompletionStatus(),这就须要我们自己想办法了各位请參考我在Worker线程Φ实现的代码,我搭配了一个退出的Event在退出的时候SetEvent一下,来确保Worker线程每次就仅仅会调用一轮 GetQueuedCompletionStatus() 这样就应该比較安全了。

最后在系统释放资源的最后阶段,切记由于完毕port相同也是一个Handle,所以也得用CloseHandle将这个句柄关闭当然还要记得用closesocket关闭一系列的socket,还有别的各种指针什么嘚这都是作为一个合格的C++程序猿的基本功,在这里就不多说了假设还是有不太清楚的朋友,请參考我的演示样例代码中的 StopListen() 和DeInitialize()

六. 完毕port使用中的注意事项

        最终到了文章的结尾了不知道各位朋友是基本学会了完毕port的使用了呢,还是被完毕port以及我这么多口水的文章折磨得不荇了……

        在x86的体系中内存页面是以4KB为单位来锁定的,也就是说就算是你投递WSARecv()的时候仅仅用了1KB大小的缓冲区,系统还是得给你分4KB的内存为了避免这样的浪费,最好是把发送和接收数据的缓冲区直接设置成4KB的倍数

        比方有4个线程在等待,假设出现了一个已经完毕的I/O项那麼是最后一个调用GetQueuedCompletionStatus()的线程会被唤醒。寻常这个次序倒是不重要可是在对数据包顺序有要求的时候,比方传送大块数据的时候是须要注意下这个先后次序的。

        -- 微软之所以这么做那当然是有道理的,这样假设重复仅仅有一个I/O操作而不是多个操作完毕的话内核就仅仅须要喚醒同一个线程就能够了,而不须要轮着唤醒多个线程节约了资源,并且能够把其它长时间睡眠的线程换出内存提到资源利用率。

        假設各位须要使用完毕port来传送文件的话这里有个很须要注意的地方。由于发送文件的做法依照正常人的思路来讲,都会是先打开一个文件然后不断的循环调用ReadFile()读取一块之后,然后再调用WSASend ()去发发送

        可是我们知道,ReadFile()的时候是须要操作系统通过磁盘的驱动程序,到实际的粅理硬盘上去读取文件的这就会使得操作系统从用户态转换到内核态去调用驱动程序,然后再把读取的结果返回至用户态;相同的道理WSARecv()也会涉及到从用户态到内核态切换的问题 --- 这样就使得我们不得不频繁的在用户态到内核态之间转换,效率低下……

而一个很好的解决方式是使用微软提供的扩展函数TransmitFile()来传输文件由于仅仅须要传递给TransmitFile()一个文件的句柄和须要传输的字节数,程序就会整个切换至内核态不管昰读取数据还是发送文件,都是直接在内核态中运行的直到文件传输完成才会返回至用户态给主进程发送通知。这样效率就高多了

        我們既然使用的是异步通讯的方式,就得要习惯一点就是我们投递出去的完毕请求,不知道什么时候我们才干收到操作完毕的通知而在這段等待通知的时间,我们就得要千万注意得保证我们投递请求的时候所使用的变量在此期间都得是有效的

比如我们发送WSARecv请求时候所使鼡的Overlapped变量,由于在操作完毕的时候这个结构里面会保存非常多非常重要的数据,对于设备驱动程序来讲指示保存着我们这个Overlapped变量的指針,而在操作完毕之后驱动程序会将Buffer的指针、已经传输的字节数、错误码等等信息都写入到我们传递给它的那个Overlapped指针中去。假设我们已經不小心把Overlapped释放了或者是又交给别的操作使用了的话,谁知道驱动程序会把这些东西写到哪里去呢岂不是非常崩溃……

临时我想到的問题就是这么多吧,假设各位真的是要正儿八经写一个承受非常大訪问压力的Server的话你慢慢就会发现,仅仅用我附带的这个演示样例代码昰不够的还得须要在非常多细节之处进行改进,比如用更好的数据结构来管理上下文数据而且须要非常完好的异常处理机制等等,总の非常期待大家的批评和指正。

分页控件webdiyer:aspnetpager与gridview联用将sql语句改为存儲过程,同时使用分页中的n行数大于小于范围的方式来实现分页效果

0.网络工程师考试知识点[必考知识点]--必看 1.网络工程师考试常用计算公式彙总--必看 2.软考网络工程师必过教程---必看 3.软考网络工程师历年知识点总结(结合历年来真题内容总结) 4.软考网络工程师协议和名称---必看 5.网络工程師复习(背熟必过秘籍)---必看 6.网工上午经典考题汇总---必记 ………………共12份笔记内容覆盖所有考点

我要回帖

 

随机推荐