併發伺服器三種實現方式之程序、執行緒和select
前言:剛開始學網路程式設計,都會先寫一個客戶端和服務端,不知道你們有沒有試一下:再開啟一下客戶端,是連不上服務端的。還有一個問題不知道你們發現沒:有時啟伺服器,會提示“Address already in use”,過一會就好了,想過為啥麼?在這篇部落格會解釋這個問題。
但現實的伺服器都會連很多客戶端的,像阿里伺服器等,所以這篇主要介紹如何實現併發伺服器,主要通過三種方式:程序、執行緒和select函式來分別實現。
一、程序實現併發伺服器
先說下什麼是併發伺服器吧?不是指有多個伺服器同時執行,而是可以同時連線多個客戶端。
先簡單說下原理吧,先畫個圖,如下: PS:全部落格園最醜圖,不接受反駁!哈哈哈
先要搞清楚通訊的流程,圖上引數說明:
lfd:socket函式的返回值,就是監聽描述符
cfd1/cfd2/cfd3:accept函式的返回值,用通訊的套接字
server:伺服器
client:客戶端
socket通訊過程中,總共有幾個套接字呢?答:三個,客戶端一個,伺服器兩個。
根據上圖來大致說明一下流程:
客戶端建立一個套接字描述符,用於通訊,伺服器先用socket函式建立套接字,用於監聽客戶端,然後呼叫accept函式,會返回一個套接字,用於通訊的。圖上就是,client1先通過cfd與server建立連線,然後與cfd1建立連線通訊,這時lfd就空閒了,再監聽客戶端,client2再與lfd連線,再跟cfd2通訊。client3也是如此。
現在問題就是。如何建立多個程序與客戶端通訊呢?通過迴圈建立子程序就可以實現這個問題,可以參考我的這篇部落格:https://www.cnblogs.com/liudw-0215/p/9667686.html
服務端程式,如下:
#include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <sys/wait.h> #include <ctype.h> #includeView Code<unistd.h> #include "wrap.h" #define MAXLINE 8192 #define SERV_PORT 8000 void do_sigchild(int num) { while (waitpid(0, NULL, WNOHANG) > 0) ; } int main(void) { struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int i, n; pid_t pid; struct sigaction newact; newact.sa_handler = do_sigchild; sigemptyset(&newact.sa_mask); newact.sa_flags = 0; sigaction(SIGCHLD, &newact, NULL); //建立訊號,處理子程序退出 listenfd = Socket(AF_INET, SOCK_STREAM, 0); // int opt = 1; //setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //埠複用 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); Listen(listenfd, 20); printf("Accepting connections ...\n"); while (1) { cliaddr_len = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); pid = fork(); if (pid == 0) { Close(listenfd); while (1) { n = Read(connfd, buf, MAXLINE); if (n == 0) { printf("the other side has been closed.\n"); break; } printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port)); for (i = 0; i < n; i++) buf[i] = toupper(buf[i]); Write(STDOUT_FILENO, buf, n); Write(connfd, buf, n); } Close(connfd); return 0; } else if (pid > 0) { Close(connfd); } else perr_exit("fork"); } return 0; }
客戶端程式如下:
/* client.c */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include <arpa/inet.h> #include "wrap.h" #define MAXLINE 8192 #define SERV_PORT 8000 int main(int argc, char *argv[]) { struct sockaddr_in servaddr; char buf[MAXLINE]; int sockfd, n; sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); servaddr.sin_port = htons(SERV_PORT); Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); while (fgets(buf, MAXLINE, stdin) != NULL) { Write(sockfd, buf, strlen(buf)); n = Read(sockfd, buf, MAXLINE); if (n == 0) { printf("the other side has been closed.\n"); break; } else Write(STDOUT_FILENO, buf, n); } Close(sockfd); return 0; }View Code
演示效果,伺服器可以同時處理兩個客戶端,如下:
但我想再測試一下程式,執行./server,發現有個bind error,如下:
下面來解釋一下這個問題:
先來一張圖片(出自UNP),如下:
這張圖將三次握手、四次揮手和TCP狀態轉換圖,這些在我的這篇部落格都由介紹,可以參考一下:https://www.cnblogs.com/liudw-0215/p/9661583.html
注意最後有一個TIME_WAIT狀態,主動關閉一端會經歷2MSL時長等待(大約40秒),再變為最開始的狀態CLOSED。
復現上面的“bind error”,只需退出伺服器,在啟伺服器,就會報出此錯。因為主動關閉一端,會經歷2MSL時長,埠IP會被佔用,所以會報“bind error”。
但可能會問:為啥先退出客戶端沒有此問題?因為客戶端沒有呼叫bind函式地址結構,會“隱式”生成埠。
有沒有方法可以解決這個問題呢?當然有的,呼叫函式setsockopt即可,服務端程式如下:
#include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <sys/wait.h> #include <ctype.h> #include <unistd.h> #include "wrap.h" #define MAXLINE 8192 #define SERV_PORT 8000 void do_sigchild(int num) { while (waitpid(0, NULL, WNOHANG) > 0) ; } int main(void) { struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int i, n; pid_t pid; struct sigaction newact; newact.sa_handler = do_sigchild; sigemptyset(&newact.sa_mask); newact.sa_flags = 0; sigaction(SIGCHLD, &newact, NULL); //建立訊號,處理子程序退出 listenfd = Socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //埠複用 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); Listen(listenfd, 20); printf("Accepting connections ...\n"); while (1) { cliaddr_len = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); pid = fork(); if (pid == 0) { Close(listenfd); while (1) { n = Read(connfd, buf, MAXLINE); if (n == 0) { printf("the other side has been closed.\n"); break; } printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port)); for (i = 0; i < n; i++) buf[i] = toupper(buf[i]); Write(STDOUT_FILENO, buf, n); Write(connfd, buf, n); } Close(connfd); return 0; } else if (pid > 0) { Close(connfd); } else perr_exit("fork"); } return 0; }View Code
二、執行緒實現併發伺服器
理解了程序的方式,就是建立多個執行緒來實現,就不過多解釋了,程式需要對執行緒有一定了解,之後我還會寫篇部落格來介紹執行緒,敬請期待哦。
伺服器程式碼如下,有有詳細的解釋:
#include <stdio.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> #include <ctype.h> #include <unistd.h> #include <fcntl.h> #include "wrap.h" #define MAXLINE 8192 #define SERV_PORT 8000 struct s_info { //定義一個結構體, 將地址結構跟cfd捆綁 struct sockaddr_in cliaddr; int connfd; }; void *do_work(void *arg) { int n,i; struct s_info *ts = (struct s_info*)arg; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; //#define INET_ADDRSTRLEN 16 可用"[+d"檢視 while (1) { n = Read(ts->connfd, buf, MAXLINE); //讀客戶端 if (n == 0) { printf("the client %d closed...\n", ts->connfd); break; //跳出迴圈,關閉cfd } printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)), ntohs((*ts).cliaddr.sin_port)); //列印客戶端資訊(IP/PORT) for (i = 0; i < n; i++) buf[i] = toupper(buf[i]); //小寫-->大寫 Write(STDOUT_FILENO, buf, n); //寫出至螢幕 Write(ts->connfd, buf, n); //回寫給客戶端 } Close(ts->connfd); return (void *)0; } int main(void) { struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; pthread_t tid; struct s_info ts[256]; //根據最大執行緒數建立結構體陣列. int i = 0; listenfd = Socket(AF_INET, SOCK_STREAM, 0); //建立一個socket, 得到lfd bzero(&servaddr, sizeof(servaddr)); //地址結構清零 servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //指定本地任意IP servaddr.sin_port = htons(SERV_PORT); //指定埠號 8000 Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //繫結 Listen(listenfd, 128); //設定同一時刻連結伺服器上限數 printf("Accepting client connect ...\n"); while (1) { cliaddr_len = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); //阻塞監聽客戶端連結請求 ts[i].cliaddr = cliaddr; ts[i].connfd = connfd; /* 達到執行緒最大數時,pthread_create出錯處理, 增加伺服器穩定性 */ pthread_create(&tid, NULL, do_work, (void*)&ts[i]); pthread_detach(tid); //子執行緒分離,防止僵執行緒產生. i++; } return 0; }View Code
客戶端程式碼如下:
/* client.c */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include <arpa/inet.h> #include "wrap.h" #define MAXLINE 80 #define SERV_PORT 8000 int main(int argc, char *argv[]) { struct sockaddr_in servaddr; char buf[MAXLINE]; int sockfd, n; sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr.s_addr); servaddr.sin_port = htons(SERV_PORT); Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); while (fgets(buf, MAXLINE, stdin) != NULL) { Write(sockfd, buf, strlen(buf)); n = Read(sockfd, buf, MAXLINE); if (n == 0) printf("the other side has been closed.\n"); else Write(STDOUT_FILENO, buf, n); } Close(sockfd); return 0; }View Code
三、select實現併發伺服器
select和程序主要區別在於,程序是阻塞的,而select是交給核心自己來實現的,由於select比較複雜,參考我的另一篇部落格:https://www.cnblogs.com/liudw-0215/p/9661583.html
總結:有不懂的,歡迎及時評論。