完毕port(CompletionPort)具体解释手把手教你玩转网络编程系列之三zfyouxi

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

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

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

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

本文配套的演示样例源代码下载地址(在我的下载空间里,已经补充上了client的代码)

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

OK,Let’sgo!Havefun!

文件夹:

1.完毕port的长处

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

4.完毕port的基本流程

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

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

一.完毕port的长处

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

2.完毕port和其它网络通信方式最大的差别在哪里呢?

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

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

3.完毕port被广泛的应用于各个高性能server程序上,比如著名的Apache….假设你想要编写的server端须要同一时候处理的并发client连接数量有数百上千个的话,那不用纠结了,就是它了。

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

首先,我们先来看一下完毕port在笔者的PC机上的执行表现,笔者的PC配置例如以下:

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

如图2所看到的,是client3万个并发线程发送共发送9万条数据的log截图

图3是server端接收完成3万个并发线程和每一个线程的3份数据后的log截图

最关键是图4,图4是server端在接收到28000个并发线程的时候,CPU占用率的截图,使用的软件是大名鼎鼎的ProcessExplorer,由于相对来讲这个比自带的任务管理器要准确和精确一些。

我们能够发现一个令人吃惊的结果,採用了完毕port的Server程序(蓝色横线所看到的)所占用的CPU才为3.82%,整个执行过程中的峰值也没有超过4%,是相当气定神闲的……哦,对了,这还是在Debug环境下执行的情况,假设採用Release方式执行,性能肯定还会更高一些,除此以外,在UI上显示信息也非常大成都上影响了性能。

相反採用了多个并发线程的Client程序(紫色横线所看到的)竟然占用的CPU高达11.53%,甚至超过了Server程序的数倍……

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

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

3.1异步通信机制及其几种实现方式的比較

我们从前面的文字中了解到,高性能server程序使用异步通信机制是必须的。

而对于异步的概念,为了方便后面文字的理解,这里还是再次简单的描写叙述一下:

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

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

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

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

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

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

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

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

3.2重叠结构(OVERLAPPED)

我们从上一小节中得知,要实现异步通信,必需要用到一个非常风骚的I/O数据结构,叫重叠结构“Overlapped”,Windows里全部的异步通信都是基于它的,完毕port也不例外。

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

你要是实在不理解这是个什么玩意,那就直接看后面的代码吧,慢慢就明确了……

3.3完毕port(CompletionPort)

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

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

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

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

说了这么多的废话,大家都等不及了吧,我们最终到了详细编码的时候了。

使用完毕port,说难也难,可是说简单,事实上也简单----又说了一句废话=。=

大体上来讲,使用完毕port仅仅用遵循例如以下几个步骤:

(1)调用CreateIoCompletionPort()函数创建一个完毕port,并且在普通情况下,我们须要且仅仅须要建立这一个完毕port,把它的句柄保存好,我们今后会经经常使用到它……

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

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

(4)每当有client连入的时候,我们就还是得调用CreateIoCompletionPort()函数,这里却不是新建立完毕port了,而是把新连入的Socket(也就是前面所谓的设备句柄),与眼下的完毕port绑定在一起。

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

(6)而此时,我们预先准备的那几个Worker线程就不能闲着了,我们在前面建立的几个Worker就要忙活起来了,都须要分别调用GetQueuedCompletionStatus()函数在扫描完毕port的队列里是否有网络通信的请求存在(比如读取数据,发送数据等),一旦有的话,就将这个请求从完毕port的队列中取回来,继续运行本线程中后面的处理代码,处理完毕之后,我们再继续投递下一个网络通信的请求就OK了,如此循环。

关于完毕port的使用步骤,用文字来表述就是这么多了,非常easy吧?假设你还是不理解,我再配合一个流程图来表示一下:

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

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

採用accept方式的流程示意图例如以下:

採用AcceptEx方式的流程示意图例如以下:

两个图中最大的同样点是什么?是的,最大的同样点就是主线程无所事事,闲得蛋疼……

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

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

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

读者一定会问,这样做的优点在哪里?为什么还要异步的投递AcceptEx连接的操作呢?

依照我们眼下主流的PC来讲,假设client仅仅进行连接请求,而什么都不做的话,我们的Server仅仅能接收大约3万-4万个左右的并发连接,然后client其余的连入请求就仅仅能收到WSAENOBUFS(10055)了,由于系统来不及为新连入的client准备资源了。

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

而AcceptEx比Accept又强大在哪里呢?是有三点:

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

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

最后,各位要有一个心里准备,相比accept,异步的AcceptEx使用起来要麻烦得多……

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

又说了一节的废话,最终到了该动手实现的时候了……

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

【第一步】创建一个完毕port

首先,我们先把完毕port建好再说。

我们正常情况下,我们须要且仅仅须要建立这一个完毕port,代码非常easy:

HANDLEm_hIOCompletionPort=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);

有的时候我真的非常赞叹WindowsAPI的封装,把非常多事实上是非常复杂的事整得这么简单……

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

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

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

【第二步】依据系统中CPU核心的数量建立相应的Worker线程

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

SYSTEM_INFOsi;GetSystemInfo(&si);intm_nProcessors=si.dwNumberOfProcessors;

这样我们依据系统中CPU的核心数量来建立相应的线程就好了,下图是在我的i72600kCPU上初始化的情况,由于我的CPU是8核,一共启动了16个Worker线程,例如以下图所看到的

啊,等等!各位没发现什么问题么?为什么我8核的CPU却启动了16个线程?这个不是和我们第二步中说的原则自相矛盾了么?

哈哈,有个小秘密忘了告诉各位了,江湖上都流传着这么一个公式,就是:

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

这里也有人说是建立CPU“核心数量*2+2”个线程,我想这个应该没有什么太大的差别,我就是依照我自己的习惯来了。

然后依照这个数量,来启动这么多个Worker线程就好能够了,接下来我们開始下一个步骤。

什么?Worker线程不会建?

…囧…

Worker线程和普通线程是一样一样一样的啊~~~,代码大致上例如以下:

//依据CPU数量,建立*2的线程m_nThreads=2*m_nProcessors;HANDLE*m_phWorkerThreads=newHANDLE[m_nThreads];for(inti=0;i

当中,_WorkerThread是Worker线程的线程函数,线程函数的详细内容我们后面再讲。

【第三步】创建一个用于监听的Socket,绑定到完毕port上,然后開始在指定的port上监听连接请求

最重要的完毕port建立完毕了,我们就能够利用这个完毕port来进行网络通信了。

首先,我们须要初始化Socket,这里和通常情况下使用Socket初始化的步骤都是一样的,大约就是例如以下的这么几个过程(详情參照我代码中的LoadSocketLib()和InitializeListenSocket(),这里仅仅是挑出关键部分):

//初始化Socket库WSADATAwsaData;WSAStartup(MAKEWORD(2,2),&wsaData);//初始化Socketstructsockaddr_inServerAddress;//这里需要特别注意,假设要使用重叠I/O的话,这里必需要使用WSASocket来初始化Socket//注意里面有个WSA_FLAG_OVERLAPPED參数SOCKETm_sockListen=WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);//填充地址结构信息ZeroMemory((char*)&ServerAddress,sizeof(ServerAddress));ServerAddress.sin_family=AF_INET;//这里能够选择绑定不论什么一个可用的地址,或者是自己指定的一个IP地址//ServerAddress.sin_addr.s_addr=htonl(INADDR_ANY);ServerAddress.sin_addr.s_addr=inet_addr(“你的IP”);ServerAddress.sin_port=htons(11111);//绑定端口if(SOCKET_ERROR==bind(m_sockListen,(structsockaddr*)&ServerAddress,sizeof(ServerAddress)))//開始监听listen(m_sockListen,SOMAXCONN))

须要注意的地方有两点:

(1)想要使用重叠I/O的话,初始化Socket的时候一定要使用WSASocket并带上WSA_FLAG_OVERLAPPED參数才干够(仅仅有在server端须要这么做,在client是不须要的);

(2)注意到listen函数后面用的那个常量SOMAXCONN了吗?这个是在微软在WinSock2.h中定义的,而且还附赠了一条凝视,Maximumqueuelengthspecifiablebylisten.,所以说,不用白不用咯^_^

接下来有一个很重要的动作:既然我们要使用完毕port来帮我们进行监听工作,那么我们一定要把这个监听Socket和完毕port绑定才干够的吧:

怎样绑定呢?相同非常easy,用CreateIoCompletionPort()函数。

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

说实话,我感觉微软应该把这两个函数分开,弄个CreateNewCompletionPort()多好呢?

这里在具体解说一下CreateIoCompletionPort()的几个參数:

HANDLEWINAPICreateIoCompletionPort(__inHANDLEFileHandle,//这里当然是连入的这个套接字句柄了__in_optHANDLEExistingCompletionPort,//这个就是前面创建的那个完毕port__inULONG_PTRCompletionKey,//这个參数就是相似于线程參数一样,在//绑定的时候把自定义的结构体指针传递//这样到了Worker线程中,也能够使用这个//结构体的数据了,相当于參数的传递__inDWORDNumberOfConcurrentThreads//这里相同置0);

这些參数也没什么好讲的吧,用处一目了然了。而对于当中的那个CompletionKey,我们后面会具体提到。

到此才算是Socket所有初始化完成了。

初始化Socket完成之后,就能够在这个Socket上投递AcceptEx请求了。

【第四步】在这个监听Socket上投递AcceptEx请求

这里的处理比較复杂。

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

微软的实现是通过mswsock.dll中提供的,所以我们能够通过静态链接mswsock.lib来使用AcceptEx。可是这是一个不推荐的方式,我们应该用WSAIoctl配合SIO_GET_EXTENSION_FUNCTION_POINTER參数来获取函数的指针,然后再调用AcceptEx。

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

获取AcceptEx函数指针的代码大致例如以下:

LPFN_ACCEPTEXm_lpfnAcceptEx;//AcceptEx函数指针GUIDGuidAcceptEx=WSAID_ACCEPTEX;//GUID,这个是识别AcceptEx函数必须的DWORDdwBytes=0;WSAIoctl(m_pListenContext->m_Socket,SIO_GET_EXTENSION_FUNCTION_POINTER,&GuidAcceptEx,sizeof(GuidAcceptEx),&m_lpfnAcceptEx,sizeof(m_lpfnAcceptEx),&dwBytes,NULL,NULL);

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

然后,我们就能够通过当中的指针m_lpfnAcceptEx调用AcceptEx函数了。

AcceptEx函数的定义例如以下:

BOOLAcceptEx(SOCKETsListenSocket,SOCKETsAcceptSocket,PVOIDlpOutputBuffer,DWORDdwReceiveDataLength,DWORDdwLocalAddressLength,DWORDdwRemoteAddressLength,LPDWORDlpdwBytesReceived,LPOVERLAPPEDlpOverlapped);

乍一看起来參数非常多,可是实际用起来也非常easy:

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

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

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

这里的标志就是例如以下这种结构体:

typedefstruct_PER_IO_CONTEXT{OVERLAPPEDm_Overlapped;//每个重叠I/O网络操作都要有一个SOCKETm_sockAccept;//这个I/O操作所使用的Socket,每个连接的都是一样的WSABUFm_wsaBuf;//存储数据的缓冲区,用来给重叠操作传递參数的,关于WSABUF后面还会讲charm_szBuffer[MAX_BUFFER_LEN];//相应WSABUF里的缓冲区OPERATION_TYPEm_OpType;//标志这个重叠I/O操作是做什么的,比如Accept/Recv等}PER_IO_CONTEXT,*PPER_IO_CONTEXT;

这个结构体的成员当然是我们随便定义的,里面的成员你能够任意改动(除了OVERLAPPED那个之外……)。

可是AcceptEx不是普通的accept,buffer不是普通的buffer,那么这个结构体当然也不能是普通的结构体了……

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

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

typedefstruct_PER_SOCKET_CONTEXT{SOCKETm_Socket;//每个client连接的SocketSOCKADDR_INm_ClientAddr;//这个client的地址CArray<_PER_IO_CONTEXT*>m_arrayIoContext;//数组,全部clientIO操作的參数,//也就是说对于每个clientSocket//是能够在上面同一时候投递多个IO请求的}PER_SOCKET_CONTEXT,*PPER_SOCKET_CONTEXT;

这也是比較好理解的,也就是说我们须要在一个Socket句柄上,管理在这个Socket上投递的每个IO请求的_PER_IO_CONTEXT。

至于详细这两个结构体參数是怎样在Worker线程里大发神威的,我们后面再看。

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

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

【第五步】我们再来看看Worker线程都做了些什么

_Worker线程的工作都是涉及到详细的通信事务问题,主要完毕了例如以下的几个工作,让我们一步一步的来看。

(1)使用GetQueuedCompletionStatus()监控完毕port

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

void*lpContext=NULL;OVERLAPPED*pOverlapped=NULL;DWORDdwBytesTransfered=0;BOOLbReturn=GetQueuedCompletionStatus(pIOCPModel->m_hIOCompletionPort,&dwBytesTransfered,(LPDWORD)&lpContext,&pOverlapped,INFINITE);

一旦完毕port上出现了已完毕的I/O请求,那么等待的线程会被立马唤醒,然后继续运行兴许的代码。

至于这个奇妙的函数,原型是这种:

所以,假设这个函数突然返回了,那就说明有须要处理的网络操作了---当然,在没有出现错误的情况下。

然后switch()一下,依据须要处理的操作类型,那我们来进行对应的处理。

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

可是还有问题,这个參数到底是从哪里传进来的呢?传进来的时候内容都有些什么?

这个问题问得好!

首先,我们要知道两个关键点:

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

你细致想想,事实上真的非常奇妙……

可是要做到这样的奇妙的效果,应该确保我们在结构体PER_IO_CONTEXT定义的时候,把Overlapped变量,定义为结构体中的第一个成员。

仅仅要各位能弄清楚这个GetQueuedCompletionStatus()中各种奇怪的參数,那我们就离成功不远了。

既然我们能够获得PER_IO_CONTEXT结构体,那么我们就自然能够依据当中的m_OpType參数,得知这次收到的这个完毕通知,是关于哪个Socket上的哪个I/O操作的,这样就分别进行相应处理就好了。

在我的演示样例代码里,在有AcceptEx请求完毕的时候,我是运行的_DoAccept()函数,在有WSARecv请求完毕的时候,运行的是_DoRecv()函数,以下我就分别解说一下这两个函数的运行流程。

【第六步】当收到Accept通知时_DoAccept()

在用户收到AcceptEx的完毕通知时,须要兴许代码并不多,但却是逻辑最为混乱,最easy出错的地方,这也是非常多用户为什么宁愿用效率低下的accept()也不愿意去用AcceptEx的原因吧。

和普通的Socket通讯方式一样,在有client连入的时候,我们须要做三件事情:

(1)为这个新连入的连接分配一个Socket;

(2)在这个Socket上投递第一个异步的发送/接收请求;

(3)继续监听。

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

(2)所以,AcceptEx不是给咱们新连入的这个Socket早就建好了一个Socket吗?所以这里,我们须要再用这个新Socket又一次为新client建立一个PER_SOCKET_CONTEXT,以及以下一系列的新PER_IO_CONTEXT,千万不要去动传入的这个ListenSocket上的PER_SOCKET_CONTEXT,也不要用传入的这个Overlapped信息,由于这个是属于AcceptExI/O操作的,也不是属于你投递的那个RecvI/O操作的……,要不你下次继续监听的时候就悲剧了……

(3)等到新的Socket准备完成了,我们就赶紧还是用传入的这个ListenSocket上的PER_SOCKET_CONTEXT和PER_IO_CONTEXT去继续投递下一个AcceptEx,循环起来,留在这里太危急了,早晚得被人给改了……

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

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

这里我们还须要引入另外一个非常高端的函数,GetAcceptExSockAddrs(),它和AcceptEx()一样,都是微软提供的扩展函数,所以相同须要通过以下的方式来导入才干够使用……

WSAIoctl(m_pListenContext->m_Socket,SIO_GET_EXTENSION_FUNCTION_POINTER,&GuidGetAcceptExSockAddrs,sizeof(GuidGetAcceptExSockAddrs),&m_lpfnGetAcceptExSockAddrs,sizeof(m_lpfnGetAcceptExSockAddrs),&dwBytes,NULL,NULL);

和导出AcceptEx一样一样的,相同是须要用其GUID来获取相应的函数指针m_lpfnGetAcceptExSockAddrs。

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

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

PER_IO_CONTEXT*pIoContext=本次通信用的I/OContextSOCKADDR_IN*ClientAddr=NULL;SOCKADDR_IN*LocalAddr=NULL;intremoteLen=sizeof(SOCKADDR_IN),localLen=sizeof(SOCKADDR_IN);m_lpfnGetAcceptExSockAddrs(pIoContext->m_wsaBuf.buf,pIoContext->m_wsaBuf.len-((sizeof(SOCKADDR_IN)+16)*2),sizeof(SOCKADDR_IN)+16,sizeof(SOCKADDR_IN)+16,(LPSOCKADDR*)&LocalAddr,&localLen,(LPSOCKADDR*)&ClientAddr,&remoteLen);

解码完成之后,于是,我们就能够从例如以下的结构体指针中获得非常多有趣的地址信息了:

inet_ntoa(ClientAddr->sin_addr)是clientIP地址

ntohs(ClientAddr->sin_port)是client连入的端口

inet_ntoa(LocalAddr->sin_addr)是本地IP地址

ntohs(LocalAddr->sin_port)是本地通讯的端口

pIoContext->m_wsaBuf.buf是存储client发来第一组数据的缓冲区

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

【第七步】当收到Recv通知时,_DoRecv()

在解说怎样处理Recv请求之前,我们还是先讲一下怎样投递WSARecv请求的。

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

intnBytesRecv=WSARecv(pIoContext->m_Socket,pIoContext->p_wbuf,1,&dwBytes,0,pIoContext->p_ol,NULL);

这里,我再把WSARev函数的原型再给各位讲一下

intWSARecv(SOCKETs,//当然是投递这个操作的套接字LPWSABUFlpBuffers,//接收缓冲区//这里须要一个由WSABUF结构构成的数组DWORDdwBufferCount,//数组中WSABUF结构的数量,设置为1就可以LPDWORDlpNumberOfBytesRecvd,//假设接收操作马上完毕,这里会返回函数调用所接收到的字节数LPDWORDlpFlags,//说来话长了,我们这里设置为0就可以LPWSAOVERLAPPEDlpOverlapped,//这个Socket相应的重叠结构NULL//这个參数仅仅有完毕例程模式才会用到,//完毕port中我们设置为NULL就可以);

这里是须要我们自己new一个WSABUF的结构体传进去的;

假设你们非要追问WSABUF结构体是个什么东东?我就给各位多说两句,就是在ws2def.h中有定义的,定义例如以下:

typedefstruct_WSABUF{ULONGlen;/*thelengthofthebuffer*/__field_bcount(len)CHARFAR*buf;/*thepointertothebuffer*/}WSABUF,FAR*LPWSABUF;

并且好心的微软还附赠了凝视,真不easy….

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

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

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

OVERLAPPED*m_pol=newOVERLAPPED;ZeroMemory(m_pol,sizeof(OVERLAPPED));

在第一个重叠请求完成之后,我们的这个OVERLAPPED结构体里,就会被分配有效的系统參数了,而且我们是须要每个Socket上的每个I/O操作类型,都要有一个唯一的Overlapped结构去标识。

这样,投递一个WSARecv就讲完了,至于_DoRecv()须要做些什么呢?事实上就是做两件事:

(1)把WSARecv里这个缓冲区里收到的数据显示出来;

(2)发出下一个WSARecv();

Over……

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

【第八步】怎样关闭完毕port

歇息完成,我们继续……

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

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

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

所以我们必须得有一个非常优雅的,让线程自己退出的办法。

这时会用到我们这次见到的与完毕port有关的最后一个API,叫PostQueuedCompletionStatus(),从名字上也能看得出来,这个是和GetQueuedCompletionStatus()函数相对的,这个函数的用途就是能够让我们手动的加入一个完毕portI/O操作,这样处于睡眠等待的状态的线程就会有一个被唤醒,假设为我们每个Worker线程都调用一次PostQueuedCompletionStatus()的话,那么全部的线程也就会因此而被唤醒了。

PostQueuedCompletionStatus()函数的原型是这样定义的:

BOOLWINAPIPostQueuedCompletionStatus(__inHANDLECompletionPort,__inDWORDdwNumberOfBytesTransferred,__inULONG_PTRdwCompletionKey,__in_optLPOVERLAPPEDlpOverlapped);

我们能够看到,这个函数的參数差点儿和GetQueuedCompletionStatus()的一模一样,都是须要把我们建立的完毕port传进去,然后后面的三个參数是传输字节数、结构体參数、重叠结构的指针.

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

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

比如我们在调用的时候,就能够这样:

for(inti=0;i

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

学会了吗?

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

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

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

另外,在Vista/Win7系统中,我们另一个更简单的方式,我们能够直接CloseHandle关掉完毕port的句柄,这样全部在GetQueuedCompletionStatus()的线程都会被唤醒,而且返回FALSE,这时调用GetLastError()获取错误码时,会返回ERROR_INVALID_HANDLE,这样每个Worker线程就能够通过这样的方式轻松简单的知道自己该退出了。当然,假设我们不能保证我们的应用程序仅仅在Vista/Win7中,那还是老老实实的PostQueuedCompletionStatus()吧。

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

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

最后再补充一些前面没有提到了,实际应用中的一些注意事项吧。

1.Socket的通信缓冲区设置成多大合适?

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

2.关于完毕port通知的次序问题

这个不用想也能知道,调用GetQueuedCompletionStatus()获取I/O完毕port请求的时候,肯定是用先入先出的方式来进行的。

可是,咱们大家可能都想不到的是,唤醒那些调用了GetQueuedCompletionStatus()的线程是以后入先出的方式来进行的。

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

3.假设各位想要传输文件…

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

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

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

4.关于重叠结构数据释放的问题

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

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

THE END
1.今日最黑:面试取消原因面试新浪财经今日最黑:面试取消原因 秒懂的,点这里http://finance.sina.com.cn/wm/2024-11-20/doc-incwsues2187540.shtml
2.[今日环球]多家伊朗相关媒体网站遭美国政府强行关闭[今日环球]未来产业 进博看新 解锁未来出行的N种可能 [今日环球]第七届进博会迎来集中签约高峰 [今日环球]立冬时节 各地美食齐“补冬” [今日环球]中国多地迎来降雪天气 [今日环球]国产C919大飞机首次巡展澳门 [今日环球]第十五届中国航展开展在即 红-19地空导弹武器系统首次展出 [今日环球]第十五届中...http://m.app.cctv.com/vsetv/detail/VSET100439687580/a8645cddc1d94d91ae42dc03230a6cb7/index.shtml
3.职场信息平台#看准平台宣布关闭服务#9...来自中国网财经【职场信息平台#看准平台宣布关闭服务#】9月5日,看准运营团队发布通知:由于业务调整,看准平台(包括看准APP、看准小程序、看准网)将于2024年9月30日23:59:59关闭现有服务。服务关闭后,除法律法规另有规定或看准平台与您所签署的协议另有约定外,看准平台所有的账号数据及信息将会做删除处理。(财联社) ...https://weibo.com/2377587254/OvB8KaUa6
4.职场信息平台“看准”宣布App/小程序/看准网9月30日关闭...IT之家 9 月 5 日消息,职场信息平台“看准”9 月 4 日在其官网、App 等平台发布通知称,由于业务调整,看准平台(包括看准 App、看准小程序、看准网)将于 2024 年 9 月 30 日 23:59:59 关闭现有服务。 在此之前,用户仍可以正常访问上述平台,并对个人账号数据进行处理。用户可以通过“导出个人信息”功能...https://finance.sina.cn/tech/2024-09-05/detail-incnaxxk8948782.d.html
5.职场信息平台看准网宣布9月30日关闭服务【职场信息平台看准网宣布9月30日关闭服务】财联社9月5日电,看准运营团队发布通知:由于业务调整,看准平台(包括看准APP、看准小程序、看准网)将于2024年9月30日23:59:59关闭现有服务。服务关闭后,除法律法规另有规定或看准平台与您所签署的协议另有约定外,看准平台所有的账号数据及信息将会做删除处理。https://finance.ifeng.com/c/8cdNEyuBtQm
6.读懂总书记两会金句中的重要指引工作动态上饶市人民政府当前,中国经济前行以及实现突破的方向是十分明确的,习近平总书记强调“看准了就抓紧干”“真正行动”,就是号召大家抓住机遇、脚踏实地,努力以自身工作的确定性应对形势变化的不确定性,稳步实现既定战略目标。 “在发展中稳步提升民生保障水平” 习近平总书记去年在参加江苏代表团审议时强调“人民幸福安康是推动高质量发展...https://www.zgsr.gov.cn/wjw/gzdt/202403/9a8d94543a1a44dbb85483564d03db89.shtml
7.电脑基础常识和必备技巧大全8、完成上面的设置后,再用鼠标依次单击“关闭”按钮退出设置界面,并单击“确定”按钮完成无线局域网的无线连接设置工作,要是参数设置正确的话,系统会自动出现无线网络连接已经成功的提示。笔记本已经连接无线局域网了. 设置好IP就可以无线上网了. 在有效范围内都可以连接. ...http://openvpn.danzhao1.cn/show-496836.html
1.英国智库:英国就业数据有误近百万人从数据中消失格隆汇11月20日|据英国智库决议基金会(Resolution Foundation)称,由于英国国家统计局(ONS)的重大失误,近一百万英国工人从官方就业数据中消失,这使得英国央行更难控制通胀。该智库指责ONS公布的数据“质量差”且不可靠,歪曲了就业市场,夸大了失业危机。据智库经济学家计算,自2019年以来,统计人员低估了93万名工人的就业...https://www.gelonghui.com/live/1764830
2.港股开盘:恒指跌0.30%,科指跌0.45%,科网股多数走低快手绩后跌超5%a...中信建投:随着近期港股下行以及港股与A股走势的分化,港股估值与AH溢价再次体现高性价比。当前,特朗普胜选对港股走势造成冲击,但中期看,特朗普的政策主张有利于港股流动性。因此,在短期冲击结束之后,港股或迎来上涨行情,当下是布局港股极具性价比的时机,科网板块最值得推荐。 https://www.163.com/dy/article/JHGPONSP0519QIKK.html
3.看准网凉了,公司点评只能悄悄的?看准运营团队发布重要通知,宣布由于业务调整的需要,看准平台(涵盖看准APP、看准小程序及看准网)将于2024年9月30日23:59:59正式关闭其现有服务。 看准小程序现有 END 阅读18 声明:本文内容由脉脉用户自发贡献,部分内容可能整编自互联网,版权归原作者所有,脉脉不拥有其著作权,亦不承担相应法律责任。如果您发现有...https://maimai.cn/article/detail?fid=1846140039&efid=ttLH273foxqYShJBfC4JMg
4.看准网为什么要关闭服务了?可能因为这些平台的评价,影响的是同业竞争,看准网影响的是社会形象 赞(4) 回应 momo 2024-09-26 15:47:42 福建 可能因为这些平台的评价,影响的是同业竞争,看准网影响的是社会形象 土豆 为啥会影响社会形象呀 赞 回应 康斯坦丁的烟 2024-09-26 15:50:23 广东 动了资本家的蛋糕呗 赞(5)...https://www.douban.com/group/topic/311761134/
5.看准网现在就是鸡肋,现在关闭了对公司步行街主干道看不了公司评价了,只能看面试评价。看准网咋想的呀,这下子这app一点用没有了。 步行街主干道 发布于上海阅读27760全部回复 chelseanash2021-01-28· 湖北 已经卸载了,如果是互联网行业直接去脉脉上看 亮了(0) 回复 撒油娜拉2021-01-28· 江西 还有什么别的能看的软件没 亮了(0) 查看回复(1) 回复展开全...https://m.hupu.com/bbs/40773120.html
6.职场信息平台“看准”宣布App/小程序/看准网9月30日关闭现有服务9月4日,看准平台在官网发布通知称,由于业务调整,看准平台(包括看准 App、看准小程序、看准网)将于2024年9月30日23:59:59关闭现有服务。 在此之前,用户仍可以正常访问上述平台,并对个人账号数据进行处理。用户可以通过“导出个人信息”功能,导出自己在看准平台上的个人信息和历史发布内容,平台将会通过邮件形式发...https://www.eet-china.com/mp/a345380.html
7.职场信息平台看准网宣布9月30日关闭服务9月5日讯,看准运营团队发...【职场信息平台看准网宣布9月30日关闭服务】9月5日讯,看准运营团队发布通知:由于业务调整,看准平台(包括看准APP、看准小程序、看准网)将于2024年9月30日23:59:59关闭现有服务。服务关闭后,除法律法规另有规定或看准平台与您所签署的协议另有约定外,看准平台所有的账号数据及信息将会做删除处理。 https://www.gold678.com/C/202409051809391059
8.网站行情不好知名招聘职场网站看准网倒闭了9月4日,国内知名的招聘网站看准网发布了关闭服务通知。 通知说明中,看准平台(包括看准APP、看准小程序、看准网)将于2024年9月30日23:59:59关闭现有服务。 这已经是全平台关闭了,看准网是不打算再运营这个网站了。 看准网(Kanzhun)是国内知名的雇主点评与职场信息平台,成立于2013年12月,并于2016年上线了...https://zhujib.com/wangzhanhangqingbuhaozhimingzh.html
9.洛克王国休息时间怎么关闭洛克王国几点就不能玩了可以百川。也可以加血。适当时候可以开唯我增加攻击力。如果被破魔拳置换宠物,齐天大圣可以当头棒喝物防-1.被烧伤,就分化身影(物攻+1)。看准机会大闹天宫(3次攻击)。能不能杀死修罗王就看你的运气了。 9、洛克王国镇狱修罗王解析(要有性格、技能、战斗的解析 ...https://www.773hf.com/wiki/18234.html
10....正则表达式字符串面试题面试问题:C#上位机开发…接下来我们运行看一下效果: 可以看到,当我们发送字符“1”的时候,状态栏显示接收到1byte数据,表明计数正常,但是接收到的却是字符形式的“49”,这是因为接收到的byte类型的数据存放的就是ASCII码值,而调用byte对象的ToString()方法,由下图可看到,这个方法刚好又将这个ASCII值49转化成为了字符串“49”,而不是对应的...https://blog.csdn.net/weixin_39918084/article/details/117080996
11.祝匡武那种一经拜师求鸽,即纳为门下;进出大计,要蒙师恩准,看其脸色;弟子将来飞绩,尽计在原主账上;打着鸽系旗帜,刻意张扬,网罗雏哥的行为,实令人不屑、不耻。求鸽的时候,做徒弟的时候,中规中矩,不乱说话,唯师傅马首是瞻,尚能理解。但鸽圈没有老师,鸽学永无止境。仰人鼻息,纵然唯诺一时,当你立身扬名之际,...http://gdgs.chinaxinge.com/gdgs/show/?id=16485&newsid=2114843
12.语文教师考编作文范文(推荐22篇)2.答题时看准题目 在回答阅读理解的题目是一定不要跑题,在这个位置,出题的老师很容易玩文字游戏。所以要认真阅读题目,不要把中心思想写成某一个词语的意思。还有一点重要的就是不要过分解读题目。一般小学的阅读理解不会问很深入的问题,有些同学可能读的课外读物很多,会有一些其他的解读。尽量不要把这些角度带到回...https://m.1314zhou.com/fanwen/qitafanwen/262998.html
13.2021江苏始准考证打印入口12月18日关闭5、《准考证》要妥善保存,笔试后如进入资格复审、面试、体检等程序时需出示此证,谨防丢失。 江苏公务员考试准考证打印后需要做什么? 1.第一时间看考试地点 打印准考证真的需要这么早吗?当然不需要了,上考场前手头有准考证就可以了。那为什么要第一时间就看准考证呢?因为这里有一个关键的信息——你的考试地点!准...http://m.js.offcn.com/html/2020/12/241263.html
14.有关小学生消防安全作文(通用8篇)体验浓烟逃生时我觉得模拟的烟像真的烟一样很难闻,很熏人,差点连安全出口指示的绿光都看不清,而且在最黑暗的地方我和别的同学失散了,我当时很想往回走,但想想可能已经接近出口了,所以我又摸索着匍匐前进,忽然我看到了一个白色的东西,我想应该是同学的衣服,然后白色的东西拐弯不见了,我可高兴了,因为我断定出口...https://www.360wenmi.com/f/file7c55o599.html
15.你好,投递通道关闭了吗没看清楚就点了确定,刚好那天有事 牛客5655:其他公司的面试(事)吗 点赞 评论 收藏 分享 昨天00:14 西北工业大学 HTML5 我拒绝了秋招唯一的offer 日常加班和离家太远的工作状态让我无法接受,哪怕现在是就业寒冬,我也不想妥协。于是,我选择了重新出发,冬招,我来了!虽然现在是0offer,但我相信,总...https://www.nowcoder.com/discuss/comment/13926981