背景知识分析:
1. fd_set 结构体
fd_set是文件句柄的集合。
FD_ZERO 清空这个集合;
FD_SET 往这个集合里面加入一个文件句柄;
FD_ISSET 查看某一个文件句柄是否被设置了;
'fd_set') 是一组文件描述符(fd)的集合。由于fd_set类型的长度在不同平台上不同,因此应该用一组标准的宏定义来处理此类变量:
fd_set set;
FD_ZERO(&set); /* 将set清零 */
FD_SET(fd, &set); /* 将fd加入set */
FD_CLR(fd, &set); /* 将fd从set中清除 */
FD_ISSET(fd, &set); /* 判断fd是否处于可用状态 是为true */
2使用介绍:
- <font face="Verdana">fd_set rdfds; /* 先申明一个 fd_set 集合来保存我们要检测的 socket句柄 */
- struct timeval tv; /* 申明一个时间变量来保存时间 */
- int ret; /* 保存返回值 */
- FD_ZERO(&rdfds); /* 用select函数之前先把集合清零 */
- FD_SET(socket, &rdfds); /* 把要检测的句柄socket加入到集合里 */
- tv.tv_sec = 1;
- tv.tv_usec = 500; /* 设置select等待的最大时间为1秒加500毫秒 */
- ret = select(socket + 1, &rdfds, NULL, NULL, &tv); /* 检测我们上面设置到集合rdfds里的句柄是否有可读信息 */
- if(ret < 0) perror("select");/* 这说明select函数出错 */
- else if(ret == 0) printf("超时/n"); /* 说明在我们设定的时间值1秒加500毫秒的时间内,socket的状态没有发生变化 */
- else { /* 说明等待时间还未到1秒加500毫秒,socket的状态发生了变化 */
- printf("ret=%d/n", ret); /* ret这个返回值记录了发生状态变化的句柄的数目,由于我们只监视了socket这一个句柄,所以这里一定ret=1,如果同时有多个句柄发生变化返回的就是句柄的总和了 */
- /* 这里我们就应该从socket这个句柄里读取数据了,因为select函数已经告诉我们这个句柄里有数据可读 */
- if(FD_ISSET(socket, &rdfds)) { /* 先判断一下socket这外被监视的句柄是否真的变成可读的了 */
- /* 读取socket句柄里的数据 */
- recv(...);
- }
- }
- </font>
注意select函数的第一个参数,是所有加入集合的句柄值的最大那个值还要加1。比如我们创建了3个句柄:
- int sa, sb, sc;
- sa = socket(...); /* 分别创建3个句柄并连接到服务器上 */
- connect(sa,...);
- sb = socket(...);
- connect(sb,...);
- sc = socket(...);
- connect(sc,...);
- FD_SET(sa, &rdfds);/* 分别把3个句柄加入读监视集合里去 */
- FD_SET(sb, &rdfds);
- FD_SET(sc, &rdfds);
在使用select函数之前,一定要找到3个句柄中的最大值是哪个,我们一般定义一个变量来保存最大值,取得最大socket值如下:
- int maxfd = 0;
- if(sa > maxfd) maxfd = sa;
- if(sb > maxfd) maxfd = sb;
- if(sc > maxfd) maxfd = sc;
然后调用select函数:
- ret = select(maxfd + 1, &rdfds, NULL, NULL, &tv); /* 注意是最大值还要加1 */
3 关于 Select 的 FD_SETSIZE
nt select(int n, fd_set *rd_fds, fd_set *wr_fds, fd_set *ex_fds, struct timeval *timeout);
Select 用到了fd_set结构,从man page里可以知道fd_set能容纳的句柄和FD_SETSIZE相关。实际上fd_set在*nix下是一个bit标志数组,每个bit表示对应下标 的fd是不是在fd_set中。fd_set只能容纳编号小于 FD_SETSIZE的那些句柄。
FD_SETSIZE默认是1024,如果向 fd_set里放入过大的句柄,数组越界以后程序就会垮掉。系统默认限制了一个进程最大的句柄号不超过1024,但是可以通过ulimit -n命令/setrlimit函数来扩大这一限制。如果不幸一个程序在FD_SETSIZE=1024的环境下编译,运行时又遇到ulimit n > 1024的,那就只有祈求上帝保佑不会垮掉了。
通过上述说明,可以看到如果连接很多的话,建议还是使用epoll来解决这个问题。
4 select 机制的优势
为什么会出现select模型?
先看一下下面的这句代码:
int iResult = recv(s, buffer,1024);
这是用来接收数据的,在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返 回,不然就会一直阻塞在那里。在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永 远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差。
再看代码:
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
iResult = recv(s, buffer,1024);
这一次recv的调用不管套接字连接上有没有数据可以接收都会马上返回。原因就在于我们用ioctlsocket把套接字设置为非阻塞模式了。不过 你跟踪 一下就会发现,在没有数据的情况下,recv确实是马上返回了,但是也返回了一个错误:WSAEWOULDBLOCK,意思就是请求的操作没有成功完成。 看到这里很多人可能会说,那么就重复调用recv并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大。
select模型的出现就是为了解决上述问题。
select模型的关键是使用一种有序的方式,对多个套接字进行统一管理与调度 。
看核心代码:(这里只给出服务端的)
while ( 1 )
{
// 初始化fdset
FD_ZERO( &fdsRead );
// 将server套接字添加到可读集合中
FD_SET( sockServer, &fdsRead );
// 调用select
select( 0, &fdsRead, NULL, NULL, &tv );
// 判断server套接字的状态,如果套接字还在可读集合中,
// 说明有数据可以读入,则建立套接字可以成功
if ( FD_ISSET( sockServer, &fdsRead ) )
{
sockAccept = accept( sockServer, (sockaddr*)&addr, &nLen );
// 有数据可读,进行相关处理
}