开门见山,如果我们要对多个客户端连接的多个事件进行操作,首先会想到建立多个线程或进程让其去各自进行,这也是最简单的模式。
但对每一个线程或进程而言,无论连接是否有事件发生,都必须随时待命,也就是说,每一个对象都必须有一个线程或进程与之一一对应,直到对象销毁。
可想而知,当连接量规模变大后,系统需要在很多个线程或进程之间进行切换,时间与空间上的开销巨大,也就是说,这种模式下,程序能承载对象的最大值是很小的(一般数百个)。
那么,就要提到select函数了。man select得到函数参数及头文件如下
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);//删除fd
int FD_ISSET(int fd, fd_set *set);//检测fd是否存在于组
void FD_SET(int fd, fd_set *set);//添加fd
void FD_ZERO(fd_set *set);//对组进行清零
nfds:整型变量,是指集合中所有文件描述符的范围,假如集合中最大文件描述符为max,那么 ndfs=max+1,千万不要理解为文件描述符的总量。
readfds:指针,指向一组等待可读性检查的套接字集合。
writefds:指针,指向一组等待可写性检查的套接集合。
exceptfds:指针,指向一组等待错误检查的套接集合。
timeout:select()最多等待时间,如果为NULL,则相当于阻塞(等待直到事件发生),若为 0 则为非阻塞(没有事件便立即返回),其他值代表若有事件或是超时则返回。
函数流程解释及样例
int ret,i,fd[MAX],MAX;
struct timeval timeout;
fd_set readfds;
while(1)
{
timeout.tv_sec = 1;
timeout.tv_usec = 0;
FD_ZERO(&readfds);//组清零初始化
for(i=0;i<MAX;i++)
{
FD_SET(fd[i], &readfds);
}
ret select(MAX,&readfds, NULL,NULL,&timeout);//仅演示读监听,其他同理
}
将select放在循环体内时,有两点是要特别注意的
<1>timeout的初始化,因为select在执行时,对timeout进行的实际是类似于i–的操作,所以循环体内每次都要重新初始化,否则timeout的值将永久是0;
<2>文件描述符集合的初始化,因为集合的机理类似于二进制,FD_ZERO()后集合内全部归零(0000000),若FD_SET(5),则为(0100000),当selete在执行过程中,发现每个某个连接有事件产生时,便会将该套接字标志为1,没有事件的标志为0,执行完成后,若有事件产生,则可以FD_ISSET(fd,&readfds)来判断fd是否有事件产生。
因此,每执行一次select函数,都要重新对集合进行初始化。
select()可以确定一个或多个套接口的状态,常用来实现单进程或单线程的多路复用。
也就是说,select可以在一个线程或进程内对多个连接的事件事件进行响应。
那么它是如何完成这个功能的呢,举个例子来说:
假如你在100家不同的店各订了一份外卖(不同的店收货点当然是不同的)。
- 如果用上述多进程或线程的模式来取外卖的话,你就要再找99个朋友帮你去各个收货点去等待。(多进程模式,哪来的手机!)
- 你有一盏名叫select的信号灯,你只需要守候在select前,每当一家或多家外卖要送到的时候,都会将信号灯点亮,那么问题来了,你只知道外卖送到了,但却并不知道到底是哪家,所以你需要亲自把100家外卖的送货点全部遍历一遍,取下已到的外卖,然后继续回到信号灯前等待。有什么优点呢,很显然,100个人才能完成的事你一个人就完成了,而且多个外卖可能会同时到,顺风路走了不少嘛。
至于缺点嘛,当然是每次都要把100家收货点全部走一遍(减肥也没有这么拼的)- 找epoll函数,它会给你一部手机,送货师傅外卖送到时可以给你打电话啦。有了手机,这一切该是多么完美,那么会是怎么个完美法呢,下章再谈吧。
ps:好困啊,年纪大了,熬不了夜了!