转载请注明来源:http://blog.csdn.net/caoshiying?viewmode=contents

一、回顾重叠IO模型

用完成例程来实现重叠I/O比用事件通知简单得多。在这个模型中,主线程只用不停的接受连接即可;辅助线程判断有没有新的客户端连接被建立,如果有,就为那个客户端套接字激活一个异步的WSARecv操作,然后调用SleepEx使线程处于一种可警告的等待状态,以使得I/O完成后CompletionROUTINE可以被内核调用。如果辅助线程不调用SleepEx,则内核在完成一次I/O操作后,无法调用完成例程(因为完成例程的运行应该和当初激活WSARecv异步操作的代码在同一个线程之内)。

完成例程内的实现代码比较简单,它取出接收到的数据,然后将数据原封不动的发送给客户端,最后重新激活另一个WSARecv异步操作。注意,在这里用到了“尾随数据”。我们在调用WSARecv的时候,参数lpOverlapped实际上指向一个比它大得多的结构PER_IO_OPERATION_DATA,这个结构除了WSAOVERLAPPED以外,还被我们附加了缓冲区的结构信息,另外还包括客户端套接字等重要的信息。这样,在完成例程中通过参数lpOverlapped拿到的不仅仅是WSAOVERLAPPED结构,还有后边尾随的包含客户端套接字和接收数据缓冲区等重要信息。这样的C语言技巧在我介绍完成端口的时候还会使用到。

二、完成端口模型

“完成端口”模型是迄今为止最为复杂的一种I/O模型。然而,假若一个应用程序同时需要管理为数众多的套接字,那么采用这种模型,往往可以达到最佳的系统性能!但不幸的是,该模型只适用于Windows NT和Windows 2000操作系统。因其设计的复杂性,只有在你的应用程序需要同时管理数百乃至上千个套接字的时候,而且希望随着系统内安装的CPU数量的增多,应用程序的性能也可以线性提升,才应考虑采用“完成端口”模型。要记住的一个基本准则是,假如要为Windows NT或Windows 2000开发高性能的服务器应用,同时希望为大量套接字I/O请求提供服务(Web服务器便是这方面的典型例子),那么I/O完成端口模型便是最佳选择!

完成端口模型是我最喜爱的一种模型。虽然其实现比较复杂(其实我觉得它的实现比用事件通知实现的重叠I/O简单多了),但其效率是惊人的。我在T公司的时候曾经帮同事写过一个邮件服务器的性能测试程序,用的就是完成端口模型。结果表明,完成端口模型在多连接(成千上万)的情况下,仅仅依靠一两个辅助线程,就可以达到非常高的吞吐量。

三、关键函数

1、CreateIoCompletionPort

创建一个输入/输出(I / O)完成端口,并将其与一个指定的文件句柄关联,或者创建一个尚未与文件句柄关联的I / O完成端口,允许在稍后的时间关联。将已打开的文件句柄的实例与一个I / O完成端口关联,允许一个进程接收包含该文件句柄的异步I / O操作完成的通知。注意:这里所使用的术语文件句柄是指代表一个重叠的I / O端点的系统抽象,而不仅仅是磁盘上的一个文件。任何系统对象支持重叠I / o-such网络端点,TCP套接字,命名管道、邮件槽可以作为文件句柄。

函数原型:

HANDLE WINAPI CreateIoCompletionPort(_In_     HANDLE    FileHandle,_In_opt_ HANDLE    ExistingCompletionPort,_In_     ULONG_PTR CompletionKey,_In_     DWORD     NumberOfConcurrentThreads
);

函数参数:

FileHandle:一个打开的文件句柄或者INVALID_HANDLE_VALUE。这个文件句柄必须是支持重叠IO的object。如果提供了句柄, 它必须是已经给重叠I/O模型完成端口打开的句柄。例如,如果您使用CreateFile函数获取的句柄,那么您在调用这个函数时必须在参数中指定FILE_FLAG_OVERLAPPED旗标。如果指定 INVALID_HANDLE_VALUE,那么函数将创建一个没有关联文件句柄的IO完成端口模型,此外ExistingCompletionPort参数必须设为NULL,CompletionKey参数将被忽略。

ExistingCompletionPort:是已经存在的完成端口。如果为NULL,则为新建一个IOCP。

CompletionKey:用户定义的句柄包含的I/O完成包信息。当FileHandle被设为INVALID_HANDLE_VALUE时此参数被忽略。

NumberOfConcurrentThreads:操作系统可以允许同时处理I / O完成端口的I / O完成数据包的线程的最大数目。如果existingcompletionport参数不为空,则忽略此参数。如果这个参数为零,系统允许多个并发运行的线程,因为系统中有处理器。

返回值:

如果函数成功,返回值是一个I / O完成端口的句柄:如果ExistingCompletionPort参数为空,返回值是一个新的处理。如果ExistingCompletionPort参数是一个有效的I/O完成端口句柄,返回值是相同的处理。如果文件句柄参数是一个有效的处理,文件处理是现在与返回的I/O完成端口。如果函数失败,返回值为空。为了获得更多的错误信息,调用GetLastError函数。

2、GetQueuedCompletionStatus

失望的是微软官方MSDN没有提供关于这个API的说明。以下参照一篇英文文档进行翻译。文档说这个函数试图将一个I/O完成包从指定的I/O完成端口。如果没有完成数据包队列,则函数等待一个挂起的I / O操作与完成端口相关联的完成。

函数原型:

BOOL WINAPI GetQueuedCompletionStatus(_In_  HANDLE       CompletionPort,_Out_ LPDWORD      lpNumberOfBytes,_Out_ PULONG_PTR   lpCompletionKey,_Out_ LPOVERLAPPED *lpOverlapped,_In_  DWORD        dwMilliseconds
);

函数参数:

CompletionPort:完成端口的句柄。创建一个完成端口,使用CreateIoCompletionPort函数。

lpNumberOfBytes:指向已完成的I / O操作期间传输的字节数的变量的指针。

lpCompletionKey:指向与文件句柄关联的完成键的变量的指针,该键的I / O操作已完成。一个完成的关键是每一个文件的关键,是指定一个叫CreateIoCompletionPort。

lpOverlapped:一个指向一个变量的指针,该指针指向在已完成的I / O操作开始时指定的重叠结构的地址的变量。即使您已经通过了一个与完成端口相关联的文件句柄和一个有效的重叠结构,应用程序也可以防止完成端口通知。这是通过指定的重叠结构的hevent成员有效的事件处理完成,并设置其低阶位。一个有效的事件句柄,其低阶位设置将保持I / O完成从被队列到完成端口。

dwMilliseconds:调用方愿意等待完成数据包出现在完成端口的毫秒数。如果一个完成包没有出现在指定的时间内,功能倍出,返回false,并设置*lpOverlapped为null。如果该参数是无限的,函数将没有时间了。如果该参数为零,没有I/O操作中出列,函数将取消等待时间,立即操作。

返回值:

返回非零(真),如果成功或零(假),否则。为了获得更多的错误信息,调用GetLastError。

此功能将一个线程与指定的完成端口关联。一个线程可以与至多一个完成端口相关联的。如果因为完成端口句柄与它是封闭而调用调用GetQueuedCompletionStatus突出失败,函数返回false,*lpOverlapped会是空的,GetLastError将返回error_abandoned_wait_0。

Windows Server 2003和Windows XP:关闭完成端口句柄,调用优秀不会导致之前的行为。该函数将继续等待直到一项是从港口或直到发生超时删除,如果指定以外的无限价值。

如果GetQueuedCompletionStatus函数调用成功,它出列完成包一个成功的I/O操作完成端口和存储信息的变量所指向的下列参数:lpNumberOfBytes,lpcompletionkey,和lpOverlapped。在失败(返回值是错误的),这些相同的参数可以包含特定的值组合如下:
如果*lpOverlapped为空,功能没有出列完成包从完成端口。在这种情况下,函数不存储信息在lpNumberOfBytes and lpCompletionKey所指向的参数中,其值是不确定的。
如果*lpOverlapped不空和功能按一个失败的I/O操作的完成端口完成包的功能,存储信息有关失败操作的变量所指向的lpcompletionkey lpOverlapped lpNumberOfBytes。为了获得更多的错误信息,调用GetLastError。

3、PostQueuedCompletionStatus

将一个I / O完成数据包发送到一个I / O完成端口。I/O完成包将满足一个优秀的调用GetQueuedCompletionStatus函数。该函数返回三值传递的第二,第三,和第四个参数postqueuedcompletionstatus呼叫。该系统不使用或验证这些值。特别是,lpOverlapped参数不需要点的重叠结构。

函数原型:

BOOL WINAPI PostQueuedCompletionStatus(_In_     HANDLE       CompletionPort,_In_     DWORD        dwNumberOfBytesTransferred,_In_     ULONG_PTR    dwCompletionKey,_In_opt_ LPOVERLAPPED lpOverlapped
);

参数:

CompletionPort:一个I / O完成数据包的I / O完成端口的句柄。

dwNumberOfBytesTransferred:要通过lpnumberofbytestransferred参数GetQueuedCompletionStatus函数返回的值。0xFFFFFFFF表示处理所有尾随数据。只有准备关闭端口的时候才这样做。

dwCompletionKey:可以通过GetQueuedCompletionStatus函数返回的值lpcompletionkey参数。

lpOverlapped:要通过lpOverlapped参数GetQueuedCompletionStatus函数返回的值。

返回值:

如果函数成功,返回值是非零的。如果函数失败,返回值为零。为了获得更多的错误信息,调用GetLastError。

四、完整的示例程序

接着上面几篇Socket文章写,关于公共代码与反射式客户端请参见:《 Socket编程模型之简单选择模型》。下面是新建的overlapped_server工程,新建了一个overlapped_server_manager类型,继承自iserver_manager接口,头文件完整代码如下:

#pragma once#define SOCKET_MESSAGE_SIZE 1024#include <WinSock2.h>
#include <common_callback.h>typedef enum
{RECV_POSTED
}OPERATION_TYPE;typedef struct
{WSAOVERLAPPED overlap;WSABUF buffer;char message[SOCKET_MESSAGE_SIZE];DWORD received_count;DWORD flags;OPERATION_TYPE operation_type;
}PEERIO_OPERATION_DATA, *LPPEERIO_OPERATION_DATA;class completeio_server_manager:public iserver_manager
{
private:int iport;int iaddr_size;common_callback callback;BOOL brunning;SOCKET server;WSADATA wsaData;HANDLE hcomplete_port;SYSTEM_INFO system_info;LPPEERIO_OPERATION_DATA peer_data;bool bdisposed;protected:bool accept_by_crt();bool accept_by_winapi();public:void receive();void shutdown();void start_receive();void start_accept();public:completeio_server_manager();virtual ~completeio_server_manager();
};

实现文件完整代码如下:

#include "completeio_server_manager.h"
#include <stdio.h>
#include <tchar.h>completeio_server_manager::completeio_server_manager()
{iport = 5150;iaddr_size = sizeof(SOCKADDR_IN);brunning = FALSE;GetSystemInfo(&system_info);callback.set_manager(this);callback.set_receive_thread_coount(system_info.dwNumberOfProcessors);hcomplete_port = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);bdisposed = false;
}completeio_server_manager::~completeio_server_manager()
{if (bdisposed)shutdown();
}bool completeio_server_manager::accept_by_crt()
{return true;
}bool completeio_server_manager::accept_by_winapi()
{SOCKADDR_IN server_addr;SOCKADDR_IN client_addr;SOCKET client;LPPEERIO_OPERATION_DATA peer_data;int iresult = -1;WSAStartup(MAKEWORD(2, 2), &wsaData);server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);server_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);server_addr.sin_family = AF_INET;server_addr.sin_port = htons(iport);do{iresult = bind(server, (struct sockaddr*)&server_addr, iaddr_size);if (iresult == SOCKET_ERROR){iport++;server_addr.sin_port = htons(iport);}} while (iresult == -1);listen(server, 3);printf("基于完成端口模型的Socket服务器启动成功。监听端口是:%d\n", iport);while (brunning){printf("开始监听请求。\n");client = accept(server, (struct sockaddr*)&client_addr, &iaddr_size);if (client == SOCKET_ERROR)continue;printf("新客户端连接:%s:%d\n", inet_ntoa(client_addr.sin_addr), htons(client_addr.sin_port));CreateIoCompletionPort((HANDLE)client, hcomplete_port, (DWORD)client, 0);peer_data = (LPPEERIO_OPERATION_DATA)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PEERIO_OPERATION_DATA));peer_data->buffer.len = SOCKET_MESSAGE_SIZE;peer_data->buffer.buf = peer_data->message;peer_data->operation_type = RECV_POSTED;printf("开始接收客户端传送数据。\n");WSARecv(client, &peer_data->buffer, 1, &peer_data->received_count, &peer_data->flags, &peer_data->overlap, NULL);printf("收到客户端数据。\n");}return true;
}void completeio_server_manager::receive()
{DWORD dwtransfered = 0;SOCKET client;LPPEERIO_OPERATION_DATA peer = nullptr;while (brunning){printf("线程:%d,查询端口状态信息。\n",GetCurrentThreadId());GetQueuedCompletionStatus(hcomplete_port, &dwtransfered, (PULONG_PTR)&client, (LPOVERLAPPED*)&peer, INFINITE);printf("获得端口信息。\n");if (dwtransfered == 0xFFFFFFFF)return;if (peer->operation_type == RECV_POSTED){if (dwtransfered == 0){closesocket(client);printf("有客户端退出了。\n");HeapFree(GetProcessHeap(), 0, peer);}else{peer->message[dwtransfered] = 0;send(client, peer->message, dwtransfered, 0);memset(peer, 0, sizeof(PEERIO_OPERATION_DATA));peer->buffer.len = SOCKET_MESSAGE_SIZE;peer->buffer.buf = peer->message;peer->operation_type = RECV_POSTED;WSARecv(client, &peer->buffer, 1, &peer->received_count, &peer->flags, &peer->overlap, nullptr);}}}
}void completeio_server_manager::shutdown()
{PostQueuedCompletionStatus(hcomplete_port, 0xFFFFFFFF, 0, NULL);//端口尾随数据。brunning = FALSE;callback.shutdown();//清扫CloseHandle(hcomplete_port);closesocket(server);WSACleanup();bdisposed = true;
}void completeio_server_manager::start_accept()
{brunning = TRUE;bdisposed = false;callback.start_accept_by_winapi();
}void completeio_server_manager::start_receive()
{brunning = TRUE;bdisposed = false;callback.start_receive();
}int main()
{completeio_server_manager csm;csm.start_accept();csm.start_receive();printf("服务器启动成功。按任意键关闭服务器并退出程序。\n");getchar();csm.shutdown();return 0;
}

五、效果

六、心得体会

成功创建一个完成端口后,便可开始将套接字句柄与对象关联到一起。但在关联套接字之前,首先必须创建一个或多个“工作者线程”,以便在I/O请求投递给完成端口对象后,为完成端口提供服务。在这个时候,大家或许会觉得奇怪,到底应创建多少个线程,以便为完成端口提供服务呢?这实际正是完成端口模型显得颇为“复杂”的一个方面,因为服务I/O请求所需的数量取决于应用程序的总体设计情况。

在此要记住的一个重点在于,在我们调用CreateIoCompletionPort时指定的并发线程数量,与打算创建的工作者线程数量相比,它们代表的并非同一件事情。早些时候,我们曾建议大家用CreateIoCompletionPort函数为每个处理器都指定一个线程(处理器的数量有多少,便指定多少线程)以避免由于频繁的线程“场景”交换活动,从而影响系统的整体性能。CreateIoCompletionPort函数的NumberOfConcurrentThreads参数明确指示系统:在一个完成端口上,一次只允许n个工作者线程运行。假如在完成端口上创建的工作者线程数量超出n个,那么在同一时刻,最多只允许n个线程运行。

但实际上,在一段较短的时间内,系统有可能超过这个值,但很快便会把它减少至事先在CreateIoCompletionPort函数中设定的值。那么,为何实际创建的工作者线程数量有时要比CreateIoCompletionPort函数设定的多一些呢?这样做有必要吗?如先前所述,这主要取决于应用程序的总体设计情况。假定我们的某个工作者线程调用了一个函数,比如Sleep或WaitForSingleObject,但却进入了暂停(锁定或挂起)状态,那么允许另一个线程代替它的位置。换言之,我们希望随时都能执行尽可能多的线程;当然,最大的线程数量是事先在CreateIoCompletionPort调用里设定好的。

这样一来,假如事先预计到自己的线程有可能暂时处于停顿状态,那么最好能够创建比CreateIoCompletionPort的NumberOfConcurrentThreads参数的值多的线程,以便到时候充分发挥系统的潜力。一旦在完成端口上拥有足够多的工作者线程来为I/O请求提供服务,便可着手将套接字句柄同完成端口关联到一起。这要求我们在一个现有的完成端口上,调用CreateIoCompletionPort函数,同时为前三个参数——FileHandle,ExistingCompletionPort和CompletionKey——提供套接字的信息。其中, FileHandle参数指定一个要同完成端口关联在一起的套接字句柄。ExistingCompletionPort参数指定的是一个现有的完成端口。

CompletionKey(完成键)参数则指定要与某个特定套接字句柄关联在一起的“单句柄数据”;在这个参数中,应用程序可保存与一个套接字对应的任意类型的信息。之所以把它叫作“单句柄数据”,是由于它只对应着与那个套接字句柄关联在一起的数据。可将其作为指向一个数据结构的指针,来保存套接字句柄;在那个结构中,同时包含了套接字的句柄,以及与那个套接字有关的其他信息。

Socket编程模型之完成端口模型相关推荐

  1. SOCKET编程登峰造极之完成端口

    一.什么是完成端口? 完成端口---是一种WINDOWS内核对象.完成端口用于异步方式的重叠I/0情况下,当然重叠I/O不一定非使用完成端口不可,还有设备内核对象.事件对象.告警I/0等.但是完成端口 ...

  2. 同一端口是否可以绑定到多个IP上(关于Socket编程中地址与端口绑定那些事)

    一块网卡有多个IP,是否能够将同一个端口绑定在多个IP地址上? 废话不多说,上源码: [源码1] #include <QAbstractSocket> #include <QHost ...

  3. socket编程(二) select 模型

    select 模型是winsock中最常见的I/O模型,主要是它能够防止程序在套接字处于阻塞模式的时候经过一次I/O操作后被阻塞,同时也能够防止套接字处于非阻塞模式产生的WSAEWOULDBLOCK错 ...

  4. socket编程的select模型

    在掌握了socket相关的一些函数后,套接字编程还是比较简单的,日常工作中碰到很多的问题就是客户端/服务器模型中,如何让服务端在同一时间高效的处理多个客户端的连接,我们的处理办法可能会是在服务端不停的 ...

  5. socket编程五种模型

    客户端:创建套接字,连接服务器,然后不停的发送和接收数据. 比较容易想到的一种服务器模型就是采用一个主线程,负责监听客户端的连接请求,当接收到某个客户端的连接请求后,创建一个专门用于和该客户端通信的套 ...

  6. Socket编程5种模型——以代码说明一切,hhhhh

      我会以一个回应反射式服务器(与<Windows网络编程>第八章一样)来介绍这五种I/O模型.我们假设客户端的代码如下(为代码直观,省去所有错误检查,以下同): #include < ...

  7. windows socket编程五种模型

    客户端:创建套接字,连接服务器,然后不停的发送和接收数据. 比较容易想到的一种服务器模型就是采用一个主线程,负责监听客户端的连接请求,当接收到某个客户端的连接请求后,创建一个专门用于和该客户端通信的套 ...

  8. Socket基础八:网络IO模型的应用

    Socket基础八:网络IO模型的应用 作者:刘磊 2020.4.27 参考书目:<Windows网络编程>刘琰等著 一.实验目的 1)掌握WindowsI/O操作的基本原理. 2)掌握阻 ...

  9. 基于MFC的socket编程(异步非阻塞通信)

    对于许多初学者来说,网络通信程序的开发,普遍的一个现象就是觉得难以入手.许多概念,诸如:同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)等,初学者往往迷惑不清,只知其 ...

  10. Boost.Asio,libevent和ACE之间关于Socket编程的比较(★firecat推荐★)

    文章来源:http://blog.163.com/miky_sun/blog/static/3369405201041753652505/ ACE官网 http://download.dre.vand ...

最新文章

  1. 【Ubuntu】VirtualBox显卡驱动VBoxVGA、VBoxSVGA、VMSVGA +3D对播放视频的影响
  2. linux open()调用的注意事项
  3. Go -- 多个go文件包名都是main
  4. 菜鸟Linux攻略--目录文件权限的查看和修改(详细)
  5. 机器学习之朴素贝叶斯法
  6. 伪元素before、after示例
  7. Charles抓包工具_基本功能
  8. C++基础教程之如何定义数字
  9. pb 如何导出csv_《通讯录管理系统》之 5——数据导出
  10. 也用 Log4Net 之走进Log4Net (四)
  11. 免费在线Android线框? UX Wireframing Online Free Tool
  12. 企业微信 PC端多开
  13. DEDECMS网站地图制作XML格式
  14. 什么是MTTF、MTBF、MTRF?
  15. 【误判心理学】查理芒格的25种误判心理倾向
  16. 北京化工大学计算机复试面试题,北京化工大学综合素质测试面试题历年总结
  17. 二月AI战疫丨百度大脑加推多项AI软硬能力,赋能数千家企业超千万用户
  18. 解决PS等软件出现应用程序无法正常启动(0xc000007b)
  19. 计算机管理主分区改成逻辑分区,win10系统电脑的四个分区都是主分区,主分区如何改为逻辑分区?...
  20. 怎样学好编程(个人编程感受)!

热门文章

  1. 计算机主板图解内存插槽,图解电脑主板上的常见部件 -电脑资料
  2. 编译实验 . 递归下降分析器
  3. 【数学建模】CUMCM-2017A CT系统参数标定及成像 思路及部分代码
  4. 《无线电》杂志1955年到2000年高清扫描版,果断下载一份保存!
  5. php数据库单循环显示,单循环赛制的PHP实现
  6. jmeter 计数器的使用
  7. 玩玩Linux云主机-安装MySQL ,The server quit without updating PID file,Linux chown 权限管理
  8. 技嘉 7pesh3 安装Linux,技嘉h370主板装win7系统及BIOS设置教程(完美支持usb3.1)
  9. CSS 代码语法 代码注释
  10. 塞尔之光的树心旋转机关_塞尔之光