系统提供select函数来实现多路复用输入/输出模型。select系统调用是用来让我们的程序监视 多个文件句柄的状态变化的。程序会停在select这里等待,直到被监视的文件句柄有一个或 多个发生了状态改变。关于文件句柄(socket),其实就是一个整数,我们最熟悉的句柄是0、1、2三 个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE * 结构的表示就是stdin、stdout、stderr。
select函数:
参数:
在介绍参数前先来说说第二,三,四个参数的类型:fd_set
fd_set:这是一个独立的类型,为说明方便,取fd_set长度为1字节,fd_set中的每一个bit位 可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。
注意:没有事件 发生的fd=5被清空。
nfds是select函数要监视的文件描述符的最大值加1。
readfds,writefds,exceptfds,分别代表着可读文件描述符,可写文件描述符,和异常文件描述符的集合,对于需要监视的文件描述符,可以根据他们是要进行怎样的读写操作进行设定。当没有可监视的文件描述符时,设置为NULL。
timeout是一个结构体参数,用于描述一段时间,在这段时间里,如果没有要监视的文件描述符,读写就绪,那么select函数会返回0。
返回值:
监视到有文件描述符读写就绪时,返回就绪文件描述符的个数,当超出timeout中设置的时间时,还没有文件描述符就绪时,返回0,函数出错时返回-1。
对于select函数还有四个宏函数来对其监视的三个文件描述符集进行设置:
FD_CLR(inr fd,fd_set* set);用来清除描述词组set中相关fd 的位
FD_ISSET(int fd,fd_set *set);用来测试描述词组set中相关fd 的位是否为真
FD_SET(int fd,fd_set*set);用来设置描述词组set中相关fd的位
FD_ZERO(fd_set *set);用来清除描述词组set的全部位
下面是我写的一个基于select函数的多路转接服务器:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define FDS_NUM (sizeof(fd_set)*8)
int fds[FDS_NUM];//创建一个数组来保存要监视的文件描述
static void usage(char* proc)
{
printf("usage:%s [local_ip] [local_port]\n",proc);
}
int startup(char* ip,int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
exit(2);
}
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
perror("bind");
exit(3);
}
if(listen(sock,5)<0)
{
perror("listen");
exit(4);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
int listen_sock = startup(argv[1],atoi(argv[2]));//封装一个创建listen_sock的函数
fd_set rfds;//创建一个读字符集
int i = 0;
for(;i1;
}
while(1)
{
FD_ZERO(&rfds);
int max = 0;//select参数nfds设置
struct timeval timeout = {1,0};//select参数timeout设置
fds[0] = listen_sock;//将创建好的监听套接字放到保存数组中
for(i=0;i//将保存数组中的文件描述符设置进select的监视文件描述符集
{
if(fds[i]>0)
{
FD_SET(fds[i],&rfds);
if(max<=fds[i])
{
max = fds[i]+1;
}
}
}
switch(select(max,&rfds,NULL,NULL,&timeout))
{
case 0:
printf("wait timeout!\n");//函数超时
break;
case -1:
perror("select");//函数出错
break;
default://函数成功,返回有事件发生的fd的个数
{
for(i=0;iif(fds[i] == -1)
continue;
if((i == 0) && (FD_ISSET(fds[i],&rfds)))//有外部链接访问
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_sock < 0)
{
perror("accept");
close(new_sock);
break;
}
printf("get a client#[%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
int j = 0;
for(;jif(fds[j] == -1)//将new_sock添加到保存数组中
{
fds[j] = new_sock;
break;
}
if(j == FDS_NUM)//select的文件描述符集达到上限
{
close(new_sock);
}
}
}
else if((i != 0)&&(FD_ISSET(fds[i],&rfds)))//读事件就绪
{
char buf[1024];
ssize_t s = read(fds[i],buf,sizeof(buf)-1);
if(s > 0)//读到数据
{
buf[s] = 0;
printf("client: %s\n",buf);
}else if(s == 0)//链接断开
{
printf("client quit!\n");
close(fds[i]);
fds[i] = -1;
break;
}else
{
perror("read");
close(fds[i]);
fds[i] = -1;
break;
}
}
}
}
break;
}
}
return 0;
}
由于select函数每次返回时都会把没有发生事件发生的文件描述符清除掉,因此每次调用时都需要把要监视的文件描述符重新设定进select函数所监视的文件描述符集,因此我们创建一个数组,来保存,所要监视的文件描述符集,也便于把要监视的文件描述符设定进select函数的文件描述符集。
当外部有链接请求时,select会监听到listen_sock事件就绪,并返回1,然后利用accept获得这个链接,成功建立连接后,会产生一个新的文件描述符,把这个新的文件描述符放到保存数组中,然后select函数结束,这时就要把保存数组中的文件描述符重新设定进select的文件描述符集中,select函数又开始监听。因此select函数需要放在一个死循环中
select服务器的优缺点:
优点:
1、始终只有一个进程
2、select目前几乎在所有的平台上都支持,其良好跨平台支持也是它的一个优点
缺点:
1、代码编写时,调用前需要特别设定
2、当用户增多时,函数会不停的在内核态和用户态之间交替
3、每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
4、同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
5、select最大的缺点就是支持的文件描述符数量太小了,默认是1024,也就是说最多同时支持1023个用户同时连接服务器