资源描述
目录
摘要 3
绪论 5
1 网络编程接口简介 6
2 WINSOCK基本概念 8
2.1 Winsock规范介绍 8
2.2 Winsock基本过程 9
2.3 客户机/服务器模型 9
2.4 套接字模式(阻塞/非阻塞) 10
2.5 同步/异步 13
3 套接字I/O 模型 14
3.1 select 模型 14
3.2 WSAAsyncSelect 模型 15
3.3 WSAEventSelect 模型 17
3.4 重叠I/O模型 20
3.4.1 事件通知 21
3.4.2 完成例程 24
3.5 完成端口模型 25
3.5.1 工作者线程与完成端口 26
3.5.2 完成端口和重叠I/O 27
3.5.3 单句柄数据和单I/O操作数据 28
3.5.4 其他问题 29
4 WINSOCK封装 30
4.1 VB Winsock 控件 30
4.1.1 Winsock控件的通信模式 30
4.1.2 Winsock控件的基本事件 30
4.1.3 Winsock控件应用分析 31
4.1.4 Winsock控件应用举例 31
4.1.4.1 Email客户端分析 31
4.1.4.2 协议分析 32
4.1.4.3 程序 32
4.2 MFC 套接字类 32
4.2.1 CAsyncSocket 32
4.2.2 CSocket 33
5 WINSOCK高级应用 35
5.1 多播 35
5.1.1 多播的有根与无根 35
5.1.2 IP多播 35
5.1.2.1 Internet网关管理协议 36
5.1.2.2 加入多播组 37
5.2 常规质量服务(GQoS) 38
5.2.1 资源预约协议 38
5.2.2 网络设备 39
5.2.3 本地工作站 39
5.2.4 策略组件 39
5.2.5 Winsock2与QoS 39
结论 41
致谢 42
参考文献 43
附录 45
摘要
本文将介绍网络编程接口的概念以及详细探讨在Windows操作系统上的网络编程接口。本文首先回顾了Winsock与其他网络编程接口的关系,并对网络编程接口的基本概念作了简单的介绍。然后通过对Winsock的输入/输出模型的分析,我会对网络应用的网络接口部分实的现方案做归类和分析。之后,本文的话题会涉及到应用相当广泛的Winsock的封装上面来,并对使用这些封装好了的VB Winsock 控件和Socket MFC类(CAsyncSocket类和Csocket类)的使用方法作介绍。最后,本文将描述多播和常规质量服务的概念。
关键字:
套接字、阻塞/非阻塞、I/O模型、封装、高级应用
Abstract
The general concepts of Network Programming will be introduced in this paper. This paper will give a review of the Winsock’s relationship to others’ network programming interface,and show the basic concepts in this filed. And particular discussion of the network programming interface under the "Microsoft Windows" OS could also be seen in this paper.I will analysis schemes of network interfaces in network applications.And then, the encapsulation of Winsock will be the a topic.Useage of the Winsock ActiveX controls and Socket classes of MFC,which is widely used,will be introduced.Lastly,this paper will describe the "Multicasting" and "QoS".
Keywords:
Socket ,Block/Unblock ,I/O Model ,Encapsulation , Advanced Applications
绪论
有人这样说:“使用计算机就是使用网络。”这句话在现在看来,虽然有些夸张,但不乏道理。网络应用在近几年迅速崛起,各行各业无不将办公网络化、管理网络化、生产网络化作为现代化企业必须的标志。另外一方面,人们的娱乐活动,交流方式也发生了巨大的变化,网络也渗入生活的每个角落。
有网络,必有应用。而对本专业来讲:应用的质量是我们所关心的。如何建立标准的,可靠的,高性能的网络应用,将是一个巨大的话题。而本文将在这个巨大话题中,选取一小块——Winsock通信及应用——进行探讨。
本文的主题其实已经有相当的历史了——在网络技术方面而言,成果相当丰硕。仅凭一人一时之力,难以面面俱到,因此就不妨分清重点,更取部分,加以探讨。
1 网络编程接口简介
计算机联网应用可以说是计算机应用发展历史上的划时代时刻。从此,计算机应用不再局限于地域性,计算机之间的通信,可以使应用的思维方式发生重大的转变。其代表性的应用方式就是通常所谓的C/S(客户端/服务器)模式。
当然,在这之外,还有很多别的模式,这里且不提。所有这些模式都遵循自己的标准,以便不同应用之间的交互。而这个标准,就是所谓的网络编程接口。
什么是网络编程接口?可以这样理解:应用程序是运行在操作系统之上的,它的许多功能在底层上由操作系统提供。那么,网络编程接口就可以认为是介于操作系统与应用之间的用户界面,而这里的用户就是网络编程人员。图1-1表示了它们之间的关系。
网络应用(Network Applications)
网络编程接口(Network Interface)
操作系统(OS)
物理计算机
图1-1 网络编程接口在系统中所处地位
事实上,在上图中的中间两层,也就是网络编程接口和操作系统的关系上面,也可以有另外一种情况,也就是接口的实现方式问题:是库函数还是系统调用?如图1-2所示:
系统调用
网络编程接口
系统调用
网络编程接口库
调用
开发中的网络应用
开发中的网络应用
OS
OS
编译器界线
图1-2
左:接口是操作系统调用的一部分,是操作系统的一部分;
右:接口可以只是由某组织开发的库,该库遵循标准;
但两者对开发人员而言,在应用的源代码级别上是一致的.
在以上文字中,我们反复提到了“标准”这个词,那么我们就有必要具体的谈一谈标准的接口是什么样的。
NETBIOS(NETWORK BASIC INPUT/OUTPUT SYSTEM 网络基本输入输出系统),是一个与协议无关的网络编程接口。它是一种标准的应用程序编程接口(API),1983年由sytek公司专为IBM开发成功。NETBIOS为网络通信定义了一种编程接口。NETBIOS提供了异步调用,同时兼容于较老的操作系统,如OS/2和DOS等等。
Socket(套接字)是一个多重概念,它既代表一个接口标准,也代表这个标准的实现――套接字API。最早美国加州大学Berkeley分校在UNIX下为TCP/IP协议开发了一个API,这个API就是著名的Berkeley Socket。它是作为BSD UINIX的一部分而分发的。随着unix操作系统的流行,Berkeley Socket就逐渐成为了一个事实上的标准,而得到了广泛的应用。而且实际上,很多系统开发商为了使自己的系统兼容于这个标准,便开发了自己的套接字库(Socket library),来提供套接字API。
Winsock(Windows Socket)是从 Berkeley Sockets 扩展而来的,其在继承 Berkeley Sockets 的基础上,又进行了新的扩充。这些扩充主要是提供了一些异步函数,并增加了符合WINDOWS消息驱动特性的网络事件异步选择机制。Winsock的最初版本功能十分有限,仅支持TCP/IP协议。而自Winsock1.1开始,Winsock成为了一个“协议无关的”接口标准。现在广泛使用的Winsock2则更是添加了许多强有力的功能。
我们以后的内容将围绕着Winsock展开。
2 Winsock基本概念
2.1 Winsock规范介绍
如前所述,微软以U.C. Berkeley大学BSD UNIX中流行的Socket接口为范例定义了一套Micosoft Windows下网络编程接口。它不仅包含了人们所熟悉的Berkeley Socket风格的库函数;也包含了一组针对Windows的扩展库函数,以使程序员能充分地利用Windows消息驱动机制进行编程。
Windows Sockets规范本意在于提供给应用程序开发者一套简单的API,并让各家网络软件供应商共同遵守。此外,在Winsock 2中还定义了一个二进制接口(ABI),以此来保证应用Windows Sockets API的应用程序能够在任何网络软件供应商的符合Windows Sockets协议的实现上工作。从而实现了真正的“协议无关性”。这份规范定义了应用程序开发者能够使用,并且网络软件供应商能够实现的一套库函数调用和相关语义。
遵守这套Windows Sockets规范的网络软件,我们称之为Windows Sockets兼容的,而Windows Sockets兼容实现的提供者,我们称之为Windows Sockets提供者。一个网络软件供应商必须百分之百地实现Windows Sockets规范才能做到现Windows Sockets兼容。
任何能够与Windows Sockets兼容实现协同工作的应用程序就被认为是具有Windows Sockets接口。我们称这种应用程序为Windows Sockets应用程序。
Windows Sockets规范定义并记录了如何使用API与Internet协议族(IPS,通常我们指的是TCP/IP)连接,尤其要指出的是所有的Windows Sockets实现都支持流套接口和数据报套接口.
应用程序调用Windows Sockets的API实现相互之间的通讯。Windows Sockets又利用下层的网络通讯协议功能和操作系统调用实现实际的通讯工作。它们的关系如图2.1-1所示:
图2.1-1 Winsock在与应用及操作系统的关系
2.2 Winsock基本过程
Winsock的过程函数继承了Berkeley Socket的风格,其调用步骤和调用方式基本一致.这里只做一下简单介绍,关于这些过程本身的更详细的信息将不在本文中讨论,可以参照《Windows Sockets规范及应用》等相关书籍,它们对标准本身进行了详尽的描述。
下面是常规的过程及调用顺序:
Socket():创建一个通讯端点并返回一个套接口。
bind():把一个本地的名字和一个无名的套接口捆绑起来。
connect():初始化到一个指定套接口上的连接。
listen():监听某一指定套接口上连接请求的到来。
accept():响应联结请求,并且新建一个套接口。原来的套接口则返回监听状态。
recv():从一个已连接的套接口接收数据。
send():从一已连接的套接口发送数据。
closeSocket():把套接口从拥有对象参考表中取消。该函数只有在SO_LINGER被设置时才会阻塞。
进一步,这些过程涉及到Winsock的连接类型:面向连接的和无连接的.比如:recv()过程适用于面向连接的,而对于无连接的则有recvfrom(). send()与sendto()之间的关系类似.
2.3 客户机/服务器模型
一个在建立分布式应用时最常用的范例便是客户机/服务器模型。在这种方案中客户应用程序向服务器程序请求服务。这种方式隐含了在建立客户机/服务器间通讯时的非对称性。客户机/服务器模型工作时要求有一套为客户机和服务器所共识的惯例来保证服务能够被提供(或被接受)。这一套惯例包含了一套协议。它必须在通讯的两头都被实现。根据不同的实际情况,协议可能是对称的或是非对称的。在对称的协议中,每一方都有可能扮演主从角色;在非对称协议中,一方被不可改变地认为是主机,而另一方则是从机。一个对称协议的例子是Internet中用于终端仿真的TELNET。而非对称协议的例子是Internet中的FTP。无论具体的协议是对称的或是非对称的,当服务被提供时必然存在“客户进程”和“服务进程”。
一个服务程序通常在一个众所周知的地址监听对服务的请求,也就是说,服务进程一直处于休眠状态,直到一个客户对这个服务的地址提出了连接请求。在这个时刻,服务程序被“惊醒”并且为客户提供服务-对客户的请求作出适当的反应。这一请求/相应的过程可以简单的用图2.3-1表示。虽然基于连接的服务是设计客户机/服务器应用程序时的标准,但有些服务也是可以通过数据报套接口提供的。
图2.3-1客户机/服务器模型
2.4 套接字模式(阻塞/非阻塞)
套接字模式用于决定在随一个套接字调用时,那些Winsock函数的行为.具体而言:处于阻塞(Block)模式下,函数会在执行完毕之后返回,如果函数应为某种原因(比如说网络故障等)不能执行完毕,则函数会挂起,导致所在网络应用程序进程(线程)挂起.而当函数为非阻塞(Unblock)模式下时,无论操作是否能够正常完成,函数都会返回.
在阻塞模式下,程序的编制要注意一个问题:就是函数能否及时的被执行?如果不能,则导致程序停留在该函数处,而不能继续。所以使用阻塞模式的条件应该有如下要求:第一种选择,保证该应用程序的使用环境,使得不会出现函数无法返回的情况;第二种选择,使用多线程技术解决这个问题。
在非阻塞模式下,则不必为上述情况担心。因为函数会返回一个错误代码——WSAEWOULDBLOCK。这使得程序不仅不会挂起,还使得我们可以判断函数是否是正常返回。但是这样做也有代价:首先,我们就不得不写更多的代码用以处理错误代码;另外一方面,不断的循环调用函数会使得大量的CPU时间被占用。
下面,这里提供了三个程序来演示上面所述。这三个程序分别是:
lockser.exe 服务器端的阻塞版本
ulockser.exe 服务器端的非阻塞版本
lockcl.exe 客户端程序,使用阻塞模式(但实际在这里,使用什么模式不重要)
这三个程序的相关代码会见附录中的说明。
图2.4-1所示的是lockser.exe与lockcl.exe两个程序,可以看到当客户端程序不向服务器端发送消息时,服务器端程序处于等待状态.
图2.4-1 阻塞模式范例(1)
图2.4-2 阻塞模式范例(2)
而直至客户端发送了消息“test!”之后,服务端程序才能够继续(见上图).
对于非阻塞模式的例子,如图2.4-3和图2.4-4:所示
图2.4-3 非阻塞模式范例(1)——未连接
图2.4-4 非阻塞模式范例(2)——已连接
当客户端发送了消息之后,服务器端接收到消息,并从缓存读消息(图2.4-5):
图2.4-5 非阻塞模式--接收消息
2.5 同步/异步
通信的方式,有同步/异步之分,这并不仅限于Winsock通信。同步(Sync)指的是通信双方不论对方的状态如何,都不断的发送数据。相反的,异步(ASync)就是指发送方只有在收到对方的确认消息之后才发送数据。
3 套接字I/O 模型
在2.4小节中,我们提到了阻塞/非阻塞两种I/O模式。每种模式各有优缺点。那么在实际应用中,我们应该如何选择呢?
实际上,Winsock为我们提供了5种I/O模型用以开发网络应用程序。你可以将这5种模型看作5种解决2.4小节中提到的问题的解决方案。这5种模型解决了包括阻塞与非阻塞模式的问题。下面我们就分别介绍这5种模型。
3.1 select 模型
select(选择)模型是Winsock中最常见的I/O模型。之所以称其为“select模型”,是由于它的“中心思想”便是利用select函数,实现对I/O的管理!利用select函数,我们判断套接字上是否存在数据,或者能否向一个套接字写入数据。之所以要设计这个函数,唯一的目的便是防止应用程序在套接字处于阻塞模式中时,在一次I/O绑定调用(如send或recv)过程中,被迫进入“阻塞”状态;同时防止在套接字处于非阻塞模式中时,产生WSAWOULDBLOCK错误。除非满足事先用参数规定的条件,否则select函数会在进行I/O操作时阻塞。select的函数原型如下:
int select(
int nfds,
fd_set FAR * readfds,
fd_set FAR * writefds,
fd_set FAR * exceptds,
const struct timeval FAR * timeout
);
其中,第一个参数nfds会被忽略。这个参数只是为了保持与早期的Berkeley套接字应用程序的兼容。剩下的三个fd_set参数:一个用于检查可读性(readfds),一个用于检查可写性(writefds),另一个用于例外数据(exceptfds)。从根本上说,fd_set数据类型代表着一系列特定套接字的集合。其中, readfds集合包括符合下述任何一个条件的套接字:
· 有数据可以读入。
· 连接已经关闭、重设或中止。
· 假如已调用了listen,而且一个连接正在建立,那么accept函数调用会成功。
writefds集合包括符合下述任何一个条件的套接字:
· 有数据可以发出。
· 如果已完成了对一个非阻塞连接调用的处理,连接就会成功。
最后,exceptfds集合包括符合下述任何一个条件的套接字:
· 假如已完成了对一个非阻塞连接调用的处理,连接尝试就会失败。
· 有带外(Out-of-band ,OOB)数据可供读取。
例如,假定我们想测试一个套接字是否“可读”,必须将自己的套接字增添到readfds集合,再等待select函数完成。select完成之后,必须判断自己的套接字是否仍为readfds集合的一部分。若答案是肯定的,便表明该套接字“可读”,可立即着手从它上面读取数据。在三个参数中(readfds、writefds和exceptfds),任何两个都可以是空值(NULL);但是,至少有一个不能为空值!在任何不为空的集合中,必须包含至少一个套接字句柄;否则,select函数便没有任何东西可以等待。最后一个参数timeout对应的是一个指针,它指向一个timeval结构,用于决定select最多等待I/O操作完成多久的时间。如timeout是一个空指针,那么select调用会无限期地“阻塞”或停顿下去,直到至少有一个描述符符合指定的条件后结束。对timeval结构的定义如下:
strucr timeval
{ long tv_sec;
long tv_usec;
};
其中,tv_sec字段以秒为单位指定等待时间; tv_usec字段则以毫秒为单位指定等待时间。若将超时值设置为(0,0),表明select会立即返回,允许应用程序对select操作进行“轮询”。出于对性能方面的考虑,应避免这样的设置。select成功完成后,会在fd_set结构中,返回刚好有未完成的I/O操作的所有套接字句柄的总量。若超过timeval设定的时间,便会返回0。不管由于什么原因,假如select调用失败,都会返回Socket_ERROR。
用select对套接字进行监视之前,在自己的应用程序中,必须将套接字句柄分配给一个集合,设置好一个或全部读、写以及例外fd_set结构。将一个套接字分配给任何一个集合后,再来调用select,便可知道一个套接字上是否正在发生上述的I/O活动。Winsock提供了下列宏操作,可用来针对I/O活动,对fd_set进行处理与检查:
· FD_CLR(s, *set):从set中删除套接字s。
· FD_ISSET(s, *set):检查s是否set集合的一名成员;如答案是肯定的是,则返回TRUE。
· FD_SET(s, *set):将套接字s加入集合set。
· FD_ZERO(*set):将set初始化成空集合。
例如,假定我们想知道是否可从一个套接字中安全地读取数据,同时不会陷于无休止的“阻塞”状态,便可使用FD_SET宏,将自己的套接字分配给fd_read集合,再来调用select。要想检测自己的套接字是否仍属fd_read集合的一部分,可使用FD_ISSET宏。采用下述步骤,便可完成用select操作一个或多个套接字句柄的全过程:
1) 使用FD_ZERO宏,初始化自己感兴趣的每一个fd_set。
2) 使用FD_SET宏,将套接字句柄分配给自己感兴趣的每个fd_set。
3) 调用select函数,然后等待在指定的fd_set集合中,I/O活动设置好一个或多个套接字句柄。select完成后,会返回在所有fd_set集合中设置的套接字句柄总数,并对每个集合进行相应的更新。
4) 根据select的返回值,我们的应用程序便可判断出哪些套接字存在着尚未完成(待决)的I/O操作—具体的方法是使用FD_ISSET宏,对每个fd_set集合进行检查。
5) 知道了每个集合中“待决”的I/O操作之后,对I/O进行处理,然后返回步骤1),继续进行select处理。
select返回后,它会修改每个fd_set结构,删除那些不存在待决I/O操作的套接字句柄。
3.2 WSAAsyncSelect 模型
Winsock提供了一个有用的异步I / O模型。利用这个模型,应用程序可在一个套接字上,接收以Windows消息为基础的网络事件通知。具体的做法是在建好一个套接字后,调用WSAAsyncSelect函数。
要想使用WSAAsyncSelect模型,在应用程序中,首先必须用CreatWindow函数创建一个窗口,再为该窗口提供一个窗口例程支持函数(Winproc)。亦可使用一个对话框,为其提供一个对话例程,而非窗口例程,因为对话框本质也是“窗口”。考虑到我们的目的,我们打算用一个简单的窗口来演示这种模型,采用的是一个支持窗口例程。设置好窗口的框架后,便可开始创建套接字,并调用WSAAsyncSelect函数,打开窗口消息通知。该函数的定义如下:
int WSAAsyncSelect(
Socket s,
HWND hWnd,
unsigned int wMsg,
long lEvent
)
其中,s参数指定的是我们感兴趣的那个套接字。hWnd参数指定的是一个窗口句柄,它对应于网络事件发生之后,想要收到通知消息的那个窗口或对话框。wMsg参数指定在发生网络事件时,打算接收的消息。该消息会投递到由hWnd窗口句柄指定的那个窗口。通常,应用程序需要将这个消息设为比Windows的WM_USER大的一个值,避免网络窗口消息与预定义的标准窗口消息发生混淆与冲突。最后一个参数是lEvent,它指定的是一个位掩码,对应于一系列网络事件的组合(请参考表3.2-1),应用程序感兴趣的便是这一系列事件。大多数应用程序通常感兴趣的网络事件类型包括: FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT和FD_CLOSE。当然,到底使用FD_ACCEPT,还是使用FD_CONNECT类型,要取决于应用程序的身份到底是一个客户机,还是一个服务器。如应用程序同时对多个网络事件有兴趣,只需对各种类型执行一次简单的按位OR(或)运算,然后将它们分配给lEvent就可以了。举个例子来说:
WSAAsyncSelect(s,hwnd,WM_Socket,FD_SONNECT|FD_READ|FD_WRITE|FD_CLOSE);
这样一来,我们的应用程序以后便可在套接字s上,接收到有关连接、发送、接收以及套接字关闭这一系列网络事件的通知。特别要注意的是,多个事件务必在套接字上一次注册!
另外还要注意的是,一旦在某个套接字上允许了事件通知,那么以后除非明确调用closeSocket命令,或者由应用程序针对那个套接字调用了WSAAsyncSelect,从而更改了注册的网络事件类型,否则的话,事件通知会永远有效。若将lEvent参数设为0,效果相当于停止在套接字上进行的所有网络事件通知。
若应用程序针对一个套接字调用了WSAAsyncSelect,那么套接字的模式会从“阻塞”自动变成“非阻塞”,我们在前面已提到过这一点。这样一来,假如调用了像WSArecv这样的Winsock I/O函数,但当时却并没有数据可用,那么必然会造成调用的失败,并返回WSAWOULDBLOCK错误。为防止这一点,应用程序应依赖于由WSAAsyncSelect的uMsg参数指定的用户自定义窗口消息,来判断网络事件类型何时在套接字上发生;而不应盲目地进行调用。
表3.2-1 用于WSAAsyncSelect函数的网络事件类型
事件类型 含义
---------------------------------------------------------------------------------------------------------------------
FD_READ 应用程序想要接收有关是否可读的通
知,以便读入数据
FD_WRITE 应用程序想要接收有关是否可写的通知,
以便写入数据
FD_OOB 应用程序想接收是否有带外(OOB)数据
抵达的通知
FD_ACCEPT 应用程序想接收与进入连接有关的通知
FD_CONNECT 应用程序想接收与一次连接或者多点
join操作完成的通知
FD_CLOSE 应用程序想接收与套接字关闭有关的通
知
FD_QOS 应用程序想接收套接字“服务质量”(QoS)
发生更改的通知
FD_GROUP_QOS 应用程序想接收套接字组“服务质量”
发生更改的通知(现在没什么用处,为未
来套接字组的使用保留)
FD_ROUTING_INTERAFCE_CHANGE 应用程序想接收在指定的方向上,与路由
接口发生变化的通知
FD_ADDRESS_LIST_CHANGE 应用程序想接收针对套接字的协议家族,
本地地址列表发生变化的通知
应用程序在一个套接字上成功调用了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的低字部分。
下面要说明一下如何对FD_WRITE事件通知进行处理。只有在三种条件下,才会发出FD_WRITE通知:
· 使用connect或WSAConnect,一个套接字首次建立了连接。
· 使用accept或WSAAccept,套接字被接受以后。
· 若send、WSASend、sendto或WSASendTo操作失败,返回了WSAWOULDBLOCK错误,而且缓冲区的空间变得可用。
因此,作为一个应用程序,自收到首条FD_WRITE消息开始,便应认为自己必然能在一个套接字上发出数据,直至一个send、WSASend、sendto或WSASendTo返回套接字错误WSAWOULDBLOCK。经过了这样的失败以后,要再用另一条FD_WRITE通知应用程序再次发送数据。
3.3 WSAEventSelect 模型
Winsock提供了另一个有用的异步I/O模型。和WSAAsyncSelect模型类似的是,它也允许应用程序在一个或多个套接字上,接收以事件为基础的网络事件通知。对于表3.2-1总结的、由WSAAsyncSelect模型采用的网络事件来说,它们均可原封不动地移植到新模型。在用新模型开发的应用程序中,也能接收和处理所有那些事件。该模型最主要的差别在于网络事件会投递至一个事件对象句柄,而非投递至一个窗口例程。
事件通知模型要求我们的应用程序针对打算使用的每一个套接字,首先创建一个事件对象。创建方法是调用WSACreateEvent函数,它的定义如下:
WSAEVENT WSACreateEvent(Void);
WSACreateEvent函数的返回值很简单,就是一个创建好的事件对象句柄。得到事件对象句柄后,接下来必须将其与某个套接字关联在一起,同时注册自己感兴趣的网络事件类型,如表3.2-1所示。要做到这一点,方法是调用WSAEventSelect函数,对它的定义如下:
int WSAEventSelect(
Socket s,
WSAEVENT hEventObject,
long NetworkEvents
);
其中,s参数代表自己感兴趣的套接字。hEventObject参数指定要与套接字关联在一起的事件对象—用WSACreateEvent取得的那一个。而最后一个参数lNetworkEvents,则对应一个“位掩码”,用于指定应用程序感兴趣的各种网络事件类型的一个组合(如表3.2-1所示)。要想获知对这些事件类型的详细说明,请参考早先讨论过的WSAAsyncSelect I/O模型。
为WSAEventSelect创建的事件拥有两种工作状态,以及两种工作模式。其中,两种工作状态分别是“已传信”(signaled)和“未传信”(nonsignaled)。工作模式则包括“人工重设”(manual reset)和“自动重设”(auto reset)。WSACreateEvent最开始在一种未传信的工作状态中,并用一种人工重设模式,来创建事件句柄。随着网络事件触发了与一个套接字关联在一起的事件对象,工作状态便会从“未传信”转变成“已传信”。由于事件对象是在一种人工重设模式中创建的,所以在完成了一个I/O请求的处理之后,我们的应用程序需要负责将工作状态从已传信更改为未传信。要做到这一点,可调用WSAResetEvent函数,对它的定义如下:
BOOL WSAResetEvent(WSAEVENT hEvent);
该函数唯一的参数便是一个事件句柄;基于调用是成功还是失败,会分别返回TRUE或FALSE。应用程序完成了对一个事件对象的处理后,便应调用WSACloseEvent函数,释放由事件句柄使用的系统资源。对WSACloseEvent函数的定义如下:
BOOL WSACloseEvent(WSAEVENT hEvent);
该函数也要拿一个事件句柄作为自己唯一的参数,并会在成功后返回TRUE,失败后返回FALSE。
一个套接字同一个事件对象句柄关联在一起后,应用程序便可开始I/O处理;方法是等待网络事件触发事件对象句柄的工作状态。WSAWaitForMultipleEvents函数的设计宗旨便是用来等待一个或多个事件对象句柄,并在事先指定的一个或所有句柄进入“已传信”状态后,或在超过了一个规定的时间周期后,立即返回。下面是WSAWaitForMultipleEvents函数的定义:
DWORD WSAWaitForMultilpeEvents(
DWORD cEvents,
const WSAEVENT FAR * lphevents,
BOOL fWaitAll,
DWORD dwTimeout,
BOOL fAlertable
);
其中, cEvents和lphEvents参数定义了由WSAEVENT对象构成的一个数组。在这个数组中,cEvents指定的是事件对象的数量,而lphEvents对应的是一个指针,用于直接引用该数组。要注意的是,WSAWaitForMultipleEvents只能支持由WSA_MAXIMUM_WAIT_EVENTS对象规定的一个最大值——64个。因此,该I/O模型一次最多都只能支持64个套接字。
fWaitAll参数指定了WSAWaitForMultipleEvents如何等待在事件数组中的对象。若为TRUE,那么只有等lphEvents数组内包含的所有事件对象都已进入“已传信”状态,函数才会返回;若设为FALSE,任何一个事件对象进入“已传信”状态,函数就会返回。就后一种情况来说,返回值指出了到底是哪个事件对象造成了函数的返回。通常,应用程序应将该参数设为FALSE。
dwTimeout参数规定了WSAWaitForMultipleEvents最多可等待一个网络事件发生有多长时间,以毫秒为单位。超过规定的时间,函数就会立即返回,即使由fWaitAll参数规定的条件
展开阅读全文