Winsock网络编程套接字I/O模型之选择模型
第一章 I/O模型简介与选择模型
Winsock 提供了一些 I/O 模型帮助应用程序以异步方式在一个或者多个套接字上管理 I/O。大体上,这样的 I/O 模型共有 6 种:
- 阻塞(blocking)模型
- 选择(select)模型
- WSAAsyncSelect模型
- WSAEventSelect 模型
- 重叠(overlapped)模型
- 完成端口(completion port)模型
一、引言
Winsock 以两种模式执行 I/O 操作:阻塞和非阻塞。在阻塞模式下,执行 I/O 的 Winsock 调用(如 send 和 recv)一直到操作完成才返回。在非阻塞模式下,Winsock 函数会立即返回。
1. 阻塞模式
套接字创建时,默认工作在阻塞模式下。例如,对 recv 函数的调用会使程序进入等待状态,直到接收到数据才返回。大多数 Winsock 程序设计者都是从阻塞套接字模式开始学习的,因为这是最容易和最直接的方式。处理阻塞模式套接字的应用程序使用的程序框架便是阻塞模型。它的好处是使用简单,但是当需要处理多个套接字连接时,就必须创建多个线程,即典型的一个连接使用一个线程的问题,这给编程带来了许多不便。
2. 非阻塞模式
非阻塞套接字使用起来比较复杂,但是却有许多优点。应用程序可以调用 ioctlsocket 函数显式地让套接字工作在非阻塞模式下,如下代码所示。
u_long ul = 1;
SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
ioctlsocket(s, FIONBIO, (u_long *)&ul);
3. 为何要使用I/O模型
一旦套接字被置于非阻塞模式,处理发送和接收数据或者管理连接的 Winsock 调用将会立即返回。大多少情况下,调用失败的出错代码是 WSAEWOULDBLOCK,这意味着请求的操作在调用期间没有完成。例如,如果系统输入缓冲区中没有待处理的数据,那么对 recv 的调用将返回 WSAEWOULDBLOCK。通常,要对相同函数调用多次直到它返回成功为止。非阻塞调用经常以 WSAEWOULDBLOCK 出错代码失败,所以将套接字设置为非阻塞之后,关键的问题在于如何确定套接字什么时候可读/可写,也就是说确定网络事件何时发生。如果需要自己不断调用函数去测试的话,程序的性能势必会受到影响(线程膨胀),解决的办法就是使用Windows 提供的不同的 I/O 模型。
二、选择模型
1. select函数
int select(
__in int nfds, // 忽略,仅是为了与 Berkeley 套接字兼容
__inout fd_set* readfds, // 指向一个套接字集合,用来检查其可读性
__inout fd_set* writefds, // 指向一个套接字集合,用来检查其可写性
__inout fd_set* exceptfds, // 指向一个套接字集合,用来检查错误
__in const struct timeval* timeout // 指定此函数等待的最长时间,如果为 NULL,则最长时间为无限大
);
select 函数可以确定一个或者多个套接字的状态。如果套接字上没有网络事件发生,便进入等待状态,以便执行同步 I/O。函数调用成功,返回发生网络事件的所有套接字数量的总和。如果超过了时间限制,返回 0,失败则返回 SOCKET_ERROR(更多信息可自行查看MSDN Library)。
2. 套接字集合fd_set
typedef struct fd_set
{
u_int fd_count; // 下面这个数组fd_array的大小
SOCKET fd_array[FD_SETSIZE]; // 套接字句柄数组
} fd_set;
- fd_set 结构可以把多个套接字连在一起,形成一个套接字集合。select 函数可以测试这个集合中哪些套接字有事件发生。
- FD_SETSIZE确定了一个集合中套接字的最大数目,默认值是64,可以自行修改,但不要超过1024。FD_SETSIZE 值太大,服务器性能就会受到影响。例如有 1000 个套接字,那么在调用 select 之前就不得不设置这 1000 个套接字,select 返回之后,又必须检查这 1000 个套接字。
- 在头文件Winsock2.h中定义了4个宏,用于操作和检查套接字集合。fd_set 结构中的套接字句柄并不像Berkeley Unix那样表示为位标志。它们的数据表示是不透明的,使用这些宏可以保持不同套接字环境之间的软件可移植性。用于操作和检查fd_set内容的4个宏如下:
FD_CLR(s, *set) // 将指定套接字s从集合中移除
FD_ISSET(s, *set) // 如果s是集合的成员,则为非零,否则为零
FD_SET(s, *set) // 添加s到集合中
FD_ZERO(*set) // 将集合初始化为空集
3. 网络事件
传递给 select 函数的 3 个 fd_set 结构中,一个是为了检查可读性(readfds),一个是为了检查可写性(writefds),另一个是为了检查错误(exceptfds)。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 的集合必须至少包含一个套接字句柄。下图示例了使用 select 确定套接字状态的过程。
4. timeval 设置超时
select函数最后的参数 timeout 是 timeval 结构的指针,它指定了 select 函数等待的最长时间。如果设为 NULL,select 将会无限阻塞,直到有网络事件发生。timeval 结构定义如下。
typedef struct timeval {
long tv_sec; // 指示等待多少秒
long tv_usec; // 指示等待多少毫秒
} timeval;
5. 完整代码实现
采用 select 模型之后,即便是在单个线程中,也可以管理多个套接字。流程如下:
(1)初始化套接字集合 fdSocket,向这个集合添加监听套接字句柄。
(2)将 fdSocket 集合的拷贝fdRead传递给 select 函数,当有事件发生时,select 函数移除fdRead集合中没有未决 I/O 操作的套接字句柄,然后返回。
(3)比较原来 fdSocket 集合与 select 处理过的 fdRead 集合,确定哪些套接字有未决 I/O, 并进一步处理这些 I/O。
(4)回到第(2)步继续进行选择处理。
#include "../common/initsock.h"
#include <stdio.h>
CInitSock theSock; // 初始化Winsock库
int main()
{
USHORT nPort = 2356; // 此服务器监听的端口号
// 创建监听套节字
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(nPort);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
// 绑定套节字到本地机器
if(::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf(" Failed bind() \n");
return -1;
}
// 进入监听模式
::listen(sListen, 5);
// select模型处理过程
// 1)初始化一个套节字集合fdSocket,添加监听套节字句柄到这个集合
fd_set fdSocket; // 所有可用套节字集合
FD_ZERO(&fdSocket);
FD_SET(sListen, &fdSocket);
while(TRUE)
{
// 2)将fdSocket集合的一个拷贝fdRead传递给select函数,
// 当有事件发生时,select函数移除fdRead集合中没有未决I/O操作的套节字句柄,然后返回。
fd_set fdRead = fdSocket;
int nRet = ::select(0, &fdRead, NULL, NULL, NULL);
if(nRet > 0)
{
// 3)通过将原来fdSocket集合与select处理过的fdRead集合比较,
// 确定都有哪些套节字有未决I/O,并进一步处理这些I/O。
for(int i=0; i<(int)fdSocket.fd_count; i++)
{
if(FD_ISSET(fdSocket.fd_array[i], &fdRead))
{
if(fdSocket.fd_array[i] == sListen) // (1)监听套节字接收到新连接
{
if(fdSocket.fd_count < FD_SETSIZE)
{
sockaddr_in addrRemote;
int nAddrLen = sizeof(addrRemote);
SOCKET sNew = ::accept(sListen, (SOCKADDR*)&addrRemote, &nAddrLen);
FD_SET(sNew, &fdSocket);
printf("接收到连接(%s)\n", ::inet_ntoa(addrRemote.sin_addr));
}
else
{
printf(" Too much connections! \n");
continue;
}
}
else
{
char szText[256];
int nRecv = ::recv(fdSocket.fd_array[i], szText, strlen(szText), 0);
if(nRecv > 0) // (2)可读
{
szText[nRecv] = '\0';
printf("接收到数据:%s \n", szText);
}
else // (3)连接关闭、重启或者中断
{
::closesocket(fdSocket.fd_array[i]);
FD_CLR(fdSocket.fd_array[i], &fdSocket);
}
}
}
}
}
else
{
printf(" Failed select() \n");
break;
}
}
return 0;
}
参考文献
《Windows 网络与通信程序设计》
MSDN Library