總結一下Linux下常見的幾種并發編程方式,
多行程并發
這是出現的最早的也是寫起來最簡單的一種方式,大概可以總結成父行程接收客戶端的連接請求,啟動子行程負責與客戶端通信,

多行程服務器代碼:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void client_handler(int fd)
{
char buf[32] = {0};
int ret;
while (1)
{
ret = recv(fd, buf, sizeof(buf), 0);
if (-1 == ret)
{
perror("recv");
exit(1);
}
else if (0 == ret)
{
break;
}
if (!strcmp(buf, "bye"))
{
break;
}
printf("%s\n", buf);
memset(buf, 0, sizeof(buf));
}
close(fd);
kill(getppid(), SIGALRM);
}
void my_wait(int sig)
{
int status;
wait(&status);
}
int main()
{
//創建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
exit(1);
}
int opt = 1;
setsockopt(sockfd,SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8000);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (-1 == ret)
{
perror("bind");
exit(1);
}
ret = listen(sockfd, 10);
if (-1 == ret)
{
perror("listen");
exit(1);
}
printf("等待客戶端的連接...\n");
struct sockaddr_in client_addr;
int length = sizeof(client_addr);
signal(SIGALRM, my_wait);
while (1)
{
int fd = accept(sockfd, (struct sockaddr *)&client_addr, &length);
if (-1 == fd)
{
perror("accept");
exit(1);
}
printf("接受客戶端的連接 %d\n", fd);
pid_t pid = fork();
if (0 == pid)
{
client_handler(fd);
exit(0);
}
}
close(sockfd);
return 0;
}
多行程服務器優點在于:
- 方法簡單,易于理解;
- 行程的特點在于健壯,即一個行程奔潰掉并不會影響其他行程的執行,
多行程并發的缺點也很明顯:
- 因為多個行程地址空間相互獨立,所以各個行程想要實作資料的傳遞,必須使用指定的行程間通信方式;
- 行程的開銷比較大,如果并發量比較大,服務器的負載會變得很大,
為了解決以上兩個缺點,于是就有了執行緒,
多執行緒并發
跟行程相比,執行緒的優點很多:
- 資源消耗少;
- 執行緒間切換速度快;
- 執行緒間通信不需要復雜的通信機制;
- 使多CPU的系統作業更加有效,
我們可以為每一個客戶端創建一個執行緒,這樣同樣的并發量對服務器造成的壓力會比行程小,

多執行緒服務器代碼:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
void *ClientHandler(void *arg)
{
int ret;
int fd = *(int *)arg;
char buf[32] = {0};
pthread_detach(pthread_self()); //執行緒結束,自動釋放資源
while (1)
{
ret = recv(fd, buf, sizeof(buf), 0);
if (-1 == ret)
{
perror("recv");
exit(1);
}
else if (0 == ret) //客戶端例外退出
{
break;
}
printf("接收%d客戶端%s\n", fd, buf);
memset(buf, 0, sizeof(buf));
}
printf("%d 客戶端退出!\n", fd);
close(fd);
}
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
exit(1);
}
int opt = 1;
setsockopt(sockfd,SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = 8000;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (-1 == ret)
{
perror("bind");
exit(1);
}
ret = listen(sockfd, 10);
if (-1 == ret)
{
perror("listen");
exit(1);
}
printf("等待客戶端的連接...\n");
struct sockaddr_in client_addr;
int length = sizeof(client_addr);
while (1)
{
int fd = accept(sockfd, (struct sockaddr *)&client_addr, &length);
if (-1 == fd)
{
perror("accept");
exit(1);
}
printf("接受客戶端的連接 %d\n", fd);
//為每一個客戶端創建新的執行緒
pthread_t tid;
ret = pthread_create(&tid, NULL, ClientHandler, &fd);
if (ret != 0)
{
perror("pthread_create");
exit(1);
}
}
close(sockfd);
return 0;
}
是不是使用多執行緒實作的服務器就是完美的呢?當然不是,
多執行緒因為是共享同一個地址空間,所以一個執行緒的奔潰會導致整個行程掛掉,同時執行緒的通信方式過于簡單,只需要讀取記憶體就行,會導致多個執行緒同時訪問共享資源,程式員的大部分時間都消耗在了解決多執行緒并發的問題上,
因此,多執行緒解決并發的問題也并不是完美的方案,
多路復用
并發服務器并不是只有行程和執行緒才能解決,還有一個目前比較流行的技術叫做多路復用,
什么是多路復用技術?
舉個例子,假如你是一個老師(服務器),現在讓班級里30個學生做一道數學題,做好之后你逐個檢查,有這么幾種方案,
第一種:從第一個人開始檢查,順序把30個人檢查完,這種效率是最低的,一旦中間遇到某個學生解題沒有思路,那么將會影響整個班的進度,這種方法都談不上是并發服務器,
第二種:找來30個老師,每個老師負責檢查一個學生,不難理解,效率是相當的高,這種方法就是多行程/多執行緒并發服務器,
第三種:你站在講臺上,告訴學生題目答完后叫一聲,但是你又不知道是誰叫的,所以一聽到聲音你就逐個詢問,直到找到這個學生并且檢查他的答案,這種就是多路復用select的實作方案,
順便講一下第四種:你站在講臺上,告訴學生題目答完后舉手,這種方法直接省去了你逐個詢問,效率高于select,在服務器上使用epoll技術實作,也是多路復用的一種,
多路復用基本上可以歸納為兩個因素:事件以及處理事件的函式,
程式在不斷的回圈,等待事件的到來,來了之后根據事件型別的不同呼叫不同的事件處理函式,

select并發服務器代碼:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int fd[1024] = {0};
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
exit(1);
}
int opt = 1;
setsockopt(sockfd,SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = 8000;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (-1 == ret)
{
perror("bind");
exit(1);
}
ret = listen(sockfd, 10);
if (-1 == ret)
{
perror("listen");
exit(1);
}
fd_set readfd, tmpfd; //定義集合
FD_ZERO(&readfd); //清空集合
FD_SET(sockfd, &readfd); //添加到集合
int maxfd = sockfd, i = 0;
struct sockaddr_in client_addr;
int length = sizeof(client_addr);
char buf[32] = {0};
while (1)
{
tmpfd = readfd;
ret = select(maxfd + 1, &tmpfd, NULL, NULL, NULL);
if (-1 == ret)
{
perror("select");
exit(1);
}
if (FD_ISSET(sockfd, &tmpfd)) //判斷sockfd是否還留在集合里面 判斷是否有客戶端發起連接
{
for (i = 0; i < 1024; i++) //選擇合適的i
{
if (fd[i] == 0)
{
break;
}
}
fd[i] = accept(sockfd, (struct sockaddr *)&client_addr, &length);
if (-1 == fd[i])
{
perror("accept");
exit(1);
}
printf("接受來自%s的客戶端的連接 fd = %d\n", inet_ntoa(client_addr.sin_addr), fd[i]);
FD_SET(fd[i], &readfd); //新的檔案描述符加入到集合中
if (maxfd < fd[i])
{
maxfd = fd[i];
}
}
else //有客戶端發訊息
{
for (i = 0; i < 1024; i++)
{
if (FD_ISSET(fd[i], &tmpfd)) //判斷是哪個fd可讀
{
ret = recv(fd[i], buf, sizeof(buf), 0);
if (-1 == ret)
{
perror("recv");
}
else if (0 == ret)
{
close(fd[i]); //關閉TCP連接
FD_CLR(fd[i], &readfd); //從集合中清除掉
printf("客戶端%d下線!\n", fd[i]);
fd[i] = 0;
}
else
{
printf("收到%d客戶端的訊息%s\n", fd[i], buf);
}
memset(buf, 0, sizeof(buf));
break;
}
}
}
}
return 0;
}
select的缺點:
- 最大的并發數受內核的限制;
- 每次都要掃描整個集合,集合越大,效率越低,
epoll并發服務器代碼:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/epoll.h>
#define MAXSIZE 256
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
exit(1);
}
int opt = 1;
setsockopt(sockfd,SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = 8000;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (-1 == ret)
{
perror("bind");
exit(1);
}
ret = listen(sockfd, 10);
if (-1 == ret)
{
perror("listen");
exit(1);
}
int epfd = epoll_create(MAXSIZE); //創建epoll物件
if (-1 == epfd)
{
perror("epoll_create");
exit(1);
}
struct epoll_event ev, events[MAXSIZE] = {0};
ev.events = EPOLLIN; //監聽sockfd可讀
ev.data.fd = sockfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
if (-1 == ret)
{
perror("epoll_ctl");
exit(1);
}
struct sockaddr_in client_addr; //用于保存客戶端的資訊
int length = sizeof(client_addr), i;
char buf[32] = {0};
while (1)
{
int num = epoll_wait(epfd, events, MAXSIZE, -1); //-1表示阻塞
if (-1 == num)
{
perror("epoll_wait");
exit(1);
}
for (i = 0; i < num; i++)
{
if (events[i].data.fd == sockfd) //有客戶端發起連接
{
int fd = accept(sockfd, (struct sockaddr *)&client_addr, &length);
if (-1 == fd)
{
perror("accept");
exit(1);
}
printf("接受來自%s的連接fd=%d\n", inet_ntoa(client_addr.sin_addr), fd);
//為新的檔案描述符注冊事件
ev.data.fd = fd;
ev.events = EPOLLIN;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
if (-1 == ret)
{
perror("epoll_ctl");
}
}
else
{
if (events[i].events & EPOLLIN) //如果事件是可讀的
{
ret = recv(events[i].data.fd, buf, sizeof(buf), 0);
if (-1 == ret)
{
perror("recv");
}
else if (0 == ret)
{
ev.data.fd = events[i].data.fd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, &ev); //客戶端退出,注銷事件
close(events[i].data.fd);
}
else
{
printf("收到%d客戶端的訊息%s\n", events[i].data.fd, buf);
}
memset(buf, 0, sizeof(buf));
}
}
}
}
return 0;
}
epoll的性能提升:
- 沒有最大并發連接的限制;
- 主動呼叫回呼函式,無需逐個檢測,
多路復用解決了多執行緒中同步的問題,確實省去了很多麻煩,
但是由于只有一個執行緒,如果在處理某個事件的時候,遇到了IO操作,比如讀取大檔案,那么程式將會被阻塞,此時其他的事件將不會被處理,解決這個問題可以使用異步IO,就是讀取檔案的時候,不管檔案有沒有讀完都會立即回傳,不影響處理其他事件,至于IO操作什么時候完成,作業系統會有其他辦法去檢測,
總結
隨著服務器負載的越來越大,高并發問題早已不是行程、執行緒或者多路復用能解決的,而是事件 + 執行緒 + 協程的組合,但是不管怎樣,了解了歷史才能更深刻的理解當下,以上就是給大家總結的幾種比較經典的并發服務器實作方案,
以上有不足的地方歡迎指出討論,最后,如果覺得學習資料難找的話,可以添加學習交流群:960994558 學習資料已經共享在群里了,期待你的加入~

這里推薦一個金牌大佬的免費課程,這個跟以往所見到的只會空談理論的有所不同,正在學習的朋友可以體驗一下
C/C++Linux服務器開發/后臺架構師
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/252717.html
標籤:其他
上一篇:2021-01-25
