Windows下使用winsock2与完成端口(IOCP)编写高伸缩性的网络服务器

来源:互联网 发布:二级顶级域名有哪些 编辑:程序博客网 时间:2024/05/06 18:56

转载地址:http://hi.baidu.com/icexile/blog/item/fe4a224cf41dbcf0d72afcaa.html

近期初学winsock2……,编写了一个小小的使用IOCP的服务器测试程序。总结了一下程序的重点部分:
为何使用IOCP:

1、传统的socket服务程序流程是这样:

创建侦听socket -> 绑定socket和IP、端口号 -> listen() -> 循环accept客户端的连接、收发数据recv/send

此模型很古董,俺觉得也没什么实用价值……因为accept()函数,revc()/send()都是阻塞式的,如果有客户端响应慢等情况,服务器线程会卡在这些函数上出不来,状似瘫痪。

2、多线程模型:

与前面的模型差不多一样,只是最后一步,accept连接以后,创建一个新的线程去与客户端交互。

此模型也不咋实用(我认为)……且不说主线程还是会卡死在accept函数上,单看如果有并发很多个客户端连接的时候,服务器创建了N多线程,时间都花在了切换线程context上,效率欠佳…… (正因为Microsoft发现过多的工作线程并不能提高工作效率,于是弄出了IO完成端口,以适应大规模IO交互服务)

3、异步IO模型:

Windows提供了一些异步IO的设施供程序员使用,使用这些异步机制,可以编写出无阻塞,响应度比较好的程序。下表中列出了Windows下支持异步传输数据的设备(摘自《Windows核心编程》):

设备常见用法文件存储数据目录属性和文件压缩设置逻辑磁盘驱动器格式化驱动器物理磁盘驱动器访问分区表串口通过电话线传输数据并口将数据传输至打印机邮件槽一对多数据传输,通常是通过网络传到另一台运行Windows的主机上命名管道一对一数据传输,通常是通过网络传到另一台运行Windows的主机上匿名管道单机上的一对一数据传输(绝不会跨网络)socket报文数据流的传输,通过网络传输到任何支持socket的主机上控制台文本窗口的屏幕缓存

可以看出,利用Windows的异步IO机制可以实现socket的异步传输,因此下文中将把Socket当作一种“设备”,收发数据当作IO操作。

假设一个线程想向socket发出一个异步IO请求,这个请求被传给网络驱动程序,后者负责完成实际IO操作。当驱动程序在等待设备响应的时候,应用程序线程并没有被挂起,线程会继续运行其它有用的任务。

向底层投递异步IO请求比较简单,针对于Socket来说,使用WSASocket创建套接字时指定WSA_FLAG_OVERLAPPED标志,则该socket带有了异步传输属性,而后,使用WSASend()、WSARecv()代替原来的send()、recv()函数就实现了异步IO的投递。

使用这些函数投递异步IO请求时,必须传入一个OVERLAPPED结构的地址,该结构告诉底层驱动程序从哪开始传(偏移量)、以及传完了后用于通知应用程序的事件对象句柄,并且,当调用WSASend等函数投递异步传输完成后,不管操作系统以什么方式通知用户IO结束,用户都可以取回此次异步传输调用时传递给WSASend等函数的Overlapped结构,因此,它十分适合于被扩充,以携带用户定义的、异步IO完成后要用的数据,比如该客户的标识啦……等等。

typedef struct _OVERLAPPED{
  DWORD Internal; //内部错误码
  DWORD InternalHigh; //异步传输字节数
  DWORD Offset; //低32位偏移量
  DWORD OffsetHigh; //高32位便宜量
  HANDLE hEvent; //用于通知完成的事件句柄
}OVERLAPPED, *LPOVERLAPPED;

由于每次异步传输都要使用一个Overlapped,因此,它代表了这次异步IO。通常,我们对其进行扩充以携带自定义的数据的方法有:

struct OverlappedEx {
  OVERLAPPED ol_;
  //自定义的数据...
};

这样,由于ol_的首地址就是OverlappedEx的首地址,因此很容易在完成异步IO后,把取得的Overlapped指针强制转换回OverlappedEx。

struct SuperOverlapped: public OVERLAPPED {
  //自定义数据....
};

此方法直接从OVERLAPPED派生出来自己的结构,传递指针时不用强制转换,比较方便。

到某一时刻,设备驱动程序完成了IO操作,这时,他必须通知应用程序数据已发送、或者出错。这些通知方法大体包括以下四种:

1、触发此次IO操作使用的设备对象。它允许一个线程发出IO请求,比如一个线程在socket上WSASend(),另一个线程在该socket上WaitForSingleObject以收到完成通知。缺点是当提交多个异步传输请求时,此方法失效,因为线程不知道通知它的是哪次传输成功了。

2、触发事件内核对象。 调用WSASend或是WSARecv时传入的OVERLAPPED结构中,可以创建一个事件内核对象,并将其赋值给hEvent成员,当此次异步传输完成时,hEvent将被触发。这时,如果有线程正在WaitForSingleObject(lpOverlapped->hEvent, ...),则该线程将会被唤醒。 此模型避免了上面的缺陷,但是如果不同时期投递多个IO请求的话,就有多个Overlapped结构,每个结构代表了一次异步操作,因此工作线程需要WaitForMultipleObject,而且需要将hEvent形成一个数组作为该函数的参数,由于异步请求数量在程序运行时是会变的,因此程序员编写工作线程时要维护这样一个全局的Overlapped的数组,还要做一些互斥操作。

3、使用可提醒I/O。WSASend或者是WSARecv最后一个参数是LPWSAOVERLAPPED_COMPLETION_ROUTINE类型,这是一个函数指针,当调用WSASend或是WSARead时,如果最后这个参数不为NULL,则IO完成时,该指针所指向的函数将被调用。《Windows核心编程》作者指出,可提醒I/O非常糟糕,应该避免使用,原因是 a)需要使用回调函数,这将使得代码的实现变得更加复杂,我们不得不将相关的信息放在全局变量中以便该函数参考。b)发出IO请求的线程必须同时对完成通知进行处理,如果一个线程发出多个IO请求,即使其它线程处于空闲状态,也无法帮助此线程处理消息。

4、使用IO完成端口。此乃最佳方案,允许向一个设备发出多个IO请求,允许一个线程发出IO请求,操作系统完成传输后,另一个线程对结果进行处理,下面详细讨论。

IOCP使用方法:

IOCP全名叫做…… Input / Output Completion Port(……汗一个……),感觉他意思就是说,这是一个专门提醒别人“IO操作已经完事儿”的东东。因此,它内部有个设备列表,用户把某个socket传进IOCP时,它就把这个socket加入到设备列表中监视着。当用户调用了WSASend、WSARecv,设备驱动程序也完成了传输的时候,IOCP会监测到该设备上IO操作完事儿了,于是乎就提醒正在等待该消息的线程~基本就是这个过程……下面函数可以实现1)创建一个IOCP以及2)将某个设备加入IOCP的监测列表中:

HANDLE WINAPI CreateIoCompletionPort(
__in HANDLE FileHandle, //这里传入想要监测的设备句柄
__in_opt HANDLE ExistingCompletionPort, //这里传入要把设备加入的那个IOCP的句柄,传入NULL表示新建一个
__in ULONG_PTR CompletionKey, //这里传入跟要监测的这个设备相关的用户自定义的数据
__in DWORD NumberOfConcurrentThreads //这里传入最大允许同时运行的线程数
);

下面这个函数实现工作线程在IOCP上等待IO操作完成的通知:

BOOL WINAPI GetQueuedCompletionStatus(
__in HANDLE CompletionPort, //这里传入要在哪个IOCP上等待通知
__out LPDWORD lpNumberOfBytes, //这里传出了完成端口告诉用户IO传输了多少字节
__out PULONG_PTR lpCompletionKey, //这里传出了上一个函数中用户传入的针对于每个设备的参数
__out LPOVERLAPPED* lpOverlapped, //这里传出了用户调用WSASend等函数时的那个OVERLAPPED结构的指针
__in DWORD dwMilliseconds //这里传入线程等多少毫秒还没有通知就不等了
);

因此,工作线程一般都是这样:

DWORD WINAPI IOCPWorkerThreadStart(LPVOID lpParam) {
  //初始化....
  
  while(true) {
    bRet = GetQueuedCompletionStatus(hIOCP, &dwBytes, &lpCompletionKey, (LPOVERLAPPED*)&lpOverlapped, INFINITE);
    //处理IO完成以后需要的操作,以及判断是否退出
    //....
  }

  return 0;
}

示例程序:

本人使用IOCP和WinSock2,编写了一个服务器验证程序,该服务器可以简单的接收多个客户端的消息,并传回“×××消息已收到”。程序流程如下:

IOCP流程图

其实,使用AcceptEx函数是可以投递异步Accept请求的,这样,当新的客户端连接进来时,操作系统会完成accept工作,并通知工作线程。但是这样做的话,你无法知道什么时刻会并行连接连接进来多少客户端,只能使用某种策略,当IOCP通知工作线程某个连接已完成时,看情况继续投递合适数量的AcceptEx请求给操作系统。并且,MSDN杂志某文章http://msdn.microsoft.com/en-us/magazine/cc302334.aspx指出,"it is best to have a separate thread that posts AcceptEx and is not involved in other I/O processing"。

故而,本程序新创建了个线程专门用于accept客户端连接,其实在此线程中还可以完成其它的后台工作,比如记录日志等。而主线程使用WSAEventSelect()函数注册了Accept事件,当操作系统发现有客户端连接时,将激发此事件,accept线程侦测到该事件,直接使用accept函数接受连接并创建了新的客户socket,这里由于已经知道有一个客户端连接进来了,因此调用accept函数也不会阻塞线程。

还可以使用Windows提供的默认线程池来管理工作线程,动态的调整工作线程的数量,由于Microsoft在Vista操作系统中重新规划了其线程池,因此XP下和Vista下编写方法、调用的API是不一样的,这里不再讨论了。详情可以参看《Windows核心编程》中线程池这一章的“在异步I/O请求完成时调用一个函数”主题。不同的是《Windows核心编程》第四版中描述了2000/XP下的线程池;《Windows核心编程》第五版中描述的是Vista及更高版本的Windows线程池。

程序运行截图如下:

IOCP程序截图

程序在Visual Studio 2008 SP1 专业版下编的,为了图省事,基本上写在一个cpp文件里了……牛人看了不要雷俺,写的很烂……代码仅供参考……如果有不妥的地方,请留言告诉俺,俺好改正……。

下载连接:http://www.uushare.com/user/icexile/file/1813161

原创粉丝点击