一、概述
除了使用多线程或者多进程技术,我们是否还可以使用其他的方法来实现服务端连接多个客户端呢?答案是肯定的,那就是多路IO技术select。
多路IO技术: select, 同时监听多个文件描述符, 将监控的操作交给内核去处理, 数据类型fd_set: 文件描述符集合--本质是位图(关于集合可联想一个信号集sigset_t) int select(int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 函数介绍: 委托内核监控该文件描述符对应的读,写或者错误事件的发生. 参数说明: nfds: 最大的文件描述符+1 readfds: 读集合, 是一个传入传出参数 传入: 指的是告诉内核哪些文件描述符需要监控 传出: 指的是内核告诉应用程序哪些文件描述符发生了变化 writefds: 写文件描述符集合(传入传出参数) execptfds: 异常文件描述符集合(传入传出参数) timeout: NULL--表示永久阻塞, 直到有事件发生 0 --表示不阻塞, 立刻返回, 不管是否有监控的事件发生 >0--到指定事件或者有事件发生了就返回 返回值: 成功返回发生变化的文件描述符的个数 失败返回-1, 并设置errno值. /usr/include/x86_64-linux-gnu/sys/select.h和 /usr/include/x86_64-linux-gnu/bits/select.h 从上面的文件中可以看出, 这几个宏本质上还是位操作. void FD_CLR(int fd, fd_set *set); 将fd从set集合中清除. int FD_ISSET(int fd, fd_set *set); 功能描述: 判断fd是否在集合中 返回值: 如果fd在set集合中, 返回1, 否则返回0. void FD_SET(int fd, fd_set *set); 将fd设置到set集合中. void FD_ZERO(fd_set *set); 初始化set集合. 调用select函数其实就是委托内核帮我们去检测哪些文件描述符有可读数据,可写,错误发生;
案例:使用select技术实现高并发聊天服务
二、代码示例
//IO多路复用技术select函数的使用 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <errno.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/select.h> int main(){ int i;//for循环的初始化 int n;//读取字节个数 int lfd;//监听文件描述符 int cfd;//通讯文件描述符 int ret; int nready; int maxfd;//最大的文件描述符 char buf[FD_SETSIZE]; socklen_t len; int maxi;//有效的文件描述符最大值 int connfd[FD_SETSIZE];//有效文件描述符数组 fd_set tmpfds,rdfds;//要监控的文件描述符集 struct sockaddr_in svraddr,cliaddr; //创建socket lfd = socket(AF_INET,SOCK_STREAM,0); if(lfd<0){ perror("socket error"); return -1; } //允许端口复用 int opt = 1; setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int)); //绑定 svraddr.sin_family = AF_INET; svraddr.sin_port = htons(8888); svraddr.sin_addr.s_addr = htonl(INADDR_ANY); ret = bind(lfd,(struct sockaddr *)&svraddr,sizeof(struct sockaddr_in)); if(ret<0){ perror("bind error"); return -1; } //监听 ret = listen(lfd,5); if(ret<0){ perror("listen error"); return -1; } //文件描述符集初始化 FD_ZERO(&tmpfds); FD_ZERO(&rdfds); //将监听文件描述符加入到监控的读集合中 FD_SET(lfd,&rdfds); //初始化有效的文件描述符集,为-1表示可用,该数组不保存lfd for(i=0;i<FD_SETSIZE;i++){ connfd[i] = -1; } maxfd = lfd; len = sizeof(struct sockaddr_in); //将监听文件描述符lfd加入到select监控中 while(1){ //select为阻塞函数,若没有变化的文件描述符,就一直阻塞,若有事件发生则解除阻塞,函数返回 //select的第二个参数tmpfds为输入输出参数,调用select完毕后这个节后中保留的是发生变化的文件描述符 tmpfds = rdfds; nready = select(maxfd+1,&tmpfds,NULL,NULL,NULL); if(nready>0){//文件描述符集有变化 //发生变化的文件描述符有两类,一类是监听类的,一类是用于数据通信的。 //监听文件描述符有变化,有新的连接到来,则accept新的连接 if(FD_ISSET(lfd,&tmpfds)){ cfd = accept(lfd,(struct sockaddr *)&cliaddr,&len); if(cfd<0){ if(errno==ECONNABORTED||errno==EINTR){ continue; } break; } //先找到位置,然后将新的链接的文件描述符保存到connfd数组中 for(i=0;i<FD_SETSIZE;i++){ if(connfd[i]==-1){ connfd[i] = cfd; break; } } //若连接总数达到的最大值 if(i==FD_SETSIZE){ close(cfd); printf("too many clients,i==[%d]\n",i); continue; } //确保connfd中maxi保存的是最后一个文件描述符的下标 if(i>maxi){ maxi = i; } //打印客户端的IP和PORT char sIP[16]; memset(sIP,0x00,sizeof(sIP)); printf("receive from client ---->IP[%s],PORT=[%d]\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,sIP,sizeof(sIP)),htons(cliaddr.sin_port)); //将新的文件描述符加入到select监控的文件描述符中 FD_SET(cfd,&rdfds); if(maxfd<cfd){ maxfd = cfd; } //如果没有变化的文件描述符,则无需执行后续代码 if(--nready<=0){ continue; } } //下面是通信文件描述符有变化的情况 //只需要循环connfd数组中有效的文件描述符即可,这样可以减少循环次数 for(i=0;i<=maxi;i++){ int sockfd = connfd[i]; //数组内的文件描述符如果被释放,有可能变为-1 if(sockfd==-1){ continue; } if(FD_ISSET(sockfd,&tmpfds)){ memset(buf,0x00,sizeof(buf)); n = read(sockfd,buf,sizeof(buf)); if(n<0){ perror("read over"); close(sockfd); FD_CLR(sockfd,&rdfds); connfd[i] = -1;//将connfd[0]置为-1,表示位置可用 }else if(n==0){ printf("client is closed\n"); close(sockfd); FD_CLR(sockfd,&rdfds); connfd[i] = -1;//将connfd[0]置为-1,表示位置可用 }else{ printf("[%d]:[%s]\n",n,buf); write(sockfd,buf,n); } if(--nready<=0){ break;//注意这里是break,而不是continue,应该是从最外层的while继续循环 } } } } } //关闭监听文件描述符 close(lfd); return 0; }