众所周知,高并发的大型服务器程序一直面临着架构复杂、线程众多难以管理、并发性能提升困难的问题。为此,各种平台都提供系统级的高级设施来协助开发者解决这个难题,例如Linux平台的epoll。对于我们熟悉的Windows平台,则有一个名为IOCP(完成端口)的内核对象,通过它,我们可以方便地创建高并发、高性能、可伸缩的网络服务器程序。

IOCP即I/0 Completion Parts,它是迄今为止Windows平台上最为复杂的一种I/O模型,假如一个程序需要管理为数众多的套接字,那么采用这种模型往往可以达到最佳的系统性能。当你的应用程序需要同时管理数百乃个至上千个套接字的时候,而且希望随着系统内安装的CPU的数量增多,应用程序的性能也可以线性地提升时,你应该考虑使用IOCP模型。IOCP是Windows平台唯一适用于高负载服务器的一个技术,它利用一些线程帮助平衡”I/O请求”所引起的负载,这样的构架特别适用于产生所谓的”Scalable”服务器,这是一种能够籍着增加RAM、磁盘空间、CPU个数而提升应用程序效能的一种系统。

在决定使用IOCP之前,我们先来回顾一下传统的网络服务程序的工作模式。传统的工作模式大致是一个listen线程监听到来自网络的服务请求时,创建一个线程来对请求作出响应。这样,随着并发数的提高,系统中就会活跃着大量的线程,最终,疲于奔命的Windows内核不得不耗费大量的时间用于线程的上下文切换,而对于有价值的应用计算则力不从心。尽管可以引入线程池来改善线程的创建和管理,使系统的运行有条不紊!但创建这样的应用所需要的技巧和繁琐的逻辑足以使人敬而远之,你需要自己小心翼翼地处理一切!与系统提供的设施相比,无论从兼容性上还是性能上,传统的工作模式都不是最好的选择,你应该放弃”发明轮子”的念头。

本质上,利用IOCP模型,我们也需要线程池的思想。我们需要事先创建一定数量的工作线程,所有工作线程在同一个IOCP对象上阻塞,并等待一次IO操作完成,当一次IO操作完成时,被绑定的这个IOCP对象将得到状态的更新,IOCP对象的状态更新通知会激活工作者线程的执行。

接下来,我们看看一个最小巧的利用IOCP模型的服务器应用是如何炮制并工作的。

一、首先,你需要创建IOCP内核对象。

一个系统级的API——CreateIoCompletionPort可用于IOCP对象的创建。

HANDLE CreateIoCompletionPort(

IN HANDLE FileHandle,

IN HANDLE ExistingCompletionPort,

IN ULONG_PTR CompletionKey,

IN DWORD NumberOfConcurrentThreads

);

这个函数不仅用于IOCP对象的创建,还用于将一个句柄同IOCP对象的绑定。这个函数有4个参数,但在创建IOCP对象时,我们对前3个参数都不感兴趣。第4个参数NumberOfConcurrentThreads用于告诉系统在这个IOCP对象上的线程并发数,即允许多少个线程被同时执行。为了避免频繁的线程场景切换,人们一般希望在每一个处理器上运行一个线程,这样,我们可以把这个参数设置为0,这表明为这个IOCP对象同时服务的线程数量随系统中的CPU数量而定,有多少个CPU就允许多少个线程被同时运行。

HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 0);

这样,我们就创建了一个IOCP对象,它的句柄保存在hIOCP变量中。接下来,我们就可以把IO操作的句柄与这个IOCP对象建立联系。

二、IO操作与IOCP对象的绑定。

事实上,IOCP除了接受网络套接字的绑定还可以接受WIN32的其它对象,例如文件句柄。这里,我们只关心socket句柄的绑定问题。

我们需要建立一个socket监听,然后把accept返回的socket绑定到IOCP对象上,让IOCP对象与IO操作的状态建立起联系。有关socket的建立问题在此用代码代替讨论、略过不表:

SOCKET socketServer = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

if  (socketServer == INVALID_SOCKET)

{

return FALSE;

}

sockaddr_in addrServer;

ZeroMemory(&addrServer, sizeof(sockaddr_in));

addrServer.sin_family = AF_INET;

addrServer.sin_addr.s_addr = INADDR_ANY;

addrServer.sin_port = htons(usPort);

if (bind(socketServer, (sockaddr*)&addrServer, sizeof(sockaddr_in)) == SOCKET_ERROR)

{

int nError = WSAGetLastError();

closesocket(m_SocketServer);

return FALSE;

}

if (listen(socketServer, MAX_CONNECT_QUEUE) == SOCKET_ERROR)

{

closesocket(m_SocketServer);

return FALSE;

}

上面的步骤创建一个用于监听的socket并开始网络监听,然后我们使用WSAAccept不断接受来自网络的接入请求,把该函数返回的socket句柄与IOCP对象绑定,随后,我们将在该socket上执行异步的网络操作,操作完成时IOCP对象将使挂起的工作线程立即进入工作状态。

While (TRUE)

{

socketAccept = WSAAccept(socketServer, NULL, NULL, NULL, 0);

LPPerHandleData pPerHandleData = (LPPerHandleData)GlobalAlloc(GPTR, sizeof(PerHandleData));

pPerHandleData->sock = socketAccept;

if (CreateIoCompletionPort((HANDLE)socketAccept, hIOCP, (ULONG_PTR)pPerHandleData , 0) == NULL)

{

ASSERT(FALSE);

}

// 接下来,就可以就可以使用WSARecv/WSASend来异步地接收/发送消息了。

WSARecv(…);

WSASend(…);

}

注意CreateIoCompletionPort的第三个参数CompletionKey,它指向一个被称作单句柄对象的自定义数据结构体地址,当IO操作完成时,工作线程籍由IOCP对象获得这个状态的改变,IOCP将原原本本地把绑定时指定的这个结构体指针传给工作线程。我们可以把socket句柄保存在单句柄结构体中,这样,借助于这个自定义的结构体,工作线程可以明确地知道是与哪个句柄相关的操作发生了状态的改变。

可以定义类似于下面的结构体用于单句柄数据:

// 单句柄数据结构

typedef struct tagPerHandleData{

bool bUsed; // 是否正在使用

SOCKET sock;

sockaddr_in addrClient;

LPPerIoOperData lpPerIoOpDataRecv;

LPPerIoOperData lpPerIoOpDataSend;

ZJRecvBufItem stRecvBufItem;

}PerHandleData, *LPPerHandleData;

一般情况,我们并不象上述代码那样直接GlobalAlloc出来一个单句柄结构体,使用一个所谓的单句柄池来管理是更好的选择。每个新接入的socket需要单句柄数据时,我们从单句柄池中获取,socket断开后,我们再把与之相关联的单句柄归还给单句柄池。

三、创建工作线程。

在完成了IOCP的创建之后,我们最重要的工作还没有做,那就是创建工作线程。工作线程创建多少个为宜,需要根据应用的性质来确定,一般来说,推荐的数量是CPU数量的两倍略多一点。因此,我们首先获得CPU的数量,然后乘2加2。

int nThreadsNum = nNumberOfProcessors * 2 + 2;

HANDLE hThread = NULL;

for (int i=0; i<nThreadsNum; ++i)

{

hThread = CreateThread(NULL, 0, WorkerThread, hIOCP, 0, NULL);

}

DWORD WINAPI WorkerThread(LPVOID lpParam)

{

HANDLE hIOCP = (HANDLE)lpParam;

DWORD dwBytesTransfered;

LPPerHandleData lpPerHandleData;

LPPerIoOperData lpPerIoOperData;

while (TRUE)

{

// 等待绑定到hIOCP指定的完成端口上的socket完成IO操作

GetQueuedCompletionStatus(hIOCP, &dwBytesTransfered, (LPDWORD)&lpPerHandleData, (LPOVERLAPPED*)&lpPerIoOperData, INFINITE);

if (dwBytesTransfered == 0 && lpPerHandleData == 0 && lpPerIoOperData == 0)

{

// WorkerThread线程收到退出消息!

break;

}

// 处理网络事件

}

}

GetQueuedCompletionStatus函数用于获取指定IOCP的”排队完成状态”,它让一个以上的线程在此函数上挂起,等待指定的IOCP的状态通知。当IOCP状态更新后,在此IOCP上挂起的线程中的某一个线程将被立即激活。该函数原型是:

BOOL GetQueuedCompletionStatus(

IN HANDLE CompletionPort,

OUT LPDWORD lpNumberOfBytesTransferred,

OUT PULONG_PTR lpCompletionKey,

OUT LPOVERLAPPED *lpOverlapped,

IN DWORD dwMilliseconds

);

参数lpNumberOfBytesTransferred是指一次IO操作完成的字节数,lpCompletionKey是我们在调用CreateIoCompletionPort绑定socket句柄与IOCP时指定的单句柄数据,前面已经提到,通过这个参数我们可以知道是在哪个socket上发生的。lpOverlapped用于获取WIN32重叠IO操作的一个重叠结果,本质上,IOCP利用了WIN32重叠IO的机制。我们使用WSASend和WSARecv函数异步地投递发送和接收请求时,需要一个OVERLAPPED的结构体,WSASend和WSARecv将直接返回,操作完成后这个OVERLAPPED结构体指针由GetQueuedCompletionStatus函数取回。

由于IO操作的数据在IO操作完成后是必须要让工作线程知道的,而WSASend、WSARecv及GetQueuedCompletionStatus函数均接受一个OVERLAPPED指针,因此,我们可以定义一个结构体来保存OVERLAPPED结构和IO操作的数据,让OVERLAPPED成为第一个成员,并在调用那三个函数时把参数强转成OVERLAPPED指针。这样的结构体叫作”单IO操作数据”,它的定义可以是这样的:

// 单IO操作数据结构

typedef struct tagPerIoOperData{

WSAOVERLAPPED stOverlapped

bool bUsed;

WSABUF stDataBuf;

char szBuf[TCP_BUF_SIZE];

OperationType emOperType;

LPZJSendBufItem lpSendBuf;

tagPerHandleData* pPHD;

}PerIoOperData, *LPPerIoOperData;

至此,一个以IOCP模型建立服务器应用的基本步骤就完成了。但实际的应用还必须考虑更多的细节,迎接更多的挑战。例如,如何投递近饱和的accept请求以更快的速度响应更多网络接入请求,如何通知上层应用接收到了来自网络的数据包,以及如何向网络发送数据包……只要理解了基本的IOCP模型,其它的尽管有些繁琐,但并不复杂,基于这种模型去深入地考虑问题,一切的问题都可以迎刃而解!

Windows平台利用完成端口模型创建高性能网络服务器相关推荐

  1. 利用群辉Docker创建MC服务器

    利用群辉Docker创建MC服务器 前言 一.安装 二.使用步骤 1.镜像选择 2.MC版本 3.Model安装 总结 前言 前段时间用旧笔记本创建了一台黑群晖,创建完就扔到角落吃灰了.今天忽然想玩M ...

  2. Android手机利用KSWEB+端口转发搭建PHP服务器

    Android手机利用KSWEB+端口转发搭建PHP服务器 转载来自:https://golthr.gitee.io/articles/202002281619/ KSWEB是一款基于Android的 ...

  3. windows平台下使用Github(2 创建代码仓库,安装Github客户端.)

    本文会分章节的来介绍如何在windows平台下使用GitHub 一.注册Github账号 查看 二.创建代码仓库,安装Github客户端.查看 三.Github上传和下载(1 客户端方式)查看 四.G ...

  4. php iocp,完成端口(iocp)实现高性能网络服务器

    在Windows系统里使用完成端口 在Windows系统里,使用完成端口高性能的方法之一,比如把完成端口使用到线程池和网络服务器里.现在就通过线程池的方法来介绍怎么样使用完成端口,高性能的服务器以后再 ...

  5. windows下利用.bat批处理文件来创建以当前时间命名的文件夹

    分享一下利用批处理文件创建以当前时间命名文件夹的命令 单位测试环境发新包的时候总会需要把旧包备份一下,每次手动创建文件夹太麻烦了,干脆写个批处理文件来创建 命令如下: @echo off set NO ...

  6. windows平台下建立HTTP站点及FTP服务器站点

    基于win平台下搭建一个ftp,或http服务器站点,可以方便的实现文件共享. 比如FTP站点: 基于窗口的访问:ftp://192.168.1.202/ 基于web的访问: HTTP站点访问: 下面 ...

  7. 《ASCE1885的源码分析》の基于完成端口模型的TCP服务器框架

    使用IOCP的TCP服务器使用过程大体如下: 1)  使用CreateIoCompletionPort函数创建完成端口,并以该I/O完成端口为参数创建多个服务线程: 2) 创建监听套接字: 3) 接收 ...

  8. 在windows server上用Mosquitto软件创建MQTT服务器

    今天下午捣鼓了半天,在云服务器上面创建了个MQTT服务器,然后用MQTTX软件进行了测试.过程记录如下: 1.下载mosquitto软件,链接如下图: 2.下载完成后安装,一直点下一步下一步就好了. ...

  9. c++ 编译添加dll_(windows平台下)深入详解C++创建动态链接库DLL以及如何使用它(一)...

    前言:C以及C++的动态链接库和静态链接库,说起来很简单,但是实际上在创建的过程中有很多的坑,本人也是一路踩了很多坑,查了很多资料,下决定写一篇完整的文章来详细解释使用VS创建C++动态链接库的完整流 ...

最新文章

  1. ios开发之系统信息
  2. (原創) 將map輸出到cout,是否有更方便的方法? (C/C++) (STL)
  3. Spring Cloud Security:Oauth2使用入门
  4. 使用Google Guava Cache进行本地缓存
  5. Python---冒泡排序、选择排序
  6. php面向对象异常处理,PHP面向对象编程——自定义PHP异常处理类
  7. C# 获取视频文件播放时长
  8. 计算机应用技术专业盲打键盘,一种双手八指轨道定键位盲打器与盲打键盘
  9. java代码生成apk_android – 如何通过java代码以编程方式生成apk文件
  10. 限时删!字节总监总结一套目标检测、卷积神经网络和OpenCV学习资料(教程/PPT/代码)...
  11. micropython常用模块-Python时间模块之datetime
  12. LINNAEUS:生物医学文献的物种名称识别系统
  13. 底部任务栏桌面计算机怎么删除,桌面下方的任务栏总是隐藏怎么办
  14. 2008服务器远程桌面连接设置密码,WinServer 2008 远程桌面连接设置
  15. MSCOCO检测数据集类别中文名
  16. 大数据学习之 ElasticSearch 练习
  17. win10安装虚拟机Linux Centos7系统网络配置
  18. amazon 云平台入门
  19. 第11章组件装饰和视觉效果-DecoratedBox装饰盒子-背景图效果
  20. org.apache.solr.client.solrj.impl.CloudSolrServer$RouteException: Exception writing document id xxxx

热门文章

  1. (转)Inno Setup入门(八)——有选择性的安装文件
  2. NOJ 1003.快速排序
  3. WOT2015 互联网运维与开发者大会上的演讲
  4. 彩信库 mmslib 设计备忘录
  5. 关于在计算机同学之间建立社区讨论氛围的疑惑
  6. “笨办法”学Python3,Zed A. Shaw,习题20
  7. 如何实现OpenStack STT隧道(by quqi99)
  8. nvm use命令出现乱码 exit status 145
  9. java.lang.IllegalStateException: Duplicate key 【java8 toMap(key重复如何解决)】
  10. 激光打印机的粉盒装粉