1、select函数原型
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
参数解释:
maxfdp——传入参数,集合中所有文件描述符的范围,即最大文件描述符值+1
readfds——传入传出参数,select调用时传入要监听的可读文件描述符集合,select返回时传出发生可读事件的文件描述符集合
writefds——传入传出参数,select调用时传入要监听的可写文件描述符集合,select返回时传出发生可写事件的文件描述符集合
errorfds——传出参数,select返回时传出发生事件(包括可读和可写)中异常事件的文件描述符集合
timeout——传入参数,设置select阻塞的时间。若设置为NULL,则select一直阻塞直到有事件发生;
若设置为0,则select为非阻塞模式,执行后立即返回;
若设置为一个大于0的数,即select的阻塞时间,若阻塞时间内有事件发生就返回,否则时间到了立即返回
fd_set是自定义的一个数据结构,可看作一个集合,存放可读、可写或异常事件的文件描述符。fd_set集合通常有以下四个宏来操作:
void FD_ZERO(fd_set *fdset); //清空fdset中所有文件描述符
void FD_SET(int fd,fd_set *fdset); //添加文件描述符fd到集合fdset中
void FD_CLR(int fd,fd_set *fdset); //将文件描述符fd从集合fdset中去除
int FD_ISSET(int fd,fd_set *fdset); //判断文件描述符fd是否在集合fdset中
select工作原理:传入要监听的文件描述符集合(可读、可写或异常)开始监听,select处于阻塞状态,当有事件发生或设置的等待时间timeout到了就会返回,返回之前自动去除集合中无事件发生的文件描述符,返回时传出有事件发生的文件描述符集合。但select传出的集合并没有告诉用户集合中包括哪几个就绪的文件描述符,需要用户后续进行遍历操作。
2、select优缺点
优点:
(1)select的可移植性较好,可以跨平台;
(2)select可设置的监听时间timeout精度更好,可精确到微秒,而poll为毫秒。
缺点:
(1)select支持的文件描述符数量上限为1024,不能根据用户需求进行更改;
(2)select每次调用时都要将文件描述符集合从用户态拷贝到内核态,开销较大;
(3)select返回的就绪文件描述符集合,需要用户循环遍历所监听的所有文件描述符是否在该集合中,当监听描述符数量很大时效率较低。
3、select使用经典案例
用select函数编写一个简单的高并发服务器,且假设服务器启动时处于无连接状态,满足以下功能:
a)可处理来自一个新客户端的连接请求;
b)监听可读事件,若已连接客户端的已连接描述符发生可读事件,服务器从客户端读取数据并处理;
服务器端代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<ctype.h>
#define SERV_PORT 6666
int main()
{
int i,j,n,maxi;
int maxfd,listenfd,connfd,sockfd;
int nready,client[FD_SETSIZE-1]; //FD_SETSIZE=1024,定义数组client来储存已连接描述符,最多1023个
char buf[BUFSIZ], str;
struct sockaddr_in clie_addr,serv_addr;
socklen_t clie_addr_len;
fd_set allset,readset; //定义监听描述符集合allset和发生事件描述符集合readset
bzero(&serv_addr,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(SERV_PORT): //端口号,将无符号短整型转换为网络字节顺序
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); //一个主机可能有多个网卡,所以是本机的任意IP地址
listenfd=socket(AF_INET,SOCK_STREAM,0); //AF_INET表示使用32位IP地址,SOCK_STREAM表示使用TCP连接
bind(listenfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)); //将服务器套接字地址与套接字描述符联系起来
listen(listenfd,1024); //设置可监听的连接数量为1024
maxfd=listenfd; //初始化最大文件描述符为监听描述符listenfd
//初始化client数组,将数组所有元素置为-1
int maxi=-1; //数组client储存的文件描述符的个数,初始化为-1
for (i=0;i<FD_SETSIZE;i++)
client[i]=-1
//初始化select监听文件描述符的集合
FD_ZERO(&allset); //初始化监听集合
FD_SET(listenfd,&allset); //将监听描述符listenfd添加到集合中
while(1)
{
readset=allset;
nready=select(maxfd+1,&readset,NULL,NULL,NULL); //select只监听可读事件,且为永久阻塞直到有事件发生
if (nready<0)
perr_exit("select error");
//判断listenfd是否发生事件,若发生,则处理新客户端连接请求
if (FD_ISSET(listenfd,&readset))
{
clie_addr_len=sizeof(clie_addr);
connfd=accept(listenfd,(struct sockaddr *)&clie_addr,&clie_addr_len);//与请求客户端建立连接
printf(“received from %s at port %d\n”,
inet_ntop(AF_INET,&clie_addr.sin_addr.s_addr,&str,sizeof(str)),
ntohs(clie_add.sin_port)); //打印该客户端的IP地址和端口号
//将connfd赋值给client数组中第一个为-1的元素位置
for (i=0;i<FD_SETSIZE;i++)
{
if (client[i]<0)
{
client[i]=connfd;
break;
}
}
//判断select监听的文件描述符的个数是否超过上限
if (i == FD_SIZE-1) //减1的原因是要考虑监听描述符listenfd也属于select监控
{
fputs("too many clients\n",stderr);
exit(1);
}
FD_SET(connfd,&allset); //向监控的文件描述符集合allset中添加新的描述符connfd
if (connfd>maxfd)
maxfd=connfd; //更新最大文件描述符值
//保证maxi永远是client数组中最后一个非-1的元素的位置
if(i>maxi)
maxi=i;
//如果nready=1,即只有一个发生事件的描述符,在此条件下必为listenfd,则返回循环位置,继续调用select监控;否则继续向下执行
--nready;
if (nready==0)
continue;
}
//找到client数组中发生事件的已连接描述符,并读取、处理数据
for (i=0;i<=maxi;i++)
{
sockfd=client[i];
if (sockfd<0) //已连接描述符失效,重新开始循环
continue;
if (FD_ISSET(sockfd,&readset))
{
n=read(sockfd,buf,sizeof(buf));
if (n==0) //当客户端关闭连接,服务端也关闭连接
{
colse(sockfd);
FD_CLR(sockfd,&allset); //解除select对该已连接文件描述符的监控
client[i]=-1;
}
else if (n>0)
{
for (j=0;j<n;j++)
buf[j]=toupper(buf[j]);
sleep(2);
write(sockfd,buf,n);
}
--nready;
if (nready==0)
break; //跳出for循环,还在while中
}
}
}
close(listenfd);
return 0;
}
客户端代码
#include<stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#define SERV_IP "127.0.0.1" //客户端、服务端都在一台主机上,所以直接用本机IP地址
#define SERV_PORT 6666
int main()
{
int cfd;
struct sockaddr_in serv_addr;
char buf[BUFSIZ];
int n;
cfd=socket(AF_INET,SOCK_STREAM,0);
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(SERV_PORT);
inet_pton(AF_INET,SERV_IP,&serv_addr.sin_addr.s_addr); //将点十进制字节串转换为网络字节序
connect(cfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
while(1)
{
fgets(buf,sizeof(buf),stdin);
write(cfd,buf,strlen(buf));
n=Read(cfd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,n);
}
close(cfd);
return 0;
}
将服务端、客户端生成可执行文件之后,先启动服务器,再启动客户端与服务器建立连接,用户输入字符,服务器将读取到的字符转换为大写,再写回客户端的屏幕上,测试结果如下所示:
服务器会显示客户端的IP地址以及端口号,可发现客户端与服务器来自同一台主机,这是没有问题的
客户端会显示用户输入的字符串以及转为大写后写回的字符串,如图:
注意:数据传输完毕后,一定要客户端先断开连接,避免服务器出现TIME_WAIT状态,从而占用服务器资源。