linux使用多進程實現一個TCP server,使多個客戶端和伺服器通信

lee哥說架構 發佈 2020-08-08T00:15:02+00:00

要實現多個客戶端連接到伺服器,就需要解決阻塞問題,比如當伺服器在read阻塞讀客戶端數據時,如果客戶端沒有數據到達,伺服器端就會阻塞在read函數上,這時如果有新的客戶端連接請求,由於伺服器阻塞在read函數,就不能及時響應客戶端的請求,使用多進程並發可以解決這個問題,實現多個客戶端連接同一個伺服器,當伺服器接收到一個客戶端的連接後,就fork出一個的進程去處理客戶端數據,讓父進程去accept接收新的客戶端連接請求。

最近看了一些零聲學院的linux視頻,把「多進程進行socket編程」好好理解了一下,整理出來的。

用TCP協議編寫了一個簡單的伺服器、客戶端,其中伺服器一直在監聽本機8000號埠。如果收到客戶端的連結,就在伺服器端把客戶端的IP和埠號列印出來,收到客戶端發送的數據,伺服器會把數據變成大寫並發送回客戶端。要實現多個客戶端連接到伺服器,就需要解決阻塞問題,比如當伺服器在read阻塞讀客戶端數據時,如果客戶端沒有數據到達,伺服器端就會阻塞在read函數上,這時如果有新的客戶端連接請求,由於伺服器阻塞在read函數,就不能及時響應客戶端的請求,使用多進程並發可以解決這個問題,實現多個客戶端連接同一個伺服器,當伺服器接收到一個客戶端的連接後,就fork出一個的進程去處理客戶端數據,讓父進程去accept接收新的客戶端連接請求。代碼及詳細解釋如下:

伺服器端程序:server.c

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <unistd.h>
#include <ctype.h> 
#define SERVER_PORT 8000 //監聽本機8000埠
#define MAX 4096
int main(void)
         {      
         struct sockaddr_in serveraddr,clientaddr;
         int sockfd,addrlen,confd,len;
         char ipstr[128];
char buf[4096];
pid_t pid;      
//1.socket
sockfd = socket(AF_INET,SOCK_STREAM,0);
//2.bind        bzero(&serveraddr,sizeof(serveraddr));
//地址族協議ipv4
serveraddr.sin_family = AF_INET;
//ip地址
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(SERVER_PORT);
bind(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
//3.listen
listen(sockfd,128);     
while(1){       
  //4. accept阻塞監聽客戶端的連結請求
  addrlen = sizeof(clientaddr); 
  confd = accept(sockfd,(struct sockaddr *)&clientaddr,&addrlen);
  //如果有客戶端連接上伺服器,就輸出客戶端的ip地址和埠號
  printf("client ip %s\tport %d\n",     
         inet_ntop(AF_INET,(struct sockaddr *)&clientaddr.sin_addr.s_addr,ipstr,sizeof(ipstr)),ntohs(clientaddr.sin_port));
  //這塊是多進程的關鍵,當accept收到了客戶端的連接之後,就創建子進程,讓子進程去處理客戶端  
  //發來的數據,父進程裡面關閉confd(因為用不到了),然後父進程回到while循環繼續監聽客戶端的連接
  pid = fork();
  //5. 子進程處理客戶端請求
  if(pid == 0){//子進程
    close(sockfd);
    while(1){//循環讀取客戶端發來的數據,把小寫變成大寫     
      len = read(confd,buf,sizeof(buf));        
      int i = 0;
      while(i < len){        
        buf[i] = toupper(buf[i]);       
        i++;
      } 
      write(confd,buf,len);     
    }   
    close(confd);
    return 0;   
  }     
  else if(pid > 0){//父進程關閉文件描述符,釋放資源 
    close(confd); 
  }
}       
return 0;
}

客戶端程序:client.c

#include <netinet/in.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/stat.h>
#include <ctype.h>
#include <stdlib.h>
#define SERVER_PORT 8000
#define MAXLINE 4096
int main(void)
         {
         struct sockaddr_in serveraddr;
         int confd,len;
         char ipstr[] = "10.170.20.238";//這是伺服器的地址,使用ifconfig來查看
char buf[MAXLINE];
//1.創建一個socket
confd = socket(AF_INET,SOCK_STREAM,0);  
//2.初始化伺服器地址,指明我要連接哪個伺服器
bzero(&serveraddr,sizeof(serveraddr));      
serveraddr.sin_family = AF_INET;        
inet_pton(AF_INET,ipstr,&serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(SERVER_PORT);
//3.連結伺服器       
connect(confd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
while(fgets(buf,sizeof(buf),stdin)){
  //4.請求伺服器處理數據
  write(confd,buf,strlen(buf));
  len = read(confd,buf,sizeof(buf));    
  write(STDOUT_FILENO,buf,len); 
}
//5.關閉socket
close(confd);   
return 0;
}

我們在虛擬機下,使用一個虛擬機開啟多個終端來觀察效果,如果你的虛擬機聯網了,就使用ifconfig命令來查看IP位址,填入客戶端代碼的 ipstr 數組中,這裡我的ip是"10.170.20.238",也可以不聯網,使用sudo ifconfig ens33 10.170.20.238 自己給虛擬機設置一個虛擬IP(關機之後這個ip就失效了)

注意:sudo ifconfig ens33 192.168.1.12 中的 ens33 每個人的虛擬機不一定一樣,還是使用ifconfig查看,如下圖:

設置好IP以後,編譯server.c和client.c文件,然後開多個終端執行,執行結果如下,開了一個伺服器,三個客戶端,伺服器能正確處理三個客戶端的請求。(客戶端的埠號是隨機的)

上圖是開了三個客戶端的,那伺服器到底能連接多少個客戶端呢?這取決於伺服器端機器的內存和性能

這個程序存在兩個問題,一個是出錯處理,為了理解方便我就沒加,還有一個就是子進程回收問題,子進程回收我們一般可以使用wait或waitpid讓父進程去回收子進程資源,但是伺服器端的父進程在等待客戶端的連接請求,沒辦法去回收,還可以使用信號去回收子進程,子進程退出時會給父進程發送SIGCHLD信號,我們可以在信號處理函數裡面去回收子進程,但是,執行信號處理函數,會打斷父進程的accept,這樣就又沒法及時響應客戶端的連接了,所以我們還是使用waitpid,多創建一個進程通過指定waitpid的第一個參數為-1(回收指定進程組內的任意子進程),專門用來回收子進程

關鍵字: