select模型是一个广泛在Winsock中石油的I/O模型。称它为select 模型,是因为它主要是使用select 函数来管理I/O 的。这个模式的设计源于UNIX 系统,目的是允许那些想要避免在套接字调用上阻塞的应用程序有能力管理多个套接字。
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 结构中,一个是为了检查可读性(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 确定套接字状态的过程。
3. 设置超时
最后的参数 timeout 是timeval 结构的指针,它指定了select 函数等待的最长时间。如果设为NULL,select 将会无限阻塞,直到有网络事件发生。timeval 结构定义如下。
typedef struct timeval
{ long tv_sec; // 指示等待多少秒
long tv_usec; // 指示等待多少毫秒
} timeval;
应用
#include "..//InitSock/InitSock.h"
#include <stdio.h>
int main()
{
CInitSock initSock;
USHORT nPort = 8000; // 服务器监听的端口
// 创建监听套接字
SOCKET sListen = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.S_un.S_addr = INADDR_ANY;
addr.sin_port = htons(nPort);
// 绑定套接字到本机
if(bind(sListen, (SOCKADDR*)&addr, sizeof(SOCKADDR)) == SOCKET_ERROR)
{
int error = WSAGetLastError();
printf("bind error = %d\n", error);
system("pause");
return 0;
}
// 进入监听模式
listen(sListen, 5);
// select模型处理过程
// 1初始化一个套接字集合fdScoket,添加监听套接字句柄到该集合
fd_set fdScoket;
FD_ZERO(&fdScoket);
FD_SET(sListen, &fdScoket);
while(1)
{
//2 将fdSocket集合的一个拷贝fdRead传递给select函数
// 当有时间发生时,select函数移除fdRead集合中没有未决I/O操作的套接字句柄,然后返回
fd_set fdRead = fdScoket;
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)fdScoket.fd_count; i++ )
{
if (FD_ISSET(fdScoket.fd_array[i], &fdRead))
{
if (fdScoket.fd_array[i] == sListen) // 监听套接字收到信连接
{
if (fdScoket.fd_count < FD_SETSIZE)
{
sockaddr_in addrRemote;
int nAddrLen = sizeof(addrRemote);
SOCKET sNew = accept(sListen, (SOCKADDR*)&addrRemote, &nAddrLen);
FD_SET(sNew, &fdScoket);
printf("接受到连接:%s\n", inet_ntoa(addrRemote.sin_addr));
}
else
{
printf("Too much connections\n");
continue;
}
}
else
{
char szText[256];
int nRecv = recv(fdScoket.fd_array[i], szText, strlen(szText), 0);
if (nRecv > 0)
{
szText[nRecv] = '\0';
printf("接收到数据:%s\n", szText);
}
}
}
}
}
else
{
printf("failed select\n");
break;
}
}
system("pause");
return 0;
}