本文对于初学网络编程的极为友好,文中所有代码全部基于C语言实现,文中见解仅限于作者对于完成端口的初步认识,由于作者才疏学浅,出现的错误和纰漏,麻烦您一定要指出来,咱们共同进步。谢谢!!!


完成端口(completion Port)

前言:

网络通信分为两种:同步和异步。

  在同步通信中,每一次接受数据都会导致主线程的挂起,从而阻塞住了其他操作。为了解决这一问题,我们通常会采取同步通信+多线程的策略,即为每一个连入的Socket分配一个线程。然而随着连入的Socket的数量的增加,线程的数量也在增加,这样CPU则需要不停地进行线程的切换,因此难以成为高性能的服务器程序。
  异步通信则可以把接收数据这一操作交给内核,即在内核接收数据的时候,主线程可以不用被阻塞并且继续执行其他操作,而一旦接收数据完成以后,再由内核通知主线程。而如何通知主线程是一个关键,不同的异步通信策略有着不同的通知方式。
  在这样的情况下,完成端口这一I/O模型被提出,成为目前Windows下性能最好的I/O模型之一。

 (注:文中所有函数参数均已MSDN上的为标准,文中观点仅代表个人理解,如有错误,还请多多包涵并及时留言,我会第一时间改正,谢谢!!!)

完成端口模型简介:

上面所说的“初学”指你已经熟悉Socket进行TCP/IP编程的基本原理,前期基本的概念我这里就略过不提了,直入主题。

嗯~~!怎么说呢,完成端口是Windows的一种机制,这种机制是在重叠IO上的优化,所以说完成端口也是基于重叠结构的,换句话说如果对于重叠IO结构特别熟悉的话,那么完成端口对于你来说就特别简单。为什么说完成端口是在重叠IO上的一种优化呢?对比一下下面第一张和第二转张结构图,一定会有人好奇,为什么两张图差不多一样呢?仔细看会发现完成端口结构图里面操作系统有一步操作是将通知放进队列(第三张结构图,模仿消息队列原理系统会创建一个通知队列)。到这就可以说明完成端口在重叠IO具体优化的是什么了,熟悉重叠IO的都知道,重叠IO最严重的问题就是线程数量,有多少的客户端,那就得有多少根线程。肯定会有人说线程多了不是更好吗?速度跟快吗?程序执行时间更短码?那就错了,恰恰是相反的,上面我也大致提到了线程太多的问题。了解操作系统的都知道,线程在一个周期内分得的时间越多,那么执行就越快。换而言之如果线程数量增加,那么每根线程上所分得的时间就会变短,再加上切换线程的时间,这样一来反而时间更久。而理论上最优的线程数就是和CPU核数一样(还有其他的几种:CPU核数*2、CPU核数*2+2。为什么会有这几种情况,这里就不多多介绍了。)这样以来就可以充分的利用CPU资源。不过这也要求线程函数中没有调用诸如Sleep(),WSAWaitForMultipleEvents()...这类函数,这类函数会使线程挂起(但不占cpu时间片),从而使得CPU某个核空闲了,这就不好了,所以一般我们多建个两三根,以解决此类情况,让CPU不停歇,从而在整体上保证程序执行效率。本文采取的是和CPU核数一样多。而对于重叠IO中的无序性问题,完成端口采用了上述所说的创建一个通知队列(第三张结构图)来进行管理,从而达到有序。所以说完成端口是对重叠IO的改进也不为过。

    

完成端口原理以及部分函数用法:

1.CreateIoCompletionPort()函数创建一个完成端口。

对于 CreateIoCompletionPort()函数它有两个功能一个功能是创建完成端口,另一个功能就是将SOCKET与完成端口进行绑定,在这里就是创建完成端口。至于说功能不一样,也就是参数不同而已。

 HANDLE WINAPI CreateIoCompletionPort(
__in HANDLE FileHandle,
__in_opt HANDLE ExistingCompletionPort,
__in ULONG_PTR CompletionKey,
__in DWORD NumberOfConcurrentThreads
);

参数(Parameters):

此函数若要是在不关联I/O完成端口的情况下创建I/O完成端口,如果指定了参数FileHandleINVALID_HANDLE_VALUE,在这种情况下,ExistingCompletionPort参数必须为NULL,而CompletionKey参数则被忽略可填0;那么参数NumberOfConcurrentThreads是允许此端口上最多同时运行的线程数量,一般设置为零(这里的零并不是参数3中忽略的意思,而是自动获取CPU核数。当然你也可以不用自动获取自己去指定通过函数GetSystemInfo())。

(注:这里简单介绍一下GetSystemInfo()函数的用法。这个函数也特别简单,参数也就一个SYSTEM_INFO类型的结构体,在这里我们只需要专注这个结构体里面的DWORD dwNumberOfProcessors成员即可; )

返回值(Return value):

函数执行成功会返回一个可用的端口变量,否则返回0;这里可以用GetLastError()获取错误码。
(注意:这里为什么不用WSAGetLastError()获取错误码?创建完成端口是Windows的一种机制,不是专门用于网络的,和网络是无关的。完成端口的模型只是利用了这种机制。)

2.用 CreateIoCompletionPort()函数将重叠套接字(客户端SOCKET+服务器SOCKET)与完成端口进行绑定。

毋庸置疑这就是CreateIoCompletionPort()函数的第二个功能:绑定重叠套接字与完成端口

 HANDLE WINAPI CreateIoCompletionPort(
__in HANDLE FileHandle,
__in_opt HANDLE ExistingCompletionPort,
__in ULONG_PTR CompletionKey,
__in DWORD NumberOfConcurrentThreads
);

参数(Parameters):

FileHandle:要绑定的SOCKET。
ExistingCompletionPort:创建完成端口时返回的变量。
CompletionKey:这个参数就要和下面即将讲到的一个函数GetQueuedCompletionStatus()的参数3关联在一起比较着看,会很清楚。
      先大概说一下GetQueuedCompletionStatus()这个函数,上面我也提到过系统会把所有SOCKET上的通知放进通知队列里面,而GetQueuedCompletionStatus()
      函数就是从这个队列里面依次往外拿出通知然后进行分类处理,而CreateIoCompletionPort()函数的参数3就是告知函数GetQueuedCompletionStatus()
      队列里面拿出的事件通知具体是哪一个SOCKET上的发生的。
所以这里的参数就是要传入具体发生事件通知的SOCKET(如果是把所有的SOCKET装进数组里面的话,这里也可以传具体SOCKET的下标)。
NumberOfConcurrentThreads:如果参数ExistingCompletionPort不是NULL,则忽略此参数。可填0。

返回值(Return value):

函数执行成功返回自己,也就是再返回参数2;如果执行不成功那肯定就不等于参数2了啊!

3.使用AcceptEx(),WSARecv(),WSASend()函数投递请求。(这三个异步函数就偷个懒这里不过多的介绍了,因为是直接拿的重叠IO里面的函数,哈哈哈)

4.使用CreateThread()函数创建线程,使用GetSystemInfo()获得操作系统相关信息,比如获取CPU核数。

(GetSystemInfo()函数上文已经大致介绍了一下,和网络也没有太大的关系这里就不详细介绍了,想了解的可以看一下MSDN)

创建线程函数CreateThread()的功能就是一次创建一根线程,如果要创建多根线程,可以用循环

HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
__drv_aliasesMem LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);

参数(Parameters):

lpThreadAttributes:线程句柄是否被继承,不继承就填NULL。如果不继承就是子线程与父线程共享一份线程句柄,相当于全局变量;
          如果继承的话子类复制一份父类的此时就会有两份,相当于局部变量自己用自己的;
          还有一个功能就是指定线程的权限,默认权限就填NULL。
          所以此参数填NULL就好。
dwStackSize:线程大小(栈区大小),填0,默认大小为1M。可以指定大小以字节为单位。
lpStartAddress:线程函数地址;
        线程函数函数头:DWORD WINAPI ThreadProc(LPVOID lpParameter); 这个函数的参数由函数CreateThread()的参数4传入
lpParameter:外部给线程传递数据,把传递进来的数据传递给参数3中的线程函数中;
dwCreationFlags:线程创建出来的一种执行状态;
         立即执行填0,也就是立即获得时间片分得的时间;
         挂起状态填CREATE_SUSPENDED(不占用时间周期)。调用ResumeThread()函数,激活挂起状态的线程。
         如果填STACK_SIZE_PARAM_IS_A_RESERVATION,这个宏是和参数2关联在一起的。如果想修改栈区大小,
         设置了这个宏,参数2就是修改的栈保留大小,即虚拟内存上栈得大小;如果没有设置修改的就是栈提交大小,即物理内存上的大小。
lpThreadId:线程ID,每根线程的ID都不一样。不用就填NULL。

返回值(Return value):

函数执行成功返回线程句柄,失败返回NULL。可以用GetLastError()获得错误码。
线程句柄是内核对象,用完要释放用CloseHandle()函数。

5.当系统异步处理完成后,会生成一个通知,这个通知就会放进通知队列里面,而完成端口就可以理解为通知队列的头。该队列由操作系统系统创建,维护。

6.通过GetQueuedCompletionStatus()函数从队列头一个一个往外拿,进行处理。

如果通知队列里没有通知,那么会使线程处于挂起状态,这样就不会占用CPU时间。 

BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytesTransferred,
PULONG_PTR lpCompletionKey,
LPOVERLAPPED *lpOverlapped,
DWORD dwMilliseconds
);

参数(Parameters):

CompletionPort:创建完成端口时返回的变量。
lpNumberOfBytesTransferred:收到或发送的字节数。如果是客户端SOKCET发生事件通知并且此参数返回的是0,那就说明是客户端退出。
lpCompletionKey:在上面写绑定重叠套接字与完成端口的时候已经介绍到了此参数,这里就不过多说了。它就是接收绑定完成端口的时候传进来的SOCKET。
lpOverlapped:返回一个发生事件通知的SOCKET上所绑定的那个重叠结构的地址。
dwMilliseconds:等待时间。可以是具体的等待时间以毫秒为单位;也可以一直等到有事件通知为止,一直等填INFINITE。

返回值(Return value):

函数执行成功返回TRUE,失败返回FALSE,可以用GetLastError()获取错误码。

完成端口代码逻辑:

1.打开网络库(WSAStartup())

2.校验版本(副版本:HIBYTE()、主版本:LOBYTE())

3.创建SOCKET(WSASocket())

4.绑定地址与端口号(bind())

5.创建完成端口(CreateIoCompletionPort())

6.将重叠套接字(客户端SOCKET+服务器SOCKET)与完成端口进行绑定(CreateIoCompletionPort())

7.开始监听(listen())

8.创建线程(CreteThread())

9.获取事件通知(GetQueuedCompletionPort())进行分类处理

10.释放

服务器开发基础-Tcp/Ip网络模型—完成端口(Completion Port)模型的更多相关文章

  1. TCP/IP 网络模型

    前言 互联网是怎么构成的,又是怎么运作的?什么是 TCP/IP 网络?为什么远隔万里的计算机可以互相通信?计算机网络作为 IT 行业的基石,是工程师永远绕不开的话题. 计算机网络的分层体系结构 计算机 ...

  2. 完成端口(Completion Port)详解(转)

    手把手叫你玩转网络编程系列之三    完成端口(Completion Port)详解                                                           ...

  3. (转载)完成端口(Completion Port, I/OCP)详解

    http://www.cnblogs.com/lancidie/archive/2011/12/19/2293773.html 手把手叫你玩转网络编程系列之三    完成端口(Completion P ...

  4. 转:完成端口(Completion Port)详解

    手把手叫你玩转网络编程系列之三    完成端口(Completion Port)详解                                                           ...

  5. 网络基础二 tcp/ip协议簇 端口 三次握手 四次挥手 11种状态集

    第1章 概念介绍 1.1 VLAN 1.1.1 什么是VLAN VLAN(Virtual LAN),翻译成中文是“虚拟局域网”.LAN可以是由少数几台家用计算机构成的网络,也可以是数以百计的计算机构成 ...

  6. 网络基础tcp/ip协议五

    传输层的作用: ip层提供点到点的链接. 传输层提供端到端的链接. 传输层的协议: TCP: 传输控制协议可靠的,面向链接的协议,传输效率低. UDP: 用户数据报协议,不可靠,无连接的服务,传输效率 ...

  7. TCP/IP 协议簇 端口 三次握手 四次挥手 11种状态集

    第1章 概念介绍 1.1 VLAN 1.1.1 什么是VLAN VLAN(Virtual LAN),翻译成中文是“虚拟局域网”.LAN可以是由少数几台家用计算机构成的网络,也可以是数以百计的计算机构成 ...

  8. 加深理解HTTP请求---网络基础TCP/IP

    为了了解HTTP,必须的了解TCP/IP协议族. 通常使用的网络实在TCP/IP协议族的基础上运作的.而HTTP就属于他的一个子集. 1.TCP/IP 协议族 计算机与网络设备要相互通信,双方就必须基 ...

  9. linux高性能服务器编程 (一) --Tcp/Ip协议族

    前言: 在学习swoole入门基础的过程中,遇到了很多知识瓶颈,比方说多进程.多线程.以及进程池和线程池等都有诸多的疑惑.之前也有学习相关知识,但只是单纯的知识面了解.而没有真正的学习他们的来龙去脉. ...

随机推荐

  1. 用 customRef 做一个防抖函数,支持 element 等UI库。

    这几天学习Vue的官网,看到 customRef 提供了一个例子,研究半天发现这是一个防抖函数,觉得挺好,于是把这个例子扩展了一下,可以用于表单子控件和查询子控件. 需求 v-model 基于 ele ...

  2. kubernetes1.17.2结合ceph13.2.8部署gitlab12.1.6

    [root@bs-k8s-ceph ~]# ceph -s cluster: id: 11880418-1a9a-4b55-a353-4b141e2199d8 health: HEALTH_OK se ...

  3. OxyPlot.SkiaSharp显示中文乱码的问题

    oxyplot 图表控件功能强大,使用很广泛.最近考虑到性能使用OxyPlot.SkiaSharp替代OxyPlot.WPF,曲线图表初步测试,性能提升近10倍左右.基于SkiaSharp图形引擎的一 ...

  4. Java单例模式实现,一次性学完整,面试加分项

    单例模式是设计模式中使用最为普遍的一种模式.属于对象创建模式,它可以确保系统中一个类只产生一个实例.这样的行为能带来两大好处: 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而 ...

  5. [DP]城市交通

    城市交通 Time Limit:1000MS--Memory Limit:65536K 题目描述 有n个城市,编号1~n,有些城市之间有路相连,有些则没有,有路则当然有一个距离.现在规定只能从编号小的 ...

  6. MindSpore函数拟合

    技术背景 在前面一篇博客中我们介绍过基于docker的mindspore编程环境配置,这里我们基于这个环境,使用mindspore来拟合一个线性的函数,演示一下mindspore的基本用法. 环境准备 ...

  7. H5 端 rem 适配方案与 viewport 适配

    H5 端 rem 适配方案与 viewport 适配 rem rem 是 CSS3 新增的一个相对单位(root em,根 em) 只根据当前页面 HTML 页面的 font-size 设置,如果根目 ...

  8. 剑指offer--孩子们的游戏(圆圈中最后剩下的数字)

    每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此.HF作为牛客的资深元老,自然也准备了一些小游戏.其中,有个游戏是这样的:首先,让小朋友们围成一个大圈.然后,他随机指定一个数m ...

  9. 【深度学习】PyTorch CUDA环境配置及安装

    Pytorch版本介绍 torch:1.6 CUDA:10.2 cuDNN:8.1.0 安装 NVIDIA 显卡驱动程序 一般 电脑出厂/装完系统 会自动安装显卡驱动 如果有 可直接进行下一步 下载链 ...

  10. NDEBUG与assert

    当宏NDEBUG定义在assert的头文件之前,会使assert.trace这类调试函数失效, 需要注意的是#define NDEBUG必须放在这些函数的头文件之前,放在它们的 头文件后面的话就相当于 ...