目录
2. 套接字非阻塞模式的设置方法——ioctlsocket 函数
3. 网络事件等待函数WSAWaitForMultipleEvents()
3. 网络事件等待函数WSAWaitForMultipleEvents()
一、 套接字的非阻塞工作模式
1.阻塞与非阻塞模式的概念
- 阻塞是指一个线程在调用一个函数时,该函数由于某种原因不能立即完成,导致线程处于等待状态。
- 能引起阻塞的套接字函数实际上也是I/O函数,只不过他们的操作设备是网络,因此阻塞模式下的函数也被称为网络i/o函数。
- C程序中能引起阻塞的函数调用有scanf()、getc()、gets()等I/O函数
-
在阻塞模式下,线程或进程调用套接字函数时,如果不满足执行条件,套接字函数将被阻塞,直到相应的条件满足时才执行成功并返回;比如调用recv()函数读取网络缓冲区中的数据,如果没有数据到达,将一直阻塞在recv()这个函数调用上,直到读到一些数据,此函数才返回。
-
在非阻塞模式下,执行套接字函数时,不管套接字是否满足执行条件,执行是否成功都立即返回,该函数所在的线程继续运行;比如调用recv()函数读取网络缓冲区中数据,不管是否读到数据都立即返回,而不会一直挂在此函数调用上。如果缓冲区中没有数据则返回错误:WSAEWOULDBLOCK
-
套接字除了可以工作在阻塞模式下,还可以工作在非阻塞模式下,同时提高效率增加公平性。在非阻塞模式下,执行套接字不论是否满足条件,执行是否成功都立即返回,程序继续执行。非阻塞模式的一个缺点是当一个I/O操作不能及时完成时应用程序不再阻塞,而是继续做其他的事。在有多个套接字的情况下,就可以通过循环来轮询个套接字的I/O操作,从而提高工作效率。
2.阻塞模式下能引起阻塞的套接字函数
-
accept()——监听套接字的缓冲队列中没有已达到的连接请求,则阻塞,当有连接请求到达时恢复。
- connect()——连接请求发送出去便阻塞,直到TCP/IP三次握手过程成功结束,返回对客户端连接请求的确认。
- recv、recvfrom()——套接字接收缓冲区无数据可读,便阻塞,直到有数据可读。
- send()、sendto()——如果套接字缓冲区中仍有以前的数据未发送完成,并且该发送缓冲区的空闲空间不能容纳要发送的数据,则阻塞,直到套接字发送缓冲区有足够的空间。
3.两种模式的比较
- 阻塞模式:
- 简单易用,因而应用广泛,但处理多个连接时效率不高:
- 若线程在某一套接字上阻塞,则整个线程都处于阻塞状态,因而不能及时对本线程内的其他套接字进行处理;
- 当客户端调用recv()函数接收数据时,如果对方服务器主机崩溃,但客户端TCP不知道这个情况,则客户端的recv()函数将一直阻塞。在单线程的程序里这种情况会导致主线程被阻塞,整个程序被锁死在这里。
- 非阻塞模式:
- 能克服阻塞模式的缺点更能适应实际应用环境的需求,但使用起来较为复杂。
2. 套接字非阻塞模式的设置方法——ioctlsocket 函数
int ioctlsocket(SOCKET s, long cmd, u_long *argp);
- 参数:
- s 要设置的套接字。
- cmd 用于设置套接字的命令。
- argp 指向cmd所需参数的指针
- 返回值:成功执行则返回0,否则返回 SOCKET_ERROR ,错误码可调用 WSAGetLastError获得。
设置套接字模式的命令:
FIONBIO
将套接字置于非阻塞模式
函数参数*argp 指向的值为0则将套接字设为阻塞模式,非0则设为非阻塞模式
FIONREAD
用于确定可以从套接字上读多少数据。函数参数*argp 用长整型来返回可读的字节数。
SIOCATMARK
确定带外数据是否可读。
SIO_RCVALL
接收网络上所有的IP包,套接字必须绑定到一个明确的本地接口上。
示例代码:
unsigned long ul = 1; //设置套接字选项
int nRet; //返回值
s = socket(AF_INET, SOCK_STREAM, 0); //创建套接字
nRet = ioctlsocket(s, FIONBIO, (unsigned long*)&ul); //设置套接字非阻塞模式
if (nRet == SOCKET_ERROR)
{
//设置套接字非阻塞模式,失败处理
}
3. 非阻塞模式下的编程方法
将套接字设置为非阻塞模式后,在调用套接字函数时,不管调用是否成功执行,函数都会立即返回。
大多数情况下,这些函数调用都会以调用“失败”告终。这些“失败”主要是因为所需“资源”未准备好、所请求的操作在函数调用期间不能完成造成的。
例如,套接字还没收到任何数据时,其接受缓冲区内没有任何数据可读,这时调用recv()函数将会因为“收”不到数据而调用失败。
这种“失败”在程序运行时是正常的,因此程序对这种“失败”的处理应不同于由其它原因引起的“失败”。
为了与其它类型的失败相区分,应调用WSAGetLastError()函数获取其错误代码,通过其错误代码与其它类型错误区分,其错误代码为WSAEWOULDBLOCK。
下面的代码是在非阻塞套接字s上接收数据的一个例子,msgbuffer为事先定义的接收数据缓冲区。
while(true)
{
if(recv(s, msgbuffer,sizeof(msgbuffer),0)<0)
{
err=WSAGetLastError();
if(err!=WSAEWOULDBLOCK)
{ //若失败原因非WSAEWOULDBLOCK则不再尝试接收
cout<<"发生错误,接收信息失败!错误码:"<<err<<endl;
break; //退出循环,不再尝试接收信息
}
}
else
{
//接收成功后对收到数据进行处理
……
break; //处理完后退出循环
}
}
上面的代码中,如果数据接收失败所对应的错误代码是WSAEWOULDBLOCK,程序将会继续循环,直到接收成功或出现其他错误。
由此可以看出,如果程序中只有一个套接字,使用非阻塞模式并不比阻塞模式好,而且需要编写更多的代码。
但是,当程序中有多个套接字时,在非阻塞模式下可以在一个循环内依次对多个套接字进行处理,并不会因为某个套接字的阻塞而过多延误其他套接字的处理。
4. 非阻塞模式服务器端程序和客户端程序
服务器端程序与客户程序的TCP连接建立后首先向客户端发送一条内容为“Connect succeed. Please send a message to me.”,然后等待接收客户端发送来的一条消息,收到后显示该信息并关闭连接。
要求服务器端套接字使用非阻塞模式,允许同时有多个客户接入。客户端程序在与服务器的连接建立成功后接收并显示从服务器收到的信息,然后从键盘接收一行信息发送给服务器。
本例是例4.1的非阻塞版,由于例4.1的服务器端使用的是阻塞模式的套接字,因此每次只能处理一个客户连接,只有跟一个客户的数据发送和数据接收的交互过程完成后,才能接收下一个客户请求。
本例的服务器端使用非阻塞模式的套接字,因此,在一个连接完成后,客户还未发信息时,仍可以返回执行accep()函数接收下一个连接请求,从而可以做到同时与多个客户保持连接。
由于服务器同时要与多个客户保持连接,因此需要保存多个已连接套接字描述符,为便于操作,需要定义一个SOCKET类型的数组来保存它们。该数组按如下方式定义:
#define N 10
……
SOCKET newsock[N+1];
为便于操作,数组中下标为0的元素在程序中不用,因此要保证数组能同时容纳N个已连接套接字描述符,数组大小需定义为N+1。
约定:
①数组中只保存尚未关闭的套接字描述符,已关闭的套接字描述符应从数组中删除;
②数组中所有的已连接套接字描述符始终保持在从下标1开始的元素中连续存放;
③每当accept()函数成功返回一个新的已连接套接字的描述符时,该套接字都保存在数组中其它套接字描述符之后。
为满足约定,首先需要定义一个变量n,该变量用于保存数组中已存入的已连接套接字个数,新的已连接套接字应保存在下标为n+1的数组元素中;其次,为了保证所有未关闭的已连接套接字在数组中连续存放,每关闭一个已连接套接字,在数组中其描述符后的每个套接字描述符都应前移。
数组中的第i个套接字被关闭时,可使用如下代码将其后套接字描述符前移:
closesocket( newsock[i] ); //关闭第i个套接字
for(int k=i;k<n;k++)
newsock[k]=newsock[k+1];
n--; //数组中套接字总数减1
非阻塞模式避免了程序在某个套接字操作上的阻塞等待,可以在等待的这段时间内轮询其他套接字或处理别的事务,从而避免了阻塞模式下的效率低下问题。但是,在整个过程中,应用程序需要不断地调用套接字函数进行尝试,直到相关操作完成,这对CPU来说仍是很大的浪费。
同时,对某个套接字而言,对它进行一次不成功的尝试后,程序就要去轮询其他套接字或处理其他事务,到再一次轮询到本套接字可能会花费较长时间,因此对一些实时性要求很高的应用来说,这种编程模型并不适合。
二、select模型
1. 套接字集合fd_set
Select模型继承自 BSD UNIX的Berkeley Sockets,该模型是因为使用select()函数来管理I/O而得名。
程序通过调用select()函数可以获取一组指定套接字的状态,这样可以保证及时捕捉到最先得到满足的网络I/O事件,从而可保证对各套接字I/O操作的及时性。
这里I/O事件是指监听套接字上有用户请求到达、非监听套接字接收到数据、套接字已准备好可以发送数据等事件。
select函数使用套接字集合fd_set来管理多个套接字,套接字集合fd_set是一个结构体,其定义如下:
typedef struct fd_set
{
unsigned int fd_count;
SOCKET fd_array[FD_SETSIZE];
} fd_set;
–fd_count用来保存集合中套接字的数目,套接字数组fd_array用于存储集合中各个套接字的描述符。
–FD_SETSIZE是一个常量,在WinSock2.h中定义,其值为64。
套接字集合相关的宏
为了方便编程,winsock提供了四个宏来对套接字集合进行操作。
FD_ZERO(*set):初始化set为空集合。套接字集在使用前总是应该清空。
FD_CLR(s,*set):从set移除套接字s
FD_ISSET(s,*set):检查s是不是set的成员,如果是返回TRUE
FD_SET(s,*set):添加套接字到集合
2. select函数
int select(
int nfds, //忽略,仅为了与berkaley套接字兼容
fd_set * readfds, //一个套接字集合,用于检查可读性
fd_set * writefds, //一个套接字集合,用于检查可写性
fd_set * exceptfds, //一个套接字集合,用于检查错误
const struct timeval * timeout //指定此函数等待的最长时间,若为NULL,则最长时间为无限大
)
返回值:负值表示select错误;正值表示某些套接字可读写或出错;0表示timeout指定的时间内没有可读写或出错误的套接字 。
select()对三个套接字集合的操作:
select()函数的三个参数指向的三个套接字集合分别用于保存要检查可读性(readfds)、可写性(writefds)和是否出错(exceptfds)的套接字。
当select()返回时,它将移除这三个套接字集合中没有发生相应I/O事件的套接字。
对于readfds,主要有以下事件:
•非监听套接字接收缓冲区有数据可读
•连接关闭、重启或中断
•监听套接字有连接请求到达,accept函数将成功
对于writefds,主要有以下事件:
•数据能够发送
•一个非阻塞连接调用正在被处理,连接已经成功
对于exceptfds,主要有以下事件:
•一个非阻塞连接调用正在被处理,连接企图失败
•OOB数据可读
需要注意:调用select函数时,如果没有需要对其可读性、可写性或者是否出错进行监听的套接字,则相应的参数应置为空(NULL),即不指向任何套接字集合,但是,三个参数不能同时为空,而且不空的指针指向的套接字集合中至少有一个套接字。
select()函数在被调用执行时将会阻塞,阻塞的最长时间由参数timeout设定。
在等待过程中一旦三个集合中至少一个套接字满足了可读或者可写或者出错的条件,函数将立刻返回。
到了设定时间,如果三个集合中的所有套接字仍然都不能满足可读、可写或者出错的条件,函数也将返回。
参数 timeout指向一个结构体(struct timeval)变量,该结构体定义如下:
typedef struct timeval
{
long tv_sec; //指示等待多少秒
long tv_usec; //指示等待多少微秒
} timeval;
如果timeout设为NULL,select()将会无限阻塞,直至有网络事件发生。如果将timeout指向的个结构设置为(0,0),select ()函数会马上返回。
3. 使用Select模型编程的方法
根据select()函数的工作过程,不难得出使用Select模型编写程序的基本步骤:
①用FD_ZERO宏来初始化需要的fd_set;
②用FD_SET宏来将套接字句柄分配给相应的fd_set,例如,如果要检查一个套接字是否有需要接收的数据,则可用FD_SET宏把该套接字的描述符加入可读性检查套接字集合中(第二个参数指向的套接字集合);
③调用select()函数,该函数将会阻塞直到满足返回条件,返回时,各集合中无网络I/O事件发生的套接字将被删除。
例如,对可读性检查集合readfds中的套接字,如果select()函数返回时接受缓冲区中没有数据需要接收,select()函数则会把套接字从集合中删除掉;
④用FD_ISSET对套接字句柄进行检查,如果被检查的套接字仍然在开始分配的那个fd_set里,则说明马上可以对该套接字进行相应的IO操 作。
例如,一个分配给可读性检查套接字集合readfds的套接字,在select()函数返回后仍然在该集合中,则说明该套接字已有数据已经到来, 马上调用recv函数可以读取成功。
事实上,实际的应用程序通常不会只有一次网络I/O,因此不会只有一次select()函数调用,而应该是上述过程的一个循环。
编写一个服务器端程序和客户端程序。
服务器端程序与客户程序的TCP连接建立后首先向客户端发送一条内容为“Connect succeed. Please send a message to me.”,然后等待接收客户端发送来的一条消息,收到后显示该信息并关闭连接。
要求服务器端套接字使用Select模型,允许同时有多个客户接入。
客户端程序在与服务器的连接建立成功后接收并显示从服务器收到的信息,然后从键盘接收一行信息发送给服务器。
参考前面非阻塞模式练习,只是将SOCKET数组改为使用套接字集合fd_set。
使用套接字集合及对套接字集合操作的四个宏可大大减少代码的编写量,同时也使得程序简洁易懂。
由于程序中的所有套接字均置于select()函数的管理中,套接字使用阻塞模式或是非阻塞模式对性能影响不大,为编程简单起见,本例题采用阻塞模式。
三、WSAAsyncSelect模型
1. WSAAsyncSelect模型
阻塞模型是在不知I/O事件是否发生的情况下,应用程序会按自己既定的流程主动去执行IO操作,结果通常是阻塞并等待相应事件发生;
非阻塞模型也是在不知I/O事件是否发生的情况下,应用程序按自己既定的流程,反复执行IO操作直到操作成功(I/O事件发生);
Select模型则是在不知I/O事件是否发生的情况下,应用程序按既定流程调用select函数主动检查关心的IO事件是否发生,如果没有发生则select()函数也是阻塞等待。
共同特点:不管 I/O事件是否发生,应用程序都会按既定流程主动试着进行I/O操作,而且直至操作成功才会罢休,因此这三种套接字模型都属于同步模型。
尽管非阻塞模型和Select模型一次能够尝试对多个套接字进行I/O操作,要比阻塞模型效率高很多,但应用程序一旦开始I/O操作,则I/O操作完成之前都是无法进行其它操作。
解决这一问题的方法是采用异步I/O模型。
异步套接字I/O模型中,当网络I/O事件发生时,系统将采用某种机制通知应用程序,应用程序只有在收到事件通知时才调用相应的套接字函数进行I/O操作。
WSAAsyncSelect模型和WSAEventSelect模型都属于异步I/O模型,二者的差别在于系统通知应用程序的方法不同。
2. WSAAsyncSelect()函数
WSAAsyncSelect模型是基于Windows的消息机制实现的,当网络事件发生时,Windows系统将发送一条消息给应用程序,应用程序将根据消息做出相应的处理。该模型的核心是WSAAsyncSelect()函数。
WSAAsyncSelect()函数的主要功能是为指定的套接字注册一个或多个应用程序需要关注的网络事件。
注册网络事件时需要指定事件发生时需要发送的消息以及处理该消息的窗口的句柄。
程序运行时,一旦被注册的事件发生,系统将向指定的窗口发送指定的消息。
int WSAAsyncSelect(
SOCKET s, //需要事件通知的套接字
HWND hWnd,//当网络事件发生时接收消息的窗口句柄
unsigned int wMsg, //当网络事件发生时向窗口发送的用户自定义消息
long lEvent //要注册的应用程序感兴趣的套接字s的网络事件集合
);
函数返回值:应用程序感兴趣的事件注册成功,则返回0;如果注册失败,则返回SOCKET_ERROR。
常用的网络事件包括FD_READ网络事件、FD_WRITE事件、FD_ACCEPT事件、FD_CONNECT事件、FD_CLOSE事件等。
FD_READ事件:有数据到达但还没有发送FD_READ网络事件;调用recv()或者recvfrom()函数后,如果仍然有可读数据。
FD_WRITE:调用connect()或者accept()函数后,连接已经建立;调用send()或者sendto()函数返回WSAEWOULDBLOCKE错误后,再次调用send()或者sendto()函数可能成功。
FD_ACCEPT事件:当前有连接请求需要接受,即有当连接请求到达但还没有发送FD_ACCEPT事件;调用accept()函数后,如果还有另外连接请求需要接受时。
FD_CONNECT事件:调用connect()函数后,建立连接完成
FD_CLOSE事件仅对面向连接套接字有效,在下面情况下发送FD_CLOSE事件:对方执行了套接字关闭并且没有数据可读,如果数据已经到达并等待读取,FD_CLOSE事件不会被发送,直到所有的数据都被接收。
WSAAsyncSelect模型是非阻塞的,在应用程序中调用WSAAsyncSelect()函数后,该函数将向系统注册完成参数lEvent指定的网络事件后立即返回。
WSAAsyncSelet模型是异步的,当已被注册的网络事件发生时,系统将向应用程序发送消息,该消息将由参数hWnd指定的窗口的相应消息处理函数进行处理,编写相应的消息处理函数是程序编写的主要工作之一。
•在VC++2017不鼓励使用 WSAAsyncSelect()模型,若使用编译器将发出错误警告并停止编译,如果要关闭错误警告继续编译,需要在头文件stdAfx.h中添加如下宏定义:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
•或者在调用该函数的CPP文件头部使用以下预处理命令:
#pragma warning(disable : 4996)
应用程序需要注册哪些网络事件,取决于实际的需求。如果应用程序同时对多个网络事件感兴趣。需要对网络事件类型执行按位或(|)运算。然后将它们赋值给lEvent参数。
SAAsyncSelect(s, hWnd,WM_SOCKET,FD_CONNECT|FD_READ|FD_CLOSE);
当套接字s连接完成、有数据可读或者套接字关闭的网络事件事件发生时,就会有WM_SOCKET消息发送给窗口句柄为hWnd窗口。
- 如果要取消某个套接字的所有已注册的网络事件,需要以参数IEvent值为0来调用WSAAsyncSelect()函数,其格式如下。
- WSAAsyncSelect(s, hWnd, 0, 0);
- –s为要被取消注册网络事件的套接字,hWnd为注册这些事件时指定的接收网络事件消息的窗口的句柄。
- 取消网络事件的注册之后,系统将不再为该套接字发送任何与网络事件相关的消息。
需要特别强调,WSAAsyncSelect模型应用在Windows环境下,使用该模型时必须创建窗口。而Slelect模型广泛应用在Unix系统和Windows系统,使用该模型不需要创建窗口。
应用程序调用WSAAsyncSelect()函数后,自动将套接字设置为非阻塞模式,而应用程序中调用select()函数后,并不能改变该套接字的工作方式。
如果已对一个套接口进行了WSAAsynSelect() 操作,则任何用ioctlsocket()来把套接口重新设置成阻塞模式的试图将以WSAEINVAL失败。为了把套接口重新设置成阻塞模式,应用程序必须首先用WSAAsynSelect()调用(IEvent参数置为0)来禁止WSAAsynSelect()。
在同一个套接字上,多次调用WSAAsyncSelect()函数注册不同的网络事件,后一次函数调用将取消前一次注册的网络事件。
例如,下面的两行代码:
WSAAsyncSelect(s, hWnd, wMsg, FD_READ);
WSAAsyncSelect(s, hWnd,wMsg, FD_WRITE);
第一行调用WSAAsyncSelect()函数为套接字s注册FD_READ网络事件,然后又再次调用WSAAsyncSelect()函数为同一个套接字s注册FD_WRITE 网络事件,那么此后应用程序将只能接收到套接字s的FD_WRITE网络事件。
在同一个套接字上多次调用WSAAsyncSelect()函数为不同的网络事件注册不同的消息,后一次的函数调用也将取消前面注册的网络事件。例如,下面的代码中,第二次函数调用将会取消第一次函数调用的作用。只有FD_WRITE网络事件能过wMsg2消息通知到窗口,而FD_READ事件则无法触发wMsg1消息。
WSAAsyncSelect(s,hWnd,wMsg1,FD_READ);
WSAAsyncSelect(s,hWnd,wMsg2,FD_WRITE);
如果为一个监听套接字注册了FD_ACCEPT、FD_READ和FD_WRITE网络事件,则在该监听套接字上调用accept()函数接受连接请求所创建的任何套接字,也会触发FD_ACCEPT、FD_READ和FD_WRITE网络事件,即相当于为这写套接字也注册了同样的网络事件。
这是因为调用accept()函数接受的套接字和监听套接字具有同样的属性。所以,任何为监听套接字设置的网络事件对接受的套接字同样起作用。若需要不同的消息和网络事件,应用程序应该调用WSAAsyncSelect()函数,为套接字注册不同的网络事件和消息。
在程序中为一个FD_READ网络事件一般不要多次调用recv()函数来接受数据,如果应用程序为一个FD_READ网络事件调用了多个recv()函数,可能会使得该应用程序接收到多个FD_READ网络事件。
例如,假设一开始套接字接收到了300字节的数据,这时系统将向应用程序发送FD_READ事件通知,如果应用程序的相应消息处理函数中连续调用三次recv()函数,每次都只接收100字节数据,前两次的recv()调用都将导致系统发送FD_READ网络事件通知,第三次调用recv()时将会把剩余数据接收完,因而不会发送FD_READ。前两次发送的FD_READ都会引发系统再次调用该消息处理函数。
当套接字上注册的网络事件发生时,系统将向指定的窗口发送指定的用户自定义消息,进而触发对该消息的消息处理函数的调用。
编写程序时,消息处理函数可使用MFC的类向导添加,函数的具体功能则由编程者实现。函数的名字在添加该函数时是由编程者指定。
应用程序不必在收到FD_READ消息时读进所有可读的数据。每接收到一次FD_READ网络事件,应用程序调用一次recv()函数是恰当的。
为了便于编程者获取网络事件或网络事件的错误信息,WinSock提供了WSAGETSELECTEVENT和WSAGETSELECTERROR两个宏,这两个宏的使用格式如下所示。
WORD wEvent,wError;
wEvent=WSAGETSELECTEVENT(lParam);
wError=WSAGETSELECTERROR(lParam);
特别说明:
•所有从Cwnd类派生出来的类都有成员变量m_hWnd,该变量即为指向当前窗口的句柄。因此,当注册消息的处理窗口为本窗口时,参数hWnd对应的实参可直接用m_hWnd。
if(WSAAsyncSelect(m_newSock,m_ hWnd, MsgRecv,FD_READ)!=0)
{
CString str3("套接字异步事件注册失败!");
MessageBox(str3);
closesocket(m_acceptSocket);
closesocket(m_ListenSocket);
m_SendButton.EnableWindow(0);
}
3. 使用WSAAsyncSelect模型接收数据的过程
调用recv()函数接收数据前,首先调用WSAAsyncselect()函数注册网络事件、事件发生时发出的用户自定义消息及处理消息的窗口。
当系统收到数据时,系统将向应用程序发送消息。
应用程序接收到这个消息后,将在消息对应的消息处理函数中调用recv()函数接收数据并处理数据。
4. WSAAsyncSelect模型的编程方法
WSAAsyncSelect模型是基于Windows消息机制的,而其WSAAsyncSelect()函数要求消息的接收对象必须是一个窗口,因此基于WSAAsyncSelect模型的应用程序一般都是图形界面的窗口应用程序。
程序的编写可以分为两大部分:建立并完善应用程序框架、编写消息处理函数。
第一步建立并完善应用程序框架需要完成如下任务:
(1)使用应用程序向导创建对话框应用程序框架”
(2)设计程序界面,主要是绘制控件并设置相关属性等;
(3)为相关控件添加控件变量;
(4)将通信所必须的套接字变量作为成员变量添加到窗口类中;
(5)添加WSAAsyncSelect()函数在为套接字注册网络事件时发送的自定义消息;
(6)在窗口类的成员函数OnInitDialog()中添加程序代码,完成创建套接字、给套接字绑定地址、使套接字处于监听状态、调用WSAAsyncSelect()函数为套接字注册网络事件等功能。
第二步编写消息处理函数是程序设计的主要工作,除了编写相关控件消息的处理函数外,最主要的就是为套接字编写与网络事件关联的自定义消息的处理函数,在这些处理函数中要调用相关的套接字函数完成相关的IO处理。
网络事件消息的处理函数具有类似下面代码所示的原型。
afx_msg LRESULT OnSocketMsg(WPARAM wParam, LPARAM lParam)
wParam参数存放发生网络事件的套接字的句柄,lParam参数的低16位存放的是发生的网络事件,高16位则用于存放网络事件发生错误时的错误码。
函数的参数个数和类型是由系统规定的,它们的值在因消息到达而触发函数运行时由系统传入。
四、WSAEventSelect模型
1. WinSock中的网络事件与事件对象函数
WSAEventSelect模型与WSAAsyncSelect一样都属于异步I/O模型,二者的不同之处在于网络事件发生时系统通知应用程序的形式不同。
WSAAsyncSelect模型是基于Windows的消息机制的,网络事件发生时系统将以消息的形式通知应用程序,并且消息必须与窗口句柄相关联,因此程序必须要有窗口对象才行。
WSAEventSelect模型是以事件对象为基础的,网络事件需要与事件对象关联,当网络事件发生时,经由事件对象句柄通知应用程序。
WinSock中的网络事件与事件对象函数
WSAEventSelect模型是以事件对象为基础的,网络事件需要与事件对象关联,当网络事件发生时,经由事件对象句柄通知应用程序。
事件对象的概念与5.4节所介绍的事件对象完全相同,只不过WinSock又对操作事件对象Windows API函数进行了扩展,下面介绍几个主要的WinSock扩展后的事件对象操作函数。
WSACreateEvent()函数
•功能是创建一个“人工重设模式”工作的事件对象,初始为“无信号”状态。
•函数原型
WSAEVENT WSACreateEvent( void );
–该函数无参数。函数执行成功则返回事件对象句柄,否则返回WSA_INVALID_EVENT。其中WSAEVENT是事件对象句柄类型。
•该函数是CreateEvent()函数的扩展,但它创建的是人工重设模式的事件对象,而CreateEvent()函数创建的则是自动重设模式。如果程序需要一个自动重设模式的事件对象,可直接使用CreateEvent()函数。
WSAResetEvent()函数
•该函数将事件对象从“有信号”状态更改为“无信号”状态。函数原型如下:
BOOL WSAResetEvent(WSAEVENT hEvent);
–参数hEvent是要设置的事件对象的句柄。
–如果该函数调用成功,则函数返回TRUE;反之函数返回FALSE。
WSASetEvent()函数
•该函数将事件对象设置为“有信号”状态,函数原型:
BOOL WSASetEvent(WSAEVENT hEvent);
•函数参数hEvent是要设置的事件对象的句柄。
•如果该函数调用成功,则函数返回TRUE;反之函数返回FALSE。
WSACloseEvent() 函数
•释放事件对象占有的系统资源。该函数声明如下:
BOOL WSACloseEvent(WSAEVENT hEvent);
–函数参数hEvent是要释放的事件对象的句柄。
–如果函数调用成功,该函数返回TRUE;否则返回FALSE。
2. WSAEventSelect模型的函数
WSAEventSelect()函数是WSAEventSelect模型的核心,该函数能够为套接字注册感兴趣的网络事件,将网络事件与事件对象关联起来。
该模型的网络事件与WSAAsyncSelect模型完全相同。当为套接字注册的网络事件发生时,关联的事件对象将从“无信号”状态转变为“有信号”状态。
网络事件注册函数WSAEventSelect()
int WSAEventSelect(
SOCKET s, //套接字。
WSAEVENT hEventObject, //事件对象句柄
Long lNetworkEvents //应用程序感兴趣的网络事件集合
);
•为套接字注册网络事件成功,函数返回0;注册失败则返回SOCKETS_ERROR。
例:为监听套接字sockserver注册一个事件对象,使得当网络事件AD_ACCEPT发生(即有连接请求到达)时,该事件对象的工作状态变为“有信号”状态。
用如下代码:
WSAEVENT hEvent= WSACreateEvent();
WSAEventSelect(sockserver, hEvent, FD_ACCEPT);
需要注意
(1)如果应用程序同时对多个网络事件感兴趣,需要对网络事件类型执行按位OR(|)运算。
例:应用程序对套接字s上的网络事件FD_READ和FD_CLOSE感兴趣,则可使用如下代码注册网络事件:
SOCKET s;
WSAEVENT hEvent;
int nReVal=WSAEventSelect(s, hEvent, FD_READ|FD_CLOSE);
(2)要取消为套接字注册的网络事件,必须再次调用WSAEventSelect()函数,并将InetworkEvents参数设置为0,例:要取消上面例子中为套接字s注册的网络事件,则只需使用如下一行代码:
WSAEventSelect( s, hEvent, 0);
(3)应用程序调用WSAEventSelect()函数后,套接字将被自动设置为非阻塞模式。如果要将套接字设置为阻塞模式,必须先取消套接字上注册的网络事件,然后再调用ioctlsocket()函数将套接字设置为阻塞模式。如果不取消已注册的网络事件而直接调用ioctlsocket()函数来设置套接字为阻塞模式,将会失败并返回WSAEINAL错误。
3. 网络事件等待函数WSAWaitForMultipleEvents()
该函数的功能是等待与套接字关联的事件对象由“无信号”状态变为“有信号”状态。
应用程序在调用WSAEventSelect()函数为套接字注册网络事件后调用该函数等待事件发生,事件发生前该函数将阻塞等待,直到等待的事件发生或设置的等待时间超时该函数才会返回。
•函数原型
DWORD WSAWaitForMultipleEvents(
DWORD cEvents,
const WSAEVENT FAR * lphEvents,
BOOL fWaitAll,
DWORD dwTimeout,
BOOL fAlertable
);
函数参数
- cEvents:等待的事件对象句柄的数量。等待事件对象句柄数量至少为1,最多数量为WSA_MAXIMUM_WAIT_EVENTS,其值为64个;
- lphEvents:指向事件对象句柄的指针,实际上是一个WSAEVENT类型的数组,参数cEvents则是数组中事件对象句柄的数量;
- fWaitAll:该参数为TRUE,则该函数在lphEvents中所有的事件对象都转变为“有信号”状态时才返回,若为FALSE,则在其中一个事件句柄转变为“有信号”状态时就返回,并且返回值指示出促使函数返回的事件对象;
- dwTimeout:函数阻塞等待的时间,单位为毫秒。超过该等待时间,即使没有满足fWaitAll参数指定的条件函数也会返回。如果该参数为0,则函数检查事件对象的状态并立即返回。如果该参数为WSA_INFINITE,则该函数会无限期等待下去,直到满足fWaitAll参数指定的条件。
- fAlertable:该参数主要用于重叠I/O模型,在完成例程的处理过程使用。如果该参数为TRUE,说明该函数返回时完成例程已经被执行。如果该参数为FALSE,说明该函数返回时完成例程还没有执行。这里只要将该参数设置为FALSE就可以了。
返回值代表使该函数返回的事件对象,分4种情况:
- 一个从WSA_WAIT_EVENT_0到WAS_WAIT_EVENT_0+cEvents-1范围的值,其中宏WAS_WAIT_EVENT_0的值为0,这时如果fWaitAll为TRUE,则表示所有事件对象都处于“有信号”状态,如果fWaitAll为FALSE,则返回值减去WAS_WAIT_EVENT_0,即为“有信号”事件对象在lphEvents数组中的序号;
- WAIT_IO_COMPLETION,表示一个或者多个完成例程已经排队等执行;
- WSA_WAIT_TIMEOUT,表示函数调用超时,并且所有事件对象都没有处于“有信号”状态。
- 如果该函数调用失败,则返回值为WSA_WAIT_FAILED。
网络事件枚举函数WSAEnumNetworkEvents()
•用于获取套接字上的发生的网络事件,同时清除系统内部的网络事件记录,如果需要,还可以重置事件对象。
•函数原型
int WSAEnumNetworkEvents(SOCKET,WSAEVENT hEventObject,
LPWSANETWORKEVENTS lpNetworkEvents);
•函数参数
–s:发生网络事件的套接字句柄。
–hEventObject:被重置的事件对象句柄(可选)
–lpNetworkEvents:指向WSANETWORKEVENTS结构指针。在该结构中包含发生网络事件的记录和相关错误代码。
WSANETWORKEVENTS结构声明如下:
typedef struct _WSANETWORKEVENTS {
long lNetworkEvents;
int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;
lNetworkEvents:发生的网络事件。事件对象进入“有信号”状态时,在套接字上可能会发生多个网络事件,因此本参数记录的可能是多个网络事件。
iErrorCode:包含网络事件错误代码的数组。错误代码与lnetworkEvents字段中的网络事件对应。在应用程序中,使用网络事件错误标识符对iErrorCode数组进行索引,检查是否发生了网络错误。这些标识符命名规则是在对应的网络事件后面添加“_BIT”。例如,对应于FD_READ网络事件的网络事件错误标识符为FD_READ_BIT,网络错误标识符代表是iErrorCode数组序号。
该函数调用成功时返回值为0,反之为SOCKETS_ERROR。如果该函数返回SOCKET_ERROR错误,则事件对象不会被重置,网络事件也不会被清除。
在调用WSAEnumNetworkEvents()函数时,如果参数hEventObjec不为NULL,则该参数指定的事件对象被置为“无信号”状态。通常参数hEventObjec被指定为与s指定的套接字相关联的事件对象,如果该参数设为NULL,则应用程序必须要调用WSAResetEvent()函数来将关联事件对象设置为“无信号”状态。
4. WSAEventSelect模型的编程方法
WSAEventSelect模型编程的基本步骤是:
①创建一个事件对象数组,用于存放所有的事件对象;
②为相关的套接字创建事件对象;
③调用WSAEventSelect()函数将事件对象和需要关的注套接字的网络事件关联起来;
④调用WSAWaitForMultipleEvents()函数等待网络事件发生。当有网络事件发生时关联的事件对象将变为“有信号”状态,此时在事件对象上等待的WSAWaitForMultipleEvents()函数将会立即返回;
⑤调用WSAEnumNetworkEvents函数,查询发生事件的事件对象获取具体发生的网络事件类型;
⑥根据网络事件类型调用相应的套接字函数进行处理。
五、WSAAsyncSelect模型
1. WSAAsyncSelect模型
阻塞模型是在不知I/O事件是否发生的情况下,应用程序会按自己既定的流程主动去执行IO操作,结果通常是阻塞并等待相应事件发生;
非阻塞模型也是在不知I/O事件是否发生的情况下,应用程序按自己既定的流程,反复执行IO操作直到操作成功(I/O事件发生);
Select模型则是在不知I/O事件是否发生的情况下,应用程序按既定流程调用select函数主动检查关心的IO事件是否发生,如果没有发生则select()函数也是阻塞等待。
共同特点:不管 I/O事件是否发生,应用程序都会按既定流程主动试着进行I/O操作,而且直至操作成功才会罢休,因此这三种套接字模型都属于同步模型。
尽管非阻塞模型和Select模型一次能够尝试对多个套接字进行I/O操作,要比阻塞模型效率高很多,但应用程序一旦开始I/O操作,则I/O操作完成之前都是无法进行其它操作。
解决这一问题的方法是采用异步I/O模型。
异步套接字I/O模型中,当网络I/O事件发生时,系统将采用某种机制通知应用程序,应用程序只有在收到事件通知时才调用相应的套接字函数进行I/O操作。
WSAAsyncSelect模型和WSAEventSelect模型都属于异步I/O模型,二者的差别在于系统通知应用程序的方法不同。
2. WSAAsyncSelect函数
WSAAsyncSelect()函数
WSAAsyncSelect模型是基于Windows的消息机制实现的,当网络事件发生时,Windows系统将发送一条消息给应用程序,应用程序将根据消息做出相应的处理。该模型的核心是WSAAsyncSelect()函数。
WSAAsyncSelect()函数的主要功能是为指定的套接字注册一个或多个应用程序需要关注的网络事件。
注册网络事件时需要指定事件发生时需要发送的消息以及处理该消息的窗口的句柄。
程序运行时,一旦被注册的事件发生,系统将向指定的窗口发送指定的消息。
int WSAAsyncSelect(
SOCKET s, //需要事件通知的套接字
HWND hWnd,//当网络事件发生时接收消息的窗口句柄
unsigned int wMsg, //当网络事件发生时向窗口发送的用户自定义消息
long lEvent //要注册的应用程序感兴趣的套接字s的网络事件集合
);
函数返回值:应用程序感兴趣的事件注册成功,则返回0;如果注册失败,则返回SOCKET_ERROR。
常用的网络事件包括FD_READ网络事件、FD_WRITE事件、FD_ACCEPT事件、FD_CONNECT事件、FD_CLOSE事件等。
FD_READ事件:读数据就绪的通知事件。事件触发时调用recv(), recvfrom(), WSARecv(), WSARecvfrom()。
FD_WRITE事件:写数据就绪的通知事件。事件触发时调用send(), sendto(), WSASend(), WSASendto()。
FD_ACCEPT事件:当前有连接请求需要接受。事件触发时调用accept(), WSAAccept()。
FD_CONNECT事件:调用connect()函数后,建立连接完成。
FD_CLOSE事件:仅对面向连接套接字有效,收到套接字关闭时触发在。
WSAAsyncSelect模型是非阻塞的,在应用程序中调用WSAAsyncSelect()函数后,该函数将向系统注册完成参数lEvent指定的网络事件后立即返回。
WSAAsyncSelet模型是异步的,当已被注册的网络事件发生时,系统将向应用程序发送消息,该消息将由参数hWnd指定的窗口的相应消息处理函数进行处理,编写相应的消息处理函数是程序编写的主要工作之一。
•在VC++2017不鼓励使用 WSAAsyncSelect()模型,若使用编译器将发出错误警告并停止编译,如果要关闭错误警告继续编译,需要在头文件stdAfx.h中添加如下宏定义:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
•或者在调用该函数的CPP文件头部使用以下预处理命令:
#pragma warning(disable : 4996)
3. 使用WSAAsyncSelect模型接收数据的过程
使用WSAAsyncSelect模型接收数据的过程
调用recv()函数接收数据前,首先调用WSAAsyncselect()函数注册网络事件、事件发生时发出的用户自定义消息及处理消息的窗口。
当系统收到数据时,系统将向应用程序发送消息。
应用程序接收到这个消息后,将在消息对应的消息处理函数中调用recv()函数接收数据并处理数据。
应用程序需要注册哪些网络事件,取决于实际的需求。如果应用程序同时对多个网络事件感兴趣。需要对网络事件类型执行按位或(|)运算。然后将它们赋值给lEvent参数。
WSAAsyncSelect(s, hWnd,WM_SOCKET,FD_CONNECT|FD_READ|FD_CLOSE);
当套接字s连接完成、有数据可读或者套接字关闭的网络事件事件发生时,就会有WM_SOCKET消息发送给窗口句柄为hWnd窗口。
•如果要取消某个套接字的所有已注册的网络事件,需要以参数IEvent值为0来调用WSAAsyncSelect()函数,其格式如下。
WSAAsyncSelect(s, hWnd, 0, 0);
s为要被取消注册网络事件的套接字,hWnd为注册这些事件时指定的接收网络事件消息的窗口的句柄。
取消网络事件的注册之后,系统将不再为该套接字发送任何与网络事件相关的消息。
需要特别强调,WSAAsyncSelect模型应用在Windows环境下,使用该模型时必须创建窗口。
而Slelect模型广泛应用在Unix系统和Windows系统,使用该模型不需要创建窗口。
应用程序调用WSAAsyncSelect()函数后,自动将套接字设置为非阻塞模式,而应用程序中调用select()函数后,并不能改变该套接字的工作方式。
如果已对一个套接口进行了WSAAsynSelect() 操作,则任何用ioctlsocket()来把套接口重新设置成阻塞模式的试图将以WSAEINVAL失败。为了把套接口重新设置成阻塞模式,应用程序必须首先用WSAAsynSelect()调用(IEvent参数置为0)来禁止WSAAsynSelect()。
在同一个套接字上,多次调用WSAAsyncSelect()函数注册不同的网络事件,后一次函数调用将取消前一次注册的网络事件。
例如,下面的两行代码:
WSAAsyncSelect(s, hWnd, wMsg, FD_READ);
WSAAsyncSelect(s, hWnd, wMsg, FD_WRITE);
第一行调用WSAAsyncSelect()函数为套接字s注册FD_READ网络事件,然后又再次调用WSAAsyncSelect()函数为同一个套接字s注册FD_WRITE 网络事件,那么此后应用程序将只能接收到套接字s的FD_WRITE网络事件。
在同一个套接字上多次调用WSAAsyncSelect()函数为不同的网络事件注册不同的消息,后一次的函数调用也将取消前面注册的网络事件。例如,下面的代码中,第二次函数调用将会取消第一次函数调用的作用。只有FD_WRITE网络事件能过wMsg2消息通知到窗口,而FD_READ事件则无法触发wMsg1消息。
WSAAsyncSelect(s,hWnd,wMsg1,FD_READ);
WSAAsyncSelect(s,hWnd,wMsg2,FD_WRITE);
如果为一个监听套接字注册了FD_ACCEPT、FD_READ和FD_WRITE网络事件,则在该监听套接字上调用accept()函数接受连接请求所创建的任何套接字,也会触发FD_ACCEPT、FD_READ和FD_WRITE网络事件,即相当于为这些套接字也注册了同样的网络事件。
这是因为调用accept()函数接受的套接字和监听套接字具有同样的属性。所以,任何为监听套接字设置的网络事件对接受的套接字同样起作用。若需要不同的消息和网络事件,应用程序应该调用WSAAsyncSelect()函数,为套接字注册不同的网络事件和消息。
在程序中为一个FD_READ网络事件一般不要多次调用recv()函数来接受数据,如果应用程序为一个FD_READ网络事件调用了多个recv()函数,可能会使得该应用程序接收到多个FD_READ网络事件。
例如,假设一开始套接字接收到了300字节的数据,这时系统将向应用程序发送FD_READ事件通知,如果应用程序的相应消息处理函数中连续调用三次recv()函数,每次都只接收100字节数据,前两次的recv()调用都将导致系统发送FD_READ网络事件通知,第三次调用recv()时将会把剩余数据接收完,因而不会发送FD_READ。前两次发送的FD_READ都会引发系统再次调用该消息处理函数。
当套接字上注册的网络事件发生时,系统将向指定的窗口发送指定的用户自定义消息,进而触发对该消息的消息处理函数的调用。
编写程序时,消息处理函数可使用MFC的类向导添加,函数的具体功能则由编程者实现。函数的名字在添加该函数时是由编程者指定。
应用程序不必在收到FD_READ消息时读进所有可读的数据。每接收到一次FD_READ网络事件,应用程序调用一次recv()函数是恰当的。
为了便于编程者获取网络事件或网络事件的错误信息,WinSock提供了WSAGETSELECTEVENT和WSAGETSELECTERROR两个宏,这两个宏的使用格式如下所示。
WORD wEvent,wError;
wEvent=WSAGETSELECTEVENT(lParam);
wError=WSAGETSELECTERROR(lParam);
特别说明:
•所有从Cwnd类派生出来的类都有成员变量m_hWnd,该变量即为指向当前窗口的句柄。因此,当注册消息的处理窗口为本窗口时,参数hWnd对应的实参可直接用m_hWnd。
if(WSAAsyncSelect(m_newSock,m_ hWnd, MsgRecv,FD_READ)!=0)
{
CString str3("套接字异步事件注册失败!");
MessageBox(str3);
closesocket(m_acceptSocket);
closesocket(m_ListenSocket);
m_SendButton.EnableWindow(0);
}
4. WSAAsyncSelect模型的编程方法
WSAAsyncSelect模型的编程方法
WSAAsyncSelect模型是基于Windows消息机制的,而其WSAAsyncSelect()函数要求消息的接收对象必须是一个窗口,因此基于WSAAsyncSelect模型的应用程序一般都是图形界面的窗口应用程序。
程序的编写可以分为两大部分:建立并完善应用程序框架、编写消息处理函数。
第一步建立并完善应用程序框架需要完成如下任务:
(1)使用应用程序向导创建对话框应用程序框架;
(2)设计程序界面,主要是绘制控件并设置相关属性等;
(3)为相关控件添加控件变量;
(4)将通信所必须的套接字变量作为成员变量添加到窗口类中;
(5)添加WSAAsyncSelect()函数在为套接字注册网络事件时发送的自定义消息;
(6)在窗口类的成员函数OnInitDialog()中添加程序代码,完成创建套接字、给套接字绑定地址、使套接字处于监听状态、调用WSAAsyncSelect()函数为套接字注册网络事件等功能。
第二步编写消息处理函数是程序设计的主要工作,除了编写相关控件消息的处理函数外,最主要的就是为套接字编写与网络事件关联的自定义消息的处理函数,在这些处理函数中要调用相关的套接字函数完成相关的IO处理。
网络事件消息的处理函数具有类似下面代码所示的原型。
afx_msg LRESULT OnSocketMsg(WPARAM wParam, LPARAM lParam)
wParam参数存放发生网络事件的套接字的句柄,lParam参数的低16位存放的是发生的网络事件,高16位则用于存放网络事件发生错误时的错误码。
函数的参数个数和类型是由系统规定的,它们的值在因消息到达而触发函数运行时由系统传入。
六、基于事件的选择模型
1. WinSock中的网络事件与事件对象函数
WinSock中的网络事件与事件对象函数
WSAEventSelect模型与WSAAsyncSelect一样都属于异步I/O模型,二者的不同之处在于网络事件发生时系统通知应用程序的形式不同。
WSAAsyncSelect模型是基于Windows的消息机制的,网络事件发生时系统将以消息的形式通知应用程序,并且消息必须与窗口句柄相关联,因此程序必须要有窗口对象才行。
WSAEventSelect模型是以事件对象为基础的,网络事件需要与事件对象关联,当网络事件发生时,经由事件对象句柄通知应用程序。
WinSock中的网络事件与事件对象函数
WSAEventSelect模型是以事件对象为基础的,网络事件需要与事件对象关联,当网络事件发生时,经由事件对象句柄通知应用程序。
事件对象的概念与5.4节所介绍的事件对象完全相同,只不过WinSock又对操作事件对象Windows API函数进行了扩展,下面介绍几个主要的WinSock扩展后的事件对象操作函数。
WSACreateEvent()函数
•功能是创建一个“人工重设模式”工作的事件对象,初始为“无信号”状态。
•函数原型
WSAEVENT WSACreateEvent( void );
–该函数无参数。函数执行成功则返回事件对象句柄,否则返回WSA_INVALID_EVENT。其中WSAEVENT是事件对象句柄类型。
•该函数是CreateEvent()函数的扩展,但它创建的是人工重设模式的事件对象,而CreateEvent()函数创建的则是自动重设模式。如果程序需要一个自动重设模式的事件对象,可直接使用CreateEvent()函数。
WSAResetEvent()函数
•该函数将事件对象从“有信号”状态更改为“无信号”状态。函数原型如下:
BOOL WSAResetEvent(WSAEVENT hEvent);
–参数hEvent是要设置的事件对象的句柄。
–如果该函数调用成功,则函数返回TRUE;反之函数返回FALSE。
WSASetEvent()函数
•该函数将事件对象设置为“有信号”状态,函数原型:
BOOL WSASetEvent(WSAEVENT hEvent);
•函数参数hEvent是要设置的事件对象的句柄。
•如果该函数调用成功,则函数返回TRUE;反之函数返回FALSE。
WSACloseEvent() 函数
•释放事件对象占有的系统资源。该函数声明如下:
BOOL WSACloseEvent(WSAEVENT hEvent);
–函数参数hEvent是要释放的事件对象的句柄。
–如果函数调用成功,该函数返回TRUE;否则返回FALSE。
2. WSAEventSelect模型的函数
WSAEventSelect模型的函数
WSAEventSelect()函数是WSAEventSelect模型的核心,该函数能够为套接字注册感兴趣的网络事件,将网络事件与事件对象关联起来。
该模型的网络事件与WSAAsyncSelect模型完全相同。当为套接字注册的网络事件发生时,关联的事件对象将从“无信号”状态转变为“有信号”状态。
网络事件注册函数WSAEventSelect()
int WSAEventSelect(
SOCKET s, //套接字。
WSAEVENT hEventObject, //事件对象句柄
Long lNetworkEvents //应用程序感兴趣的网络事件集合
);
•为套接字注册网络事件成功,函数返回0;注册失败则返回SOCKETS_ERROR。
例:为监听套接字sockserver注册一个事件对象,使得当网络事件FD_ACCEPT发生(即有连接请求到达)时,该事件对象的工作状态变为“有信号”状态。
用如下代码:
WSAEVENT hEvent= WSACreateEvent();
WSAEventSelect(sockserver, hEvent, FD_ACCEPT);
需要注意
(1)如果应用程序同时对多个网络事件感兴趣,需要对网络事件类型执行按位OR(|)运算。
例:应用程序对套接字s上的网络事件FD_READ和FD_CLOSE感兴趣,则可使用如下代码注册网络事件:
SOCKET s;
WSAEVENT hEvent;
int nReVal=WSAEventSelect(s, hEvent, FD_READ|FD_CLOSE);
(2)要取消为套接字注册的网络事件,必须再次调用WSAEventSelect()函数,并将InetworkEvents参数设置为0,例:要取消上面例子中为套接字s注册的网络事件,则只需使用如下一行代码:
WSAEventSelect( s, hEvent, 0);
(3)应用程序调用WSAEventSelect()函数后,套接字将被自动设置为非阻塞模式。如果要将套接字设置为阻塞模式,必须先取消套接字上注册的网络事件,然后再调用ioctlsocket()函数将套接字设置为阻塞模式。如果不取消已注册的网络事件而直接调用ioctlsocket()函数来设置套接字为阻塞模式,将会失败并返回WSAEINVAL错误。
3. 网络事件等待函数WSAWaitForMultipleEvents()
网络事件等待函数WSAWaitForMultipleEvents()
该函数的功能是等待与套接字关联的事件对象由“无信号”状态变为“有信号”状态。
应用程序在调用WSAEventSelect()函数为套接字注册网络事件后调用该函数等待事件发生,事件发生前该函数将阻塞等待,直到等待的事件发生或设置的等待时间超时该函数才会返回。
•函数原型
DWORD WSAWaitForMultipleEvents(
DWORD cEvents,
const WSAEVENT FAR * lphEvents,
BOOL fWaitAll,
DWORD dwTimeout,
BOOL fAlertable
);
函数参数
cEvents:等待的事件对象句柄的数量。等待事件对象句柄数量至少为1,最多数量为WSA_MAXIMUM_WAIT_EVENTS,其值为64个;
lphEvents:指向事件对象句柄的指针,实际上是一个WSAEVENT类型的数组,参数cEvents则是数组中事件对象句柄的数量;
fWaitAll:该参数为TRUE,则该函数在lphEvents中所有的事件对象都转变为“有信号”状态时才返回,若为FALSE,则在其中一个事件句柄转变为“有信号”状态时就返回,并且返回值指示出促使函数返回的事件对象;
dwTimeout:函数阻塞等待的时间,单位为毫秒。超过该等待时间,即使没有满足fWaitAll参数指定的条件函数也会返回。如果该参数为0,则函数检查事件对象的状态并立即返回。如果该参数为WSA_INFINITE,则该函数会无限期等待下去,直到满足fWaitAll参数指定的条件。
fAlertable:该参数主要用于重叠I/O模型,在完成例程的处理过程使用。如果该参数为TRUE,说明该函数返回时完成例程已经被执行。如果该参数为FALSE,说明该函数返回时完成例程还没有执行。这里只要将该参数设置为FALSE就可以了。
返回值代表使该函数返回的事件对象,分4种情况:
(1)一个从WSA_WAIT_EVENT_0到WAS_WAIT_EVENT_0+cEvents-1范围的值,其中宏WAS_WAIT_EVENT_0的值为0,这时如果fWaitAll为TRUE,则表示所有事件对象都处于“有信号”状态,如果fWaitAll为FALSE,则返回值减去WAS_WAIT_EVENT_0,即为“有信号”事件对象在lphEvents数组中的序号;
(2)WAIT_IO_COMPLETION,表示一个或者多个完成例程已经排队等待执行;
(3)WSA_WAIT_TIMEOUT,表示函数调用超时,并且所有事件对象都没有处于“有信号”状态。
(4)如果该函数调用失败,则返回值为WSA_WAIT_FAILED。
网络事件枚举函数WSAEnumNetworkEvents()
•用于获取套接字上发生的网络事件,同时清除系统内部的网络事件记录,如果需要,还可以重置事件对象。
•函数原型
int WSAEnumNetworkEvents
(SOCKET s,
WSAEVENT hEventObject,
LPWSANETWORKEVENTS lpNetworkEvents);
•函数参数
–s:发生网络事件的套接字句柄。
–hEventObject:被重置的事件对象句柄(可选)
–lpNetworkEvents:指向WSANETWORKEVENTS结构指针。在该结构中包含发生网络事件的记录和相关错误代码。
WSANETWORKEVENTS结构声明如下:
typedef struct _WSANETWORKEVENTS {
long lNetworkEvents;
int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;
lNetworkEvents:发生的网络事件。事件对象进入“有信号”状态时,在套接字上可能会发生多个网络事件,因此本参数记录的可能是多个网络事件。
iErrorCode:包含网络事件错误代码的数组。错误代码与lnetworkEvents字段中的网络事件对应。在应用程序中,使用网络事件错误标识符对iErrorCode数组进行索引,检查是否发生了网络错误。这些标识符命名规则是在对应的网络事件后面添加“_BIT”。例如,对应于FD_READ网络事件的网络事件错误标识符为FD_READ_BIT,网络错误标识符代表是iErrorCode数组序号。
该函数调用成功时返回值为0,反之为SOCKETS_ERROR。如果该函数返回SOCKET_ERROR错误,则事件对象不会被重置,网络事件也不会被清除。
在调用WSAEnumNetworkEvents()函数时,如果参数hEventObjec不为NULL,则该参数指定的事件对象被置为“无信号”状态。通常参数hEventObjec被指定为与s指定的套接字相关联的事件对象,如果该参数设为NULL,则应用程序必须要调用WSAResetEvent()函数来将关联事件对象设置为“无信号”状态。
4. WSAEventSelect模型的编程方法
WSAEventSelect模型的编程方法
WSAEventSelect模型编程的基本步骤是:
①创建一个事件对象数组,用于存放所有的事件对象;
②为相关的套接字创建事件对象;
③调用WSAEventSelect()函数将事件对象和需要关注套接字的网络事件关联起来;
④调用WSAWaitForMultipleEvents()函数等待网络事件发生。当有网络事件发生时关联的事件对象将变为“有信号”状态,此时在事件对象上等待的WSAWaitForMultipleEvents()函数将会立即返回;
⑤调用WSAEnumNetworkEvents函数,查询发生事件的事件对象获取具体发生的网络事件类型;
⑥根据网络事件类型调用相应的套接字函数进行处理。
编写一个符合自定义标准的服务器端程序。要求服务器端使用WSAEventSelect模型,允许同时有多个客户接入,并能够持久与服务器通信。
在本例中,为保存多个套接字描述符和与每个套接字关联的事件对象句柄,需要使用一个套接字描述符数组和一个事件对象句柄数组,每次调用accept()成功得到一个已连接套接字,为它创建事件对象并注册网络事件后,都需要将套接字描述符及事件对象句柄添加到相应数组中,同时需要一个变量iEventTotal来记录数组中已存入元素的个数。为了便于后面统一处理,监听套接字的描述符及其关联事件对象的句柄也应分别存入两个数组中。
•在调用WSAWaitForMultipleEvents()时,尽管fWaitALL参数指定为FALSE,但返回时它所等待的事件对象中仍可能有多个已检测到网络事件发生,这时返回的是所有已触发的事件对象中的在对象数组中下标值最小的,为了确保对所有的事件对象进行处理,需要对下标大于等于返回值nIndex的事件对象进行逐一检查,检查注册的网络事件是否发生,并对发生事件做出处理。
•这里判断一个事件对象是否有事件发生仍使用WSAWaitForMultipleEvents()函数,只不过其cEvents参数应指定为1,lphEvents参数为要查看的事件对象句柄变量的地址,而且也不能阻塞太长时间,所以参数dwTimeOUT需指定为有限值。例如,检查事件对象hEvent上是否有事件发生可用如下代码:
WSAWaitForMultipleEvents(1, &hEvent, TRUE, 1000, FALSE);
•本例中并没有为套接字注册FD_WRITE事件,原因是在本例中即便注册了也没有机会捕捉到该事件。FD_WRITE事件只有在以下情况下才会触发:
①在建立连接成功时,客户服务器两端都会触发FD_WRITE事件;
②send(WSASend)/sendto(WSASendTo)发送失败返回WSAEWOULDBLOCK,并且当缓冲区有可用空间时,则会触发FD_WRITE事件。
第①种情况在本例中虽能频繁出现,但accept()返回的同时才获取套接字描述符,用于监视套接字的事件对象还没有创建,因此在这里用事件对象无法捕捉。在第②种情况中,由于只有套接字的发送缓冲区满的时候调用send()才会返回 WSAEWOULDBLOCK错误,因此 FD_WRITE 触发的前提是缓冲区要先被充满然后随着数据的发送又出现可用空间,而不是只要缓冲区中有可用空间。由于本例中只发送少量数据,套接字发送缓冲区不可能占满。
从这里也可以知道,程序中需要发送数据时,应直接调用send()发送,而不应使用FD_WRITE触发。FD_WRITE一般用于发送大量数据时,调用send()函数发送数据失败返回WSAEWOULDBLOCK后,触发数据的再次发送。
七、重叠I/O
1. 重叠I/O的概念
重叠I/O的概念
重叠I/O模型是以Windows重叠I/O机制为基础的套接字I/O模型。Windows重叠I/O本来是一种文件操作技术。在传统文件操作中,文件的读写函数都是以阻塞模式工作的,当文件很大或磁盘读写速度较低时,程序运行就会长时间阻塞在文件的读写操作上,直到读写完成才返回。这样将浪费很多时间,导致程序性能下降。为了解决这个问题,Windows引进了重叠I/O的概念。能同时以多个线程处理多个I/O。
在重叠I/O下,应用程序在调用文件读写函数后函数会立即返回,而不必等待操作结束,文件读写的同时应用程序可以执行其他操作,这就是所谓的异步I/O操作。如果让应用程序连续进行多个文件读写函数的调用,使得系统同时执行多个文件的读写操作,就成为所谓的重叠I/O操作。
WinSock的重叠I/O模型就是以重叠I/O机制为基础开发的。从WinSock2开始,重叠I/O模型便被引入到WinSock的扩展套接字函数中,这些扩展函数的格式不再与BSD套接字函数兼容,函数名均以WSA开头,比如recv()函数和send()函数的Windows扩展版分别为WSARecv()和WSASend()。应用程序要使用重叠I/O模型,就必须使用WinSock扩展套接字函数。
重叠数据结构WSAOVERLAPPED
typedef struct _WSAOVERLAPPED{
ULONG_PTR Interal; //底层操作系统使用
ULONG_PTR InteralHigh; //底层操作系统使用
union{
struct{
DWORD offset; //套接字是忽略,文件操作使用。
DWORD offsetHigh; //忽略
};
PVOID Pointer; //忽略
};
HANDLE hEvent;//允许应用程序为这个操作关联一个事件对象句柄
}WSAOVERLAPPED, *LPWSAOVERLAPPED;
重叠I/O的事件通知方法需要Windows事件对象关联到WSAOVERLAPPED结构。当I/O完成时,会将WSAOVERLAPPED结构中的hEvents置为有信号状态。
通过调用WSAWaitForMultipleEvents()来等待I/O完成的通知,在得到通知信号后,就可以调用WSAGetOverlappedResult()来查询I/O操作的结果,并进行相关处理。WSAOVERLAPPED结构在重叠I/O请求初始化及其后续的完成之间提供一个沟通或通信机制。
2. 重叠IO的相关函数
重叠IO的相关函数
套接字创建
- 阻塞模式:socket
- 非阻塞:wsasocket
SOCKET WSASocket(
int af, int type, int protocol,
LPWSAPROTOCOL_INFO lpProtocolInfo,//指定新套接字的特性
GROUP g, //保留
DWORD dwFlags//在重叠I/O模型中,需要设置为WSA_FLAG_OVERLAPPED。
);
区别:最后一个参数dwFlags必须设置为WSA_FLAG_OVERLAPPED,其余的架构都相同。
发送数据函数
- 阻塞模式:send
- 非阻塞:wsasend
int WSASend(
SOCKET s,
LPWSABUF lpBuffers,//指向WSABUF结构数组的指针
DWORD dwBufferCount,//记录lpBuffers数组中WSABUF结构的数目
LPDWORD lpNumberOfBytesSent, //返回指向发送的字节数的指针
DWORD dwFlags, //标志
LPWSAOVERLAPPED lpOverlapped, //指向重叠结构的指针
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
//指向发送操作完成后调用的完成例程
);
第一个参数与send内的一致。
第二个参数为buffs,不是send参数Buff,非阻塞模式下可以发送缓冲区组
第三个参数和第二个参数结合起来,说明有几个buff,发送几个buff
第四个参数接收返回发送的字节数。但是返回到该处的效率较低。
第五个参数指定发送的重叠结构
最后一个参数指向完成例程的入口地址
返回值:不再为send返回的发送字节数,非阻塞下的功能是如果重叠操作立即完成,则返回0;如果重叠操作被成功初始化,并且稍后完成,则返回WSA_IO_PENDING。
两种获取传输数据数量的方法:
1)如果指定了完成例程,通过cbTransferred参数获取
2)通过WSAGetOverlappedResult()的参数lpcbTransferred获取。
非连接模式下发送:
int WSASendTo(
SOCKET s,
LPWSABUF lpBuffers,//指向WSABUF结构数组的指针
DWORD dwBufferCount,//记录lpBuffers数组中WSABUF结构的数目
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
const struct sockaddr* lpTo,//指向目的地址
int iToLen,//指向目的地址长度
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
//指向发送操作完成后调用的完成例程
);
多了参数liTo以及iToLen,发送的地址及长度,告诉发送给了谁。
数据接收函数
- 阻塞模式:recv
- 非阻塞:wsarecv
int WSARecv(
SOCKET s,
LPWSABUF lpBuffers,//指向WSABUF结构数组的指针,该结构体内有Len1,就没有把len作为参数来写
DWORD dwBufferCount, //记录lpBuffers数组中WSABUF结构的数目
LPDWORD lpNumberOfBytesRecvd,//返回收到的字节数
LPDWORD lpFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
//指向发送操作完成后调用的完成例程,完成例程的入口地址
);
返回值与wsasend一致。
非连接模式下
int WSARecvFrom(
SOCKET s,
LPWSABUF lpBuffers,//指向WSABUF结构数组的指针
DWORD dwBufferCount,//记录lpBuffers数组中WSABUF结构的数目
LPDWORD lpNumberOfBytesRecvd,
DWORD dwFlags,
struct sockaddr* lpFrom,//指向源地址
LPINT lpFromlen,//指向源地址长度
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine//指向发送操作完成后调用的完成例程
);
重叠操作结果获取函数
BOOL WSAAPI WSAGetOverlappedResult(
SOCKET s,
LPWSAOVERLAPPED lpOverlapped, //指向重叠结构
LPWORD lpcbTransfer, //本次重叠操作实际接收(发送)的字节数
BOOL fWait, //指定函数是否等待挂起的重叠操作结束。若为true则函数操作成功后返回,false则挂起。
LPWORD lpdwFlags //存放完成状态的附加标志位
);
函数成功,意味着重叠操作已经完成,返回true。
3. 重叠I/O模型的编程框架1
重叠I/O模型的编程框架
WinSock可以使用事件通知和完成例程两种方式来实现重叠I/O的操作。
1.使用事件通知方式进行重叠I/O的编程框架
(与基于事件的选择模型,通知方式一致,只是基于时间选择模型没有用到重叠I/O的机制。
- 套接字初始化,设置为重叠I/O模式;
- 创建套接字网络事件对应的用户事件对象;
- 初始化重叠结构,为套接字管理事件对象;
- 异步接收数据,无论能否接收到数据,都会直接返回;
- 调用WSAWaitForMultipleEvents(),在所有事件对象上等待,只要有一个事件对象变为授信状态,则返回;
- 调用WSAGetOverlappedResult(),获取套接字上的重叠操作状态,并保存到重叠结构中;
- 根据重置事件的状态进行处理;
- 重置已授信的事件对象、重叠结构、标志位和缓冲区;
- 回到步骤4。
4. 使用完成例程进行重叠io编程
使用完成例程方式进行重叠I/O的编程框架
对于网络重叠I/O操作,等待I/O操作结束的另一种方法是使用完成例程。异步的发送和接收接口函数的参数中的最后一个参数lpCompletionROUTINE就是用来指向完成例程的指针。若指定此参数,hEvent参数将被忽略,上下文信息将传送给完成例程函数。
完成例程函数原型:
void CALLBACK CompletionROUTINE(
DWORD dwError, //指定lpOverlapped参数中表示的重叠操作的完成状态
DWORD cbTransferred, //传送完成的数据数量
LPWSAOVERLAPPED lpOverlapped, //指定重叠结构
DWORD dwFlags //指定操作结束时的标记,通常设置为0
);
以面向连接的数据接收为例
- 套接字初始化,设置为重叠I/O模式;
- 初始化重叠结构;
- 异步传输数据,将重叠结构作为输入参数,并指定一个完成例程对应于数据传输后的处理;
- 调用WSAWaitForMultipleEvents()或SleepEx(),将自己的线程设置为一种可警告等待状态,等待一个重叠I/O请求完成,重叠请求完成后,完成例程会自动执行,在完成例程内,可随一个完成例程一起投递另一个重叠I/O操作;
- 回到步骤3。
重叠I/O模型评价
- 应用程序中的I/O操作<-->重叠结构<-->事件
- 使应用程序能达到更佳的系统性能
- 减少了一次从I/O缓冲区到应用程序缓冲区的拷贝。