Winsock网络编程笔记:select()函数详解,select例子实现非阻塞TCPServer
Select I/O模型优缺点
优点:能从单个线程的多个套接字上进行多重连接,避免多线程的资源消耗。
缺点:fd_set结构中的最大套接字数量通常为64。
套接字集合:
fd_set (defined in winsock2.h)
fd_set结构可以把多个套接字集合在一起,形成一个套接字集合。select函数可以测试这个集合中哪些套接字有事件发生。
typedef struct fd_set
{
u_int fd_count; // how many are SET
SOCKET fd_array[FD_SETSIZE]; // an array of SOCKETs
} fd_set;
该结构内置函数:
nfd_count() //Numbers of sockets in the set.
nfd_array[i] //Array of sockets that are in the set.
FD_ZERO(*set) ----初始化set为空集合。
FD_CLR (s, *set) ----从set移除套接字s。
FD_ISSET (s, *set) ----检查s是否是set的成员,返回值是ture或false。
FD_SET (s, *set) ----添加套接字s到集合set。
select函数:
1. 用途
在编程的过程中,经常会遇到许多阻塞的函数,好像read和网络编程时使用的recv, recvfrom函数都是阻塞的函数,当函数不能成功执行的时候,程序就会一直阻塞在这里,无法执行下面的代码。这时就需要用到非阻塞的编程方式,使用select函数就可以实现非阻塞编程。
select函数是一个轮循函数,循环询问文件节点,可设置超时时间,超时时间到了就跳过代码继续往下执行。
2. 大致原理
select需要驱动程序的支持,驱动程序实现fops内的poll函数。select通过每个设备文件对应的poll函数提供的信息判断当前是否有资源可用(如可读或写),如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执行。
3. 函数定义
该函数声明如下
int select(int nfds, fd_set* readset, fd_set* writeset, fe_set* exceptset, struct timeval* timeout);
nfds:需要检查的文件描述字个数,在Windows中这个参数值无所谓,可以设置不正确。
readset:readfds是指向fd_set结构的指针,用来检查可读性的一组文件描述字,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
writeset :writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。
exceptset : 用来检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内)
timeout : 超时,填NULL为阻塞,填0为非阻塞,其他为一段超时时间。
第一:若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
第二:若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
第三:timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
返回值:
返回fd的总数,错误时返回SOCKET_ERROR
select例子实现非阻塞TCPServer:
#include"../common/initsock.h"//自己写的头文件
#include<iostream>
#include<cstring>
using namespace std;
CInitSock initSock;
int main()
{
SOCKET Listen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
int port = 4567;
sockaddr_in address;
address.sin_addr.S_un.S_addr = INADDR_ANY;
address.sin_family = AF_INET;
address.sin_port = htons(port);
if (::bind(Listen, (sockaddr*)&address, sizeof(address)) == SOCKET_ERROR)
{
cout << "绑定套接字失败"<< endl;
return -1;
}
::listen(Listen, 5);
fd_set fdSocket;
FD_ZERO(&fdSocket);
FD_SET(Listen, &fdSocket);
while (true)
{
fd_set fdRead = fdSocket; //每次循环完毕都将fdSocket集合的所有套接字复制到fdRead中
int sum = ::select(0, &fdRead, NULL, NULL, NULL);//只判断fdRead集合中有几个套接字处于都状态,有几个就返回几
if (sum > 0) //select函数发现fdRead集合中有sum个套接字处于读状态
{
for (int i = 0; i < (int)fdSocket.fd_count; i++)//确定处于读状态的套接字到底是fdSocket中的哪一个
{
if (FD_ISSET(fdSocket.fd_array[i], &fdRead) == true)//在fdRead集合中检查到fdSocket.fd_array[i]的存在
{
if (fdSocket.fd_array[i] == Listen)//如果该套接字是监听套接字
{
if (fdSocket.fd_count < FD_SETSIZE)//FD_SETSIZE最大为64,也就是说最多只能放64个套接字
{
sockaddr_in RemoteAddress;
int RemoteAddressLen = sizeof(RemoteAddress);
SOCKET newSocket = ::accept(Listen, (SOCKADDR*)&RemoteAddress, &RemoteAddressLen);
FD_SET(newSocket, &fdSocket);
printf("收到来自地址为:(%s)的连接!\n", ::inet_ntoa(RemoteAddress.sin_addr));
}
else
{
cout << "fdSocket中的套接字放得太多啦!" << endl;
continue;
}
}
else//如果该套接字不是监听套接字而是接受套接字
{
char data[256];
int index = ::recv(fdSocket.fd_array[i], data, strlen(data) , 0);
if (index > 0)
{
data[index] = '\0';
printf("接收到数据: %s\n", &data);
}
else
{
printf("对方关闭了socket!\n");
::closesocket(fdSocket.fd_array[i]);
FD_CLR(fdSocket.fd_array[i], &fdSocket);
}
}
}
}
}
else
{
cout << "select函数未发现fdRead集合中有任何套接字处于读状态" << endl;
break;
}
}
return 0;
}