select函数详解及使用案例

it2023-11-14  63

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==0continue; } //找到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状态,从而占用服务器资源。

最新回复(0)