一、概述
除了使用多線程或者多進程技術,我們是否還可以使用其他的方法來實現服務端連接多個客戶端呢?答案是肯定的,那就是多路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; }