Socket網絡編程--聊天程序(4)


  上一小節講到可以實現多客戶端與服務器進行通訊,對於每一個客戶端的連接請求,服務器都要分配一個進程進行處理。對於多用戶連接時,服務器會受不了的,而且還很消耗資源。據說有個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

  

  本文出處: http://www.cnblogs.com/wunaozai/p/3870338.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM