在《套接字socket及C/S通信的基本概念》和《WinSock编程基础》中,我们介绍了套接字的基本概念和WinSock API的基本调用规范。我们讨论了阻塞模式/非阻塞模式和同步I/O和异步I/O等话题。
从概念的角度,阻塞模式因其简洁易用便于快速原型化,但在应付建立连接的多个套接字或在数据的收发量不均、时间不定时却极难管理。另一方面,我们需要对非阻塞模式套接字的 WinSock API调用频繁返回的WSAEWOULDBLOCK错误加以判断处理也显得难于管理。WinSock套接字I/O模型提供了管理I/O完成通知的方法,帮助应用程序判断套接字何时可供读写。
共有6中类型的套接字I/O模型可让WinSock应用程序对I/O进行管理,它们包括blocking(阻塞)、select(选择)、WSAAsyncSelect(异步选择)、WSAEventSelect(事件选择)、overlapped(重叠)以及completionport(完成端口)。
本文讨论三种选择(都带select)模型。
1.基于套接字集合的select模型
(1)select模型概述
该模型时最初设计是在不使用UNIX操作系统的计算机上实现的,它们采用的是Berkeley套接字方案。select模型已集成到Winsock 1.1中,它使那些想避免在套接字调用过程中被无辜“锁定”的应用程序,采取一种有序的方式,同时进行对多个套接字的管理。
之所以称其为“select模型”,是由于它的“中心思想”便是利用select函数,实现对I/O的管理! 使用select模型,一般需要调用ioctlsocket函数将一个套接字从锁定模式切换为非锁定模式。
// 将套接字s设置为非阻塞模式
unsigned long nonBlocking = 1;
ioctlsocket(s, FIONBIO, (u_long*)&nonBlocking);
select模型本质上是一种分类处理思想,预先声明几个FD_SET(fd_set结构)集合(使用FD_ZERO初始化),例如ReadSet,WriteSet,然后调用宏FD_SET(s,&ReadSet)将关注FD_READ事件的套接字s添加到ReadSet集合,调用宏FD_SET(s,&WriteSet)将关注FD_WRITE事件的套接字s添加到WriteSet集合。其中宏FD_SET(SOCKET s, fd_set set)将s添加到set集合。从根本上说,fd_set数据类型代表着一系列按关注事件分类的套接字集合。
然后再调用select函数,对声明的集合ReadSet或WriteSet进行扫描,其函数原型如下:
int WSAAPI select(
int nfds,
fd_setFAR * readfds,
fd_setFAR * writefds,
fd_setFAR *exceptfds,
const struct timevalFAR * timeout );
其中,第一个参数 nfds会被忽略,一般赋值0。之所以仍然要提供这个参数,只是为了保持与早期的Berkeley套接字应用程序的兼容。其他的三个fd_set参数,一个用于检查可读性(readfds),一个用于检查可写性(writefds),另一个用于例外数据(exceptfds)。最后一个参数timeout用于决定select()等待I/O操作完成时最大忍耐时间,在等待时间内select()函数阻塞。当timeout为空时,无限等待直到有I/O完成;当*timeout=0时,select()函数立即返回,用做轮询。
例如我们只关注FD_READ事件,则select(0,&ReadSet,NULL,NULL,NULL)。WinSock要求这三个fd_set参数至少有一个不为NULL,而在其他平台下经常只关注最后一个参数用于实现相当于sleep()的延时功能。
select()函数用于判断套接字上是否存在数据(any data incoming?)或者能否向一个套接字写数据(output buffer available?)。调用select()会修改每个fd_set结构,它扫描注册到集合ReadSet和WriteSet中的套接字是否有读写事件发生,若有,则对集合进行更新,删除那些不存在待决I/O操作的套接字句柄。select()完成后,返回所有仍在fd_set集合中的套接字句柄总数。
然后,我们需要遍历查询之前注册到某个集合中的套接字是否仍为其中一部分。这需要调用FD_ISSET(SOCKET s, fd_set set)来测试套接字是否属于关注同类事件的套接字集合set。若是,则对待决的I/O进行处理(再次recv()/send()执行真正的拷贝)。
(2)select模型的应用实例
由于select模型源于Berkeley套接字方案,故常用作实现跨平台的POLL组件。在Linux下,select和poll是一个级别的,以下梳理了经典开源通信库中用到的select模型。
(1)curl/lib/select.h(c)中的Curl_socket_ready()调用。
/*
* This is an internal function used for waiting for read or write
* events on a pair of file descriptors. It uses poll() when a fine
* poll() is available, in order to avoid limits with FD_SETSIZE,
* otherwise select() is used.
*/
(2)thttpd/fdwatch.h(c)中fdwatch()中的WATCH()调用。
/* fdwatch.h - header file for fdwatch package
**
** This package abstracts the use of the select()/poll()/kqueue()