基于epoll实现简单的web服务器
1. 簡(jiǎn)介
epoll 是 Linux 平臺(tái)下特有的一種 I/O 復(fù)用模型實(shí)現(xiàn),于 2002 年在 Linux kernel 2.5.44 中被引入。在 epoll 之前,Unix/Linux 平臺(tái)下的 I/O 復(fù)用模型包含 select 和 poll 兩個(gè)系統(tǒng)調(diào)用。隨著因特網(wǎng)的發(fā)展,因特網(wǎng)的用戶量越來越大,C10K 問題出現(xiàn)?;?select 和 poll 編寫的網(wǎng)絡(luò)服務(wù)已經(jīng)不能滿足不能滿足用戶的需求了,業(yè)界迫切希望更高效的系統(tǒng)調(diào)用出現(xiàn)。在此背景下,FreeBSD 的 kqueue 和 Linux 的 epoll 被研發(fā)了出來。kqueue 和 epoll 的出現(xiàn),終結(jié)了 C10K 問題,C10K 問題就此作古。
因?yàn)?Linux 系統(tǒng)的廣泛應(yīng)用,所以大家在說 I/O 復(fù)用時(shí),更多的是想到了 epoll,而不是 kqueue,本文也不例外。本篇文章不會(huì)涉及 kqueue,大家有興趣可以自己看看。
?2. 基于 epoll 實(shí)現(xiàn) web 服務(wù)器
在 Linux 中,epoll 并不是一個(gè)系統(tǒng)調(diào)用,而是 epoll_create、epoll_ctl 和 epoll_wait 三個(gè)系統(tǒng)調(diào)用的統(tǒng)稱。關(guān)于這三個(gè)系統(tǒng)調(diào)用的細(xì)節(jié),這里就不說明了,大家可以自己去查 man-page。接下來,我們來直接看一個(gè)例子,這個(gè)例子基于 epoll 和?TinyHttpd?實(shí)現(xiàn)了一個(gè) I/O 復(fù)用版的 HTTP Server。在上代碼前,我們先來演示這個(gè)玩具版 HTTP Server 的效果。
上面就是玩具版 HTTP Server 的運(yùn)行效果了,看起來還行。在我第一次把它成功跑起來的時(shí)候,感覺很奇妙。好了,看完效果,接下來看代碼吧,如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/sysinfo.h> #include <sys/epoll.h> #include <signal.h> #include <fcntl.h> #include <sys/wait.h> #include <sys/types.h> #include "httpd.h"#define DEFAULT_PORT 8080 #define MAX_EVENT_NUM 1024 #define INFTIM -1void process(int);void handle_subprocess_exit(int);int main(int argc, char *argv[]) {struct sockaddr_in server_addr;int listen_fd;int cpu_core_num;int on = 1;listen_fd = socket(AF_INET, SOCK_STREAM, 0);fcntl(listen_fd, F_SETFL, O_NONBLOCK); // 設(shè)置 listen_fd 為非阻塞setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));bzero(&server_addr, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = htonl(INADDR_ANY);server_addr.sin_port = htons(DEFAULT_PORT);if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("bind error, message: ");exit(1);}if (listen(listen_fd, 5) == -1) {perror("listen error, message: ");exit(1);}printf("listening 8080\n");signal(SIGCHLD, handle_subprocess_exit);cpu_core_num = get_nprocs();printf("cpu core num: %d\n", cpu_core_num);// 根據(jù) CPU 數(shù)量創(chuàng)建子進(jìn)程,為了演示“驚群現(xiàn)象”,這里多創(chuàng)建一些子進(jìn)程for (int i = 0; i < cpu_core_num * 2; i++) {pid_t pid = fork();if (pid == 0) { // 子進(jìn)程執(zhí)行此條件分支process(listen_fd);exit(0);}}while (1) {sleep(1);}return 0; }void process(int listen_fd) {int conn_fd;int ready_fd_num;struct sockaddr_in client_addr;int client_addr_size = sizeof(client_addr);char buf[128];struct epoll_event ev, events[MAX_EVENT_NUM];// 創(chuàng)建 epoll 實(shí)例,并返回 epoll 文件描述符int epoll_fd = epoll_create(MAX_EVENT_NUM);ev.data.fd = listen_fd;ev.events = EPOLLIN;// 將 listen_fd 注冊(cè)到剛剛創(chuàng)建的 epoll 中if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {perror("epoll_ctl error, message: ");exit(1);}while(1) {// 等待事件發(fā)生ready_fd_num = epoll_wait(epoll_fd, events, MAX_EVENT_NUM, INFTIM);printf("[pid %d] ? 震驚!我又被喚醒了...\n", getpid());if (ready_fd_num == -1) {perror("epoll_wait error, message: ");continue;}for(int i = 0; i < ready_fd_num; i++) {if (events[i].data.fd == listen_fd) { // 有新的連接conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_size);if (conn_fd == -1) {sprintf(buf, "[pid %d] ? accept 出錯(cuò)了: ", getpid());perror(buf);continue;}// 設(shè)置 conn_fd 為非阻塞if (fcntl(conn_fd, F_SETFL, fcntl(conn_fd, F_GETFD, 0) | O_NONBLOCK) == -1) {continue;}ev.data.fd = conn_fd;ev.events = EPOLLIN;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) {perror("epoll_ctl error, message: ");close(conn_fd);}printf("[pid %d] ? 收到來自 %s:%d 的請(qǐng)求\n", getpid(), inet_ntoa(client_addr.sin_addr), client_addr.sin_port);} else if (events[i].events & EPOLLIN) { // 某個(gè) socket 數(shù)據(jù)已準(zhǔn)備好,可以讀取了printf("[pid %d] ? 處理來自 %s:%d 的請(qǐng)求\n", getpid(), inet_ntoa(client_addr.sin_addr), client_addr.sin_port);conn_fd = events[i].data.fd;// 調(diào)用 TinyHttpd 的 accept_request 函數(shù)處理請(qǐng)求accept_request(conn_fd, &client_addr);close(conn_fd);} else if (events[i].events & EPOLLERR) {fprintf(stderr, "epoll error\n");close(conn_fd);}}} }void handle_subprocess_exit(int signo) {printf("clean subprocess.\n");int status; while(waitpid(-1, &status, WNOHANG) > 0); } |
上面的代碼有點(diǎn)長(zhǎng),不過還好,基本上都是模板代碼,沒什么特別復(fù)雜的邏輯。希望大家耐心看一下。
上面的代碼基于epoll + 多進(jìn)程的方式實(shí)現(xiàn),開始,主進(jìn)程會(huì)通過系統(tǒng)調(diào)用獲取 CPU 核心數(shù),然后根據(jù)核心數(shù)創(chuàng)建子進(jìn)程。為了演示“驚群現(xiàn)象”,這里多創(chuàng)建了一倍的子進(jìn)程。關(guān)于驚群現(xiàn)象,下一章會(huì)講到,大家先別急哈。創(chuàng)建好子進(jìn)程后,主進(jìn)程不需再做什么事了,核心邏輯都會(huì)在子線程中執(zhí)行。首先,每個(gè)子進(jìn)程都會(huì)調(diào)用 epoll_create 在內(nèi)核創(chuàng)建 epoll 實(shí)例,然后再通過 epoll_ctl 將 listen_fd 注冊(cè)到 epoll 實(shí)例中,由內(nèi)核進(jìn)行監(jiān)控。最后,再調(diào)用 epoll_wait 等待感興趣的事件發(fā)生。當(dāng) listen_fd 中有新的連接時(shí),epoll_wait 會(huì)返回。此時(shí)子進(jìn)程調(diào)用 accept 接受連接,并把客戶端 socket 注冊(cè)到 epoll 實(shí)例中,等待 EPOLLIN 事件發(fā)生。當(dāng)該事件發(fā)生后,即可接受數(shù)據(jù),并根據(jù) HTTP 請(qǐng)求信息返回相應(yīng)的頁(yè)面了。
這里說明一下,上面代碼中處理 HTTP 請(qǐng)求的邏輯是寫在?TinyHttpd?項(xiàng)目中的,TinyHttpd 是一個(gè)只有 500 行左右的超輕量型Http Server,很適合學(xué)習(xí)使用。為了適應(yīng)需求,我對(duì)其源碼進(jìn)行了一定的修改,并添加了一些注釋。本章的測(cè)試代碼已經(jīng)放到了 github 上,需要的同學(xué)自取,傳送門 ->?epoll_multiprocess_server.c。
?3. 驚群及演示
“驚群現(xiàn)象”是指并發(fā)環(huán)境下,多線程或多進(jìn)程等待同一個(gè) socket 事件,當(dāng)這個(gè)事件發(fā)生時(shí),多線程/多進(jìn)程被同時(shí)喚醒,這就是“驚群現(xiàn)象”。對(duì)應(yīng)上面的代碼,多個(gè)子進(jìn)程通過調(diào)用 epoll_wait 等待 listen_fd 上某個(gè)事件發(fā)生。當(dāng)有新連接進(jìn)來時(shí),多個(gè)進(jìn)程會(huì)被同時(shí)喚醒去處理這個(gè)事件。但最終只有一個(gè)進(jìn)程可以去處理事件,其他進(jìn)程重新進(jìn)入等待狀態(tài)。使用上面的代碼可以演示驚群現(xiàn)象,如下:
從上圖可以看出,當(dāng) listen_fd 上有新連接事件發(fā)生時(shí),進(jìn)程19571和19573被喚醒。但最終進(jìn)程19573成功處理了新連接事件,進(jìn)程19571則失敗了。
驚群現(xiàn)象會(huì)影響服務(wù)器性能,因?yàn)槎鄠€(gè)進(jìn)程被喚醒,但最終只有一個(gè)進(jìn)程可以成功處理事件。而 CPU 需要為一個(gè)事件的發(fā)生調(diào)度數(shù)個(gè)進(jìn)程,因此會(huì)浪費(fèi) CPU 資源。
對(duì)于驚群現(xiàn)象,處理的思路一般有兩種。一種是像 Lighttpd 那樣,無視驚群。另一種是像 Nginx 那樣,使用全局鎖避免驚群。簡(jiǎn)單起見,本文測(cè)試代碼采用的是 Lighttpd 的處理方式,即無視驚群。對(duì)于這兩種思路的細(xì)節(jié),由于本人未讀過兩個(gè)開源軟件的代碼,這里就不多說了。如果大家有興趣,可以參考網(wǎng)上的一些博文。
?4. 總結(jié)
epoll 是 I/O 復(fù)用模型重要的一個(gè)實(shí)現(xiàn),性能優(yōu)異,應(yīng)用廣泛。像 Linux 平臺(tái)下的 JVM,NIO 部分就是基于 epoll 實(shí)現(xiàn)的。再如大名鼎鼎 Nginx 也是使用了 epoll。由此可以看出 epoll 的重要性,因此我們有很有必要去了解 epoll。本文通過一個(gè)測(cè)試程序簡(jiǎn)單演示了一個(gè)基于 epoll 的 HTTP Server,總體上也達(dá)到了學(xué)習(xí) epoll 的目的。大家如果有興趣,可以下載源碼看看。當(dāng)然,紙上學(xué)來終覺淺,還是要自己動(dòng)手寫才行。本文的測(cè)試代碼是本人現(xiàn)學(xué)現(xiàn)賣寫的,僅測(cè)試使用,寫的不好的地方望諒解。
好了,本文到此結(jié)束,謝謝閱讀!
?參考
- 關(guān)于多進(jìn)程epoll與“驚群”問題 - CSDN
- “驚群”,看看nginx是怎么解決它的 - CSDN
- 高性能網(wǎng)絡(luò)編程(二):上一個(gè)10年,著名的C10K并發(fā)連接問題
- 本文鏈接:?https://www.tianxiaobo.com/2018/03/02/基于epoll實(shí)現(xiàn)簡(jiǎn)單的web服務(wù)器/
from:?http://www.tianxiaobo.com/2018/03/02/%E5%9F%BA%E4%BA%8Eepoll%E5%AE%9E%E7%8E%B0%E7%AE%80%E5%8D%95%E7%9A%84web%E6%9C%8D%E5%8A%A1%E5%99%A8/?
總結(jié)
以上是生活随笔為你收集整理的基于epoll实现简单的web服务器的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: I/O模型简述
- 下一篇: Java NIO之缓冲区