Winsock提供了一些I/O模型帮助应用程序以异步方式在一个或者多个套接字上管理I/O。大体上,这样的I/O模型共有6中:阻塞模型,选择模型,WSAAsyncSelect模型,WSAEventSelect模型,重叠模型和完成端口模型。
本章先介绍套接字模型、选择模型、WSAAsyncSelect模型
4.1 套接字模式
套接字模式简单的决定了操作套接字时,Winsock函数是如何运转的。 Winsock以两种模式执行I/O操作:阻塞和非阻塞。 在阻塞模式下,执行I/O的Winsock调用(如send和recv)一直到操作完成菜返回。 在非阻塞模式下,Winsock函数会立即返回。
4.1.1 阻塞模式
套接字创建时,默认工作在阻塞模式下。 例如,对recv函数的调用会使程序进入等待状态,直到接收到数据才返回。
大多数Winsock程序设计者都是从阻塞套接字模式开始学习的,因为这时最容易和最直接的方式。处理阻塞模式套接字的应用程序使用的程序框架便是阻塞模式。 此模型是非常容易理解的。
阻塞套接字的好处是使用简单,但是当需要处理多个套接字连接时,就必须创建多个线程,即典型的一个连接使用一个线程的问题。这给编程带来了许多不方便。所以实际开发中使用最多的还是下面要讲述的非阻塞模式。
4.1.2 非阻塞模式
非阻塞套接字使用起来比较复杂, 但是却有许多有点。 应用程序可以调用ioctlsocket函数显示地让套接字工作在非阻塞模式下。
u_long ul = 1;
SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
ioctlsocket(s, FIONBIO, (u_long*)&ul);
一旦套接字被置于非阻塞模式, 处理发送和接收数据或者管理连接的Winsock调用将会立即返回。 大多情况下,调用失败的出错代码是WSAEWOULDBLOCK, 这意味着请求的操作在调用期间没有完成。例如,如果系统输入缓冲区中没有待处理的数据,那么对recv的调用将返回WSAEWOULDBLOCK。 通常,要对相同函数调用多次,直到它返回成功为止。
非阻塞调用经常用WSAEWOULDBLOCK出错代码失败,所以将套接字设置为非阻塞之后,关键的问题在于如何确定套接字什么时候可读/可写, 也就是说确定网络事件何时发生。如果需要字集不断调用函数去测试的话,程序的性能势必会受到影响,解决的办法就是使用Windows提供的不同的I/O模型。
4.2 选择(select)模型
select模型是一个广泛在Winsock中使用的I/O模型。 称它为select模型,是因为它主要是使用select函数管理I/O的。 这个模式的设计源于UNIX系统, 目地是允许那些想要避免在套接字调用上阻塞的应用程序有能力管理多个套接字。
4.2.1 select函数
select 函数可以确定一个或者多个套接字的状态。 如果套接字上没有网络事件发生,便进入等待状态,以便执行同步I/O。 函数定义如下
int select(
int nfds, // 忽略, 仅是为了与Berkeley套接字兼容
fd_set* readfds, // 指向一个套接字集合, 用来检查其可读性
fd_set* writefds, // 指向一个套接字集合, 用来检查其可写性
fd_set* exceptfds, // 指向一个套接字集合, 用来检查错误
const struct timeval* timeout // 指定此函数等待的最长事件, 如果是NULL, 则最长时间为无限大
);
函数调用成功,返回发生网络事件的所有套接字数量的总和。 如果超过了时间限制,返回0, 失败则返回SOCKET_ERROR。
1 套接字集合
fd_set 结构可以把多个套接字连在一起, 形成一个套接字集合。 select函数可以测试这个集合中哪些套接字有事件发生。 下面是这个结构在WINSOCK2.h中的定义。
typedef struct fd_set
{
u_int fd_count; // 下面数组的大小
SOCKET fd_array[FD_SETSIZE]; // 套接字句柄数组
}fd_set;
下面是WINSOCK定义的4个操作fd_set套接字集合的宏
FD_ZERO(*set) 初始化set为空集合。 集合在使用前应该总是清空。
FD_CLR(s,*set) 从set移除套接字s
FD_ISSET(s, *set) 检查s是不是set的成员,如果是返回TRUE
FD_SET(s, *set) 添加套接字到集合
2 网络事件
传递给select函数的3个fd_set结构中, 一个是为了检查可读性, 一个是为了检查可行性, 另一个是为了检查错误。
select函数返回之后, 如果有下列事件发生, 其对应的套接字就会被标识。
(1)readfds集合
数据可读
连接已经关闭、重启或者中断
如果listen已经被调用, 并且有一个连接,accept函数将成功
(2)writefds集合:
数据能够发送
如果一个非阻塞连接调用正在被处理,连接已经成功
(3)exceptfds集合:
如果一个非阻塞连接调用正在被处理,连接试图失败
OOB数据可读
当select返回时,它通过移除没有未决I/O操作的套接字句柄修改每个fd_set集合。 例如,想要测试套接字s是否可读时,必须将它添加到readfds集合,然后等待select函数返回。当select调用完成后再确定s是否仍然还在readfds集合中, 如果还在,就说明s可读了。 3个参数中的任意两个都可以是NULL(至少要有一个不是NULL), 任何不是NULL的集合必须至少包含一个套接字句柄。
3、设置超时
最后的参数timeout是timeval结构的指针, 它制定了select函数等待的最长事件。 如果设为NULL, select将会无限则色,直到有网络事件发生,timeval结构定义如下:
typedef struct timeval
{
long tv_sec; // 指定等待多少秒
long tv_usec; // 指定等待多少毫秒
}timeval;