1、
int select(int fds,fd_set *readfds,fd_set * writefds,fd_set * exceptfds,struct timeval * timeout);
select 監管多個I/O,檢測遍歷[0,fds)的描述符,select實現的服務器稱爲併發(非並行)服務器,多核cpu纔有並行
可同時檢測標準出入和網絡端口事件,不會由於阻塞在標準輸入而沒法處理網絡數據數組
2、
可讀:可讀事件產生的4種狀況(前三種)
套接口緩衝區有數據可讀;
鏈接的讀一半關閉,即接收到,讀操做將返回0,通知select
若是是監聽套接口(服務器),已完成鏈接隊列不爲空時;
套接口上發生一個錯誤待處理,錯誤能夠經過getsockopt指定SO_ERROR選項來獲取服務器
可寫:(第一種)
套接口發送緩衝區有空間容納數據(不斷產生)
鏈接的寫一半關閉。對方關閉,不會發送數據過來,能夠發送數據。第一次發送write,收到RST段。再次調用會產生SIGPIPE信號
套接口上發生一個錯誤待處理,錯誤能夠經過getsockopt指定SO_ERROR選項來獲取網絡
異常:
套接口存在帶外數據。併發
利用select改進回射服務器程序:socket
如圖所示:fd最多FD_SETSIZE個,fd=3(listenfd)放進rset。接下來若監聽套接口產生事件,那麼創建鏈接,將conn放入rset中。同時更新maxfd。spa
服務器端改進:一個進程指針
#include<unistd.h> #include<sys/types.h> #include<sys/socket.h> #include<string.h> #include<stdlib.h> #include<stdio.h> #include<errno.h> #include<netinet/in.h> #include<arpa/inet.h> #include<signal.h> #include<sys/wait.h>
#define ERR_EXIT(m)\
do\ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0) ssize_t readn(int fd,void *buf,size_t count) { size_t nleft=count; ssize_t nread; char *bufp=(char*)buf; while(nleft>0) { if((nread=read(fd,bufp,nleft))<0) { if(errno==EINTR) continue; else
return -1; } else if(nread==0) return (count-nleft); bufp+=nread; nleft-=nread; } return count; } ssize_t writen(int fd, const void *buf, size_t count) { size_t nleft=count; ssize_t nwritten; char *bufp=(char*)buf; while(nleft>0) { if((nwritten=write(fd,bufp,nleft))<=0) { if(errno==EINTR) continue; return -1; }else if(nwritten==0) continue; bufp+=nwritten; nleft-=nwritten; } return count; } ssize_t recv_peek(int sockfd,void *buf,size_t len) { while(1) { int ret=recv(sockfd,buf,len,MSG_PEEK);//從sockfd讀取內容到buf,但不去清空sockfd,偷窺
if(ret==-1&&errno==EINTR) continue; return ret; } } //偷窺方案實現readline避免一次讀取一個字符
ssize_t readline(int sockfd,void * buf,size_t maxline) { int ret; int nread; size_t nleft=maxline; char *bufp=(char*)buf; while(1) { ret=recv_peek(sockfd,bufp,nleft);//不清除sockfd,只是窺看
if(ret<0) return ret; else if(ret==0) return ret; nread=ret; int i; for(i=0;i<nread;i++) { if(bufp[i]=='\n') { ret=readn(sockfd,bufp,i+1);//讀出sockfd中的一行而且清空
if(ret!=i+1) exit(EXIT_FAILURE); return ret; } } if(nread>nleft) exit(EXIT_FAILURE); nleft-=nread; ret=readn(sockfd,bufp,nread); if(ret!=nread) exit(EXIT_FAILURE); bufp+=nread;//移動指針繼續窺看
} return -1; } void handle_sigchld(int sig) { while(waitpid(-1,NULL, WNOHANG)>0) ; } int main(void) { signal(SIGCHLD,handle_sigchld); int listenfd; if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0) ERR_EXIT("socket error"); //if((listenfd=socket(PF_INET,SOCK_STREAM,0))<0) //本地協議地址賦給一個套接字
struct sockaddr_in servaddr; memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_port=htons(5188); servaddr.sin_addr.s_addr=htonl(INADDR_ANY);//表示本機地址 //開啓地址重複使用,關閉服務器再打開不用等待TIME_WAIT
int on=1; if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0) ERR_EXIT("setsockopt error"); //綁定本地套接字
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) ERR_EXIT("bind error"); if(listen(listenfd,SOMAXCONN)<0)//設置監聽套接字(被動套接字)
ERR_EXIT("listen error"); struct sockaddr_in peeraddr;//對方套接字地址
socklen_t peerlen; /* pid_t pid; while(1){ if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0) ERR_EXIT("accept error"); //鏈接好以後就構成鏈接,端口是客戶端的。peeraddr是對端 printf("ip=%s port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port)); pid=fork(); if(pid==-1) ERR_EXIT("fork"); if(pid==0){ close(listenfd); echo_srv(conn); //某個客戶端關閉,結束該子進程,不然子進程也去接受鏈接 //雖然結束了exit退出,可是內核還保留了其信息,父進程並未爲其收屍。 exit(EXIT_SUCCESS); }else close(conn); } */
int client[FD_SETSIZE];//select最大文件描述符,用來保存已鏈接文件描述符。單進程時conn只有一個。
int i=0; for(i=0;i<FD_SETSIZE;i++) { client[i]=-1; } int conn;//已鏈接套接字(主動套接字)
int nready; int maxi=0;//最大不空閒位置
int maxfd=listenfd; fd_set rset,allset; FD_ZERO(&rset); FD_ZERO(&allset); FD_SET(listenfd,&allset); while(1) { rset=allset; //accept創建鏈接,套接口就有數據可讀
nready=select(maxfd+1,&rset,NULL,NULL,NULL);//若是是監聽套接口(服務器),已完成鏈接隊列不爲空時,accept再也不阻塞;
if(nready==-1) { if(errno==EINTR) continue; //若被信號打斷可繼續執行select
ERR_EXIT("select error"); } if(nready==0) continue; if(FD_ISSET(listenfd,&rset))//監聽口有事件,已完成隊列不爲空
{ peerlen=sizeof(peeraddr); conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen); if(conn==-1) ERR_EXIT("accept error"); for(i=0;i<FD_SETSIZE;i++) { if(client[i]<0) { client[i]=conn; if(i>maxi) maxi=i;//更新最大不空閒位置
break; } } if(i==FD_SETSIZE) { fprintf(stderr,"too many clents\n"); exit(EXIT_FAILURE); } printf("ip=%s port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port)); FD_SET(conn,&allset);//將已鏈接套接字描述符放入allset,用於監測已鏈接套接口是否有客戶端數據到來
if(conn>maxfd) maxfd=conn; if(--nready<=0) continue;//若是事件已經處理完,就繼續循環監聽,再也不執行如下代碼
} for(i=0;i<=maxi;i++)//小於等於
{ conn=client[i]; if(conn==-1) continue; if(FD_ISSET(conn,&rset))//已經鏈接套接字是否有事件,不用while(1)循環處理客戶端發送,有select監聽。
{ int ret; char recvbuf[1024]; memset(&recvbuf,0,sizeof(recvbuf)); ret=readline(conn,recvbuf,1024); if(ret==-1) ERR_EXIT("readline"); else if(ret==0) { printf("client close\n"); FD_CLR(conn,&allset);//客戶端關閉,select就不用去監聽 client[i]=-1;//將已鏈接套接口數組重置爲-1
} fputs(recvbuf,stdout); writen(conn,recvbuf,strlen(recvbuf)); if(--nready==0) break; } } } return 0; }