如果你想在Windows平台上构建服务器应用,那么I/O模型是你必须考虑的。Windows操作系统提供了选择模型、异步选择模型、事件选择模型、重叠I/O模型和完成端口共五种I/O模型。每一种模型均适用于一种特定的应用场景。编程人员应综合考虑到程序的扩展性和可移植性等因素,做出自己的选择。
1 选择模式(Select)
选择模型是Winsock中最常见的I/O模型。之所以称其为“select模型” ,是由于它的“中心思想”便是利用select函数,实现对I/O的管理!最初设计该模型时,主要面向的是某些使用Unix操作系统的计算机,它们采用的是 Berkeley套接字方案。select模型已集成到Winsock 1.1中,它使那些想避免在套接字调用过程中被无辜“锁定”的应用程序,采取一种有序的方式,同时进行对多个套接字的管理。
2 异步选择模式(WSAAsyncSelect)
使用异步选择模型,应用程序可在一个套接字上,接收以Windows消息为基础的网络事件通知。具体的做法是在建好一个套接字后,调用WSAAsyncSelect函数。
3 事件选择模式(WSAEventSelect)
WSAEventSelect和WSAAsyncSelect模型类似,它也允许应用程序在一个或多个套接字上,接收以事件为基础的网络事件通知。对于WSAAsyncSelect模型采用的网络事件来说,它们均可原封不动地移植到事件选择模型上。在用事件选择模型开发的应用程序中,也能接收和处理所有那些事件。该模型最主要的差别在于网络事件会投递至一个事件对象句柄,而非投递至一个窗口例程。
4 重叠I/O模式(Overlapped I/O)
在Winsock中,相比我们迄今为止解释过的其他所有I/O模型,重叠I/O模型使应用程序能达到更佳的系统性能。重叠模型的基本设计原理便是让应用程序使用一个重叠的数据结构,一次投递一个或多个Winsock I/O请求。针对那些提交的请求,在它们完成之后,应用程序可为它们提供服务。该模型适用于除Windows CE之外的各种Windows平台。模型的总体设计以Win32重叠I/O机制为基础。那个机制可通过ReadFile和WriteFile两个函数,针对设备执行I/O操作。
5 完成端口模式(Completion Port)
“完成端口”模型是迄今为止最为复杂的一种I/O模型。然而,假若一个应用程序同时需要管理为数众多的套接字,那么采用这种模型,往往可以达到最佳的系统性能!但不幸的是,该模型只适用于Windows NT和Windows 2000操作系统。
因其设计的复杂性,只有在你的应用程序需要同时管理数百乃至上千个套接字的时候,而且希望随着系统内安装的CPU数量的增多,应用程序的性能也可以线性提升,才应考虑采用“完成端口”模型。要记住的一个基本准则是,假如要为Windows NT或Windows 2000开发高性能的服务器应用,同时希望为大量套接字I/O请求提供服务(Web服务器便是这方面的典型例子),那么I/O完成端口模型便是最佳选择!
异步选择I/O模式
Winsock提供了一个有用的异步 I/O模型。利用这个模型,应用程序可在一个套接字上,接收以Windows消息为基础的网络事件通知。具体的做法是在建好一个套接字后,调用WSAAsyncSelect函数。该模型最早出现于Winsock的1.1版本中,用于帮助应用程序开发者面向一些早期的1 6位Windows平台(如Windows for Workgroups) ,适应其“落后”的多任务消息环境。应用程序仍可从这种模型中得到好处,特别是它们用一个标准的 Windows例程(常称为“winproc”),对窗口消息进行管理的时候。该模型亦得到了 Microsoft Foundation Class(微软基本类,MFC)对象CSocket的采纳。
要想使用WSAAsyncSelect模型,在应用程序中,首先必须用CreateWindow函数创建一个窗口,再为该窗口提供一个窗口例程支持函数(Winproc)。亦可使用一个对话框,为其提供一个对话例程,而非窗口例程,因为对话框本质也是“窗口”。设置好窗口的框架后,便可开始创建套接字,并调用WSAAsyncSelect函数,打开窗口消息通知。该函数的定义如下:
int WSAAsyncSelect(
SOCKET s,
HWND hWnd,
usigned int wMsg,
long lEvent
);
其中,s参数指定的是我们感兴趣的那个套接字。hWnd参数指定的是一个窗口句柄,它对应于网络事件发生之后,想要收到通知消息的那个窗口或对话框。wMsg参数指定在发生网络事件时,打算接收的消息。该消息会投递到由hWnd窗口句柄指定的那个窗口。通常,应用程序需要将这个消息设为比 Windows的WM_USER大的一个值,避免网络窗口消息与预定义的标准窗口消息发生混淆与冲突。最后一个参数是lEvent,它指定的是一个位掩码,对应于一系列网络事件的组合(请参考表2-1) ,应用程序感兴趣的便是这一系列事件。大多数应用程序通常感兴趣的网络事件类型包括:FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT和FD_CLOSE。当然,到底使用FD_ACCEPT,还是使用FD_CONNECT类型,要取决于应用程序的身份到底是一个客户机呢,还是一个服务器。如应用程序同时对多个网络事件有兴趣,只需对各种类型执行一次简单的按位OR(或)运算,然后将它们分配给lEvent就可以了。举个例子来说:
WSAAsyncSelect(s, hwnd, WM_SOCKET,
FD_CONNECT | FD_READ | FD_WRITE | FD_CLOSE);
这样一来,我们的应用程序以后便可在套接字s上,接收到有关连接、发送、接收以及套接字关闭这一系列网络事件的通知。特别要注意的是,多个事件务必在套接字上一次注册!另外还要注意的是,一旦在某个套接字上允许了事件通知,那么以后除非明确调用closesocket命令,或者由应用程序针对那个套接字调用了WSAAsyncSelect,从而更改了注册的网络事件类型,否则的话,事件通知会永远有效!若将lEvent参数设为0,效果相当于停止在套接字上进行的所有网络事件通知。
若应用程序针对一个套接字调用了WSAAsyncSelect,那么套接字的模式会从“锁定”自动变成“非锁定”,我们在前面已提到过这一点。这样一来,假如调用了像WSARecv这样的Winsock I/O函数,但当时却并没有数据可用,那么必然会造成调用的失败,并返回WSAEWOULDBLOCK错误。为防止这一点,应用程序应依赖于由WSAAsyncSelect的uMsg参数指定的用户自定义窗口消息,来判断网络事件类型何时在套接字上发生;而不应盲目地进行调用。
表3-1 用于WSAAsyncSelect函数的网络事件类型
应用程序在一个套接字上成功调用WSAAsyncSelect之后,应用程序会在与hWnd窗口句柄参数对应的窗口例程中,以Windows消息的形式,接收网络事件通知。窗口例程通常定义如下:
LRESULT CALLBACK WindowProc(
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
);
其中,hWnd参数指定一个窗口的句柄,对窗口例程的调用正是由那个窗口发出的。uMsg参数指定需要对哪些消息进行处理。就我们的情况来说,感兴趣的是WSAAsyncSelect调用中定义的消息。wParam参数指定在其上面发生了一个网络事件的套接字。假若同时为这个窗口例程分配了多个套接字,这个参数的重要性便显示出来了。在lParam参数中,包含了两方面重要的信息。其中,lParam的低字(低位字)指定了已经发生的网络事件,而lParam的高字(高位字)包含了可能出现的任何错误代码。
网络事件消息抵达一个窗口例程后,应用程序首先应检查lParam的高字位,以判断是否在套接字上发生了一个网络错误。这里有一个特殊的宏:WSAGETSELECTERROR,可用它返回高字位包含的错误信息。若应用程序发现套接字上没有产生任何错误,接着便应调查到底是哪个网络事件类型,造成了这条Windows消息的触发—具体的做法便是读取lParam之低字位的内容。此时可使用另一个特殊的宏:WSAGETSELECTEVENT,用它返回lParam的低字部分。