上一小節講到可以實現多客戶端與服務器進行通訊,對於每一個客戶端的連接請求,服務器都要分配一個進程進行處理。對於多用戶連接時,服務器會受不了的,而且還很消耗資源。據說有個select函數可以用,好像還很NB的樣子。
使用select多路轉換處理聊天程序
下面摘取APUE 14.5小結 I/O多路轉接
當從一個描述符讀,然后又寫到另一個描述符時,可以在下列形式的循環中循環中使用阻塞I/O:
while((n = read(STDIN_FILENO, buf, BUFFSIZ))>0)
if(write(STDOUT_FILENO, buf, n)!=n)
err_sys("write error");
這種形式的阻塞I/O到處可見。但是如果必須從兩個描述符讀,又將如何呢?如果仍舊使用阻塞I/O,那么就可能長時間阻塞在一個描述符上,而另一個描述符雖有很多數據卻不能得到及時處理。所以為了處理這種情況顯然需要另一種不同的技術。
方法一:也就是上一小節使用的方法,使用多進程。每一個進程處理一個描述符
方法二:和上面相似的,使用多線程,不同的線程處理不同的描述符
方法三:仍然使用一個進程執行該程序,但使用非阻塞I/O讀取數據。然后對所有的描述符進行遍歷一遍,判斷對應的描述符是否有數據,如果有就讀取,如果沒有就立即返回。這種辦法就是輪詢(polling)
方法四:異步I/O。其基本的思想是進程告訴內核,當一個描述符已經准備好可以進行I/O時,用一個信號通知它。
方法五:這是一種比較好的辦法。叫做I/O多路轉換(I/O multiplexing)。先構造一張有關描述符的列表,然后調用一個函數,直到這些描述符中的一個已經准備好進行I/O時,該函數才會返回。在返回時,它高數進程哪些描述符已經准備好可以進行I/O。
poll,pselect和select這三個函數使我們能夠執行I/O多路轉換。本程序只使用select函數。
#include <sys/select.h>
int select (int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct time val *restrict tvptr); //返回值:准備就緒的描述符數,若超時則返回0,否則出錯返回-1
select 函數講解
FD_ISSET判斷描述符fd是否在給定的描述符集fdset中,通常配合select函數使用,由於select函數成功返回時會將未准備好的描述符位清零。通常我們使用FD_ISSET是為了檢查在select函數返回后,某個描述符是否准備好,以便進行接下來的處理操作。
fd_set數據類型的操作
#include <sys/select.h>
int FD_ISSET(int fd, fd_set *fdset); //判斷fd是否在fdset中
void FD_CLR(int fd, fd_set *fdset); //進fd從fdset中取出
void FD_SET(int fd, fd_set *fdset); //將fd放入fdset
void FD_ZERO(fd_set *fdset); //將fdset清空
timeval結構分析
struct timeval{
long tv_sec; //seconds
long tv_usec; //and microseconds
};
client.c的代碼沒有改
server.c的代碼如下
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <errno.h> 4 #include <string.h> 5 #include <netdb.h> 6 #include <sys/types.h> 7 #include <sys/socket.h> 8 #include <sys/time.h> 9 #include <sys/un.h> 10 #include <sys/ioctl.h> 11 #include <sys/wait.h> 12 #include <sys/select.h> 13 #include <netinet/in.h> 14 #include <arpa/inet.h> 15 16 #define SERVER_PORT 12138 17 #define BACKLOG 20 18 #define MAX_CON_NO 10 19 #define MAX_DATA_SIZE 4096 20 21 int MAX(int a,int b) 22 { 23 if(a>b) return a; 24 return b; 25 } 26 27 int main(int argc,char *argv[]) 28 { 29 struct sockaddr_in serverSockaddr,clientSockaddr; 30 char sendBuf[MAX_DATA_SIZE],recvBuf[MAX_DATA_SIZE]; 31 int sendSize,recvSize; 32 int sockfd,clientfd; 33 fd_set servfd,recvfd;//用於select處理用的 34 int fd_A[BACKLOG+1];//保存客戶端的socket描述符 35 int conn_amount;//用於計算客戶端的個數 36 int max_servfd,max_recvfd; 37 int on=1; 38 socklen_t sinSize=0; 39 char username[32]; 40 int pid; 41 int i; 42 struct timeval timeout; 43 44 if(argc != 2) 45 { 46 printf("usage: ./server [username]\n"); 47 exit(1); 48 } 49 strcpy(username,argv[1]); 50 printf("username:%s\n",username); 51 52 /*establish a socket*/ 53 if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) 54 { 55 perror("fail to establish a socket"); 56 exit(1); 57 } 58 printf("Success to establish a socket...\n"); 59 60 /*init sockaddr_in*/ 61 serverSockaddr.sin_family=AF_INET; 62 serverSockaddr.sin_port=htons(SERVER_PORT); 63 serverSockaddr.sin_addr.s_addr=htonl(INADDR_ANY); 64 bzero(&(serverSockaddr.sin_zero),8); 65 66 /* 67 * SOL_SOCKET.SO_REUSEADDR 允許重用本地地址 68 * */ 69 setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)); 70 71 /*bind socket*/ 72 if(bind(sockfd,(struct sockaddr *)&serverSockaddr,sizeof(struct sockaddr))==-1) 73 { 74 perror("fail to bind"); 75 exit(1); 76 } 77 printf("Success to bind the socket...\n"); 78 79 /*listen on the socket*/ 80 if(listen(sockfd,BACKLOG)==-1) 81 { 82 perror("fail to listen"); 83 exit(1); 84 } 85 86 timeout.tv_sec=1;//1秒遍歷一遍 87 timeout.tv_usec=0; 88 sinSize=sizeof(clientSockaddr);//注意要寫上,否則獲取不了IP和端口 89 90 FD_ZERO(&servfd);//清空所有server的fd 91 FD_ZERO(&recvfd);//清空所有client的fd 92 FD_SET(sockfd,&servfd); 93 conn_amount=0; 94 max_servfd=sockfd;//記錄最大的server端描述符 95 max_recvfd=0;//記錄最大的client端的socket描述符 96 while(1) 97 { 98 FD_ZERO(&servfd);//清空所有server的fd 99 FD_ZERO(&recvfd);//清空所有client的fd 100 FD_SET(sockfd,&servfd); 101 //timeout.tv_sec=30;//可以減少判斷的次數 102 switch(select(max_servfd+1,&servfd,NULL,NULL,&timeout))//為什么要+1,是因為第一個參數是所有描述符中最大的描述符fd號加一,原因的話在APUE中有講,因為內部是一個數組,第一個參數是要生成一個這樣大小的數組 103 { 104 case -1: 105 perror("select error"); 106 break; 107 case 0: 108 //在timeout時間內,如果沒有一個描述符有數據,那么就會返回0 109 break; 110 default: 111 //返回准備就緒的描述符數目 112 if(FD_ISSET(sockfd,&servfd))//sockfd 有數據表示可以進行accept 113 { 114 /*accept a client's request*/ 115 if((clientfd=accept(sockfd,(struct sockaddr *)&clientSockaddr, &sinSize))==-1) 116 { 117 perror("fail to accept"); 118 exit(1); 119 } 120 printf("Success to accpet a connection request...\n"); 121 printf(">>>>>> %s:%d join in!\n",inet_ntoa(clientSockaddr.sin_addr),ntohs(clientSockaddr.sin_port)); 122 //每加入一個客戶端都向fd_A寫入 123 fd_A[conn_amount++]=clientfd; 124 max_recvfd=MAX(max_recvfd,clientfd); 125 } 126 break; 127 } 128 //FD_COPY(recvfd,servfd); 129 for(i=0;i<MAX_CON_NO;i++)//最大隊列進行判斷,優化的話,可以使用鏈表 130 { 131 if(fd_A[i]!=0) 132 { 133 FD_SET(fd_A[i],&recvfd);//對所有還連着服務器的客戶端都放到fd_set中用於下面select的判斷 134 } 135 } 136 137 switch(select(max_recvfd+1,&recvfd,NULL,NULL,&timeout)) 138 { 139 case -1: 140 //select error 141 break; 142 case 0: 143 //timeout 144 break; 145 default: 146 for(i=0;i<conn_amount;i++) 147 { 148 if(FD_ISSET(fd_A[i],&recvfd)) 149 { 150 /*receive datas from client*/ 151 if((recvSize=recv(fd_A[i],recvBuf,MAX_DATA_SIZE,0))==-1) 152 { 153 //perror("fail to receive datas"); 154 //表示該client是關閉的 155 printf("close\n"); 156 FD_CLR(fd_A[i],&recvfd); 157 fd_A[i]=0;//表示該描述符已經關閉 158 } 159 else 160 { 161 printf("Client:%s\n",recvBuf); 162 //可以判斷recvBuf是否為bye來判斷是否可以close 163 memset(recvBuf,0,MAX_DATA_SIZE); 164 } 165 } 166 } 167 break; 168 } 169 170 } 171 return 0; 172 }
運行后的截圖結果
可以看出三個客戶端都可以隨時連接到服務器,並且發送數據給服務器。實現的效果跟上一節的多進程實現是一樣的。畢竟沒有大量客戶端進行連接,所以就看不出效果,從書中和網上介紹說,這樣可以提高某些方面的性能。
下一節將介紹服務器端向各個還在線的客戶端進行發送數據,實現交互。然后再實現聊天室功能,大概的思路就是對接收到的數據進行轉發。
參考資料: http://www.cnblogs.com/gentleming/archive/2010/11/15/1877976.html