背景知识分析:
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使用介绍:
注意select函数的第一个参数,是所有加入集合的句柄值的最大那个值还要加1。比如我们创建了3个句柄:
在使用select函数之前,一定要找到3个句柄中的最大值是哪个,我们一般定义一个变量来保存最大值,取得最大socket值如下:
然后调用select函数:
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 );
// 有数据可读,进行相关处理
}