最近看了一些零聲學院的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(回收指定進程組內的任意子進程),專門用來回收子進程