# High Performance Web Server
contributed by < `zzzxxx00019`, `ptzling310`, `tsengsam` >
---
## I. 事前學習
### Linux TCP Socket 連線
`Linux` 的 `TCP Socket` 架構圖如下,功能專注於對單一客戶端的服務,但無法滿足 `Web` 伺服器需同一時間接受大量連線的要求
![](https://i.imgur.com/sgdm8QC.png)
當 `socket` 埠 (port) 建立後,將 `socket` 狀態 `block` 在 `accept` 的狀態等待連線,一旦 `client` 成功與 `server` 建立連線,`server` 使用 `read` 從 `client` 端讀取發送過來的資訊,並透過 `write` 進行回覆,最後關閉 `socket` 連線狀態。
### 改善 blocking 問題:改用 Non-blocking I/O
> [淺談I/O Model](https://medium.com/@clu1022/%E6%B7%BA%E8%AB%87i-o-model-32da09c619e6)
將 read 改為 non-blocking I/O,且等待有資料時再做處理。
而等待有資料再處理這個動作要透過事件驅動(event-driven)模型來達成。
* I/O models
- Blocking / Non-blocking I/O
| Models | 有資料可讀取前的使用者 | 舉例 |
| -------- | -------- | --------- |
| Blocking I/O | blocked 直到資料準備好 | 未設定 O_NONBLOCK 的 read, select, epoll |
| Non-blocking I/O | 直接返回且設定`errno`,但要一直詢問 I/O 資料是否準備好 (polling) | 設定 O_NONBLOCK 的 read, select, epoll |
![](https://miro.medium.com/max/602/1*5uPdSnjRALGMiKAYTUZXcA.png)
![](https://miro.medium.com/max/602/1*LpOGE7BnIugjK9_PznlIPw.png)
- Synchronous / Asynchronous I/O
| Models | 實際進行 I/O 操作的使用者 | 舉例 |
| -------- | -------- | -------- |
| Synchronous I/O | blocked | read, recvfrom |
| Asynchronous I/O | 核心複製完資料才會告知使用者,故不會被 blocked |aio_read(), aio_write() |
* I/O multiplexing
![](https://miro.medium.com/max/602/1*jf2x3N2yYhcZDGF7wip9rA.png)
最初所使用的為 `select` ,但會受限要走訪整個 `fd set` 而產生的效率疑慮,故採用`epoll()` 。
### Linux 核心系統呼叫行為
#### epoll
```c
int epoll_create(int size);
```
建立新的 epoll instance。
- 參數
1. size:自 Linux 2.6.8 後此值只需 `> 0` 即可。
- 傳回值描述:若成功,該系統呼叫會回傳一個 `fd`;錯誤則回傳 `-1`。
```c
int epoll_create1(int flags);
```
建立新的 epoll instance。
- 參數
1. `flags == 0`,則功能與 `epoll_create` 相同;若為`EPOLL_CLOEXEC` 則表示 child process 無法存取 parent process 設定的 epoll 資源。
- 傳回值描述:若成功,該系統呼叫會回傳一個 `fd`;錯誤則回傳 `-1`。
```c
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
```
新增想要 monitor 的 `fd` 到 epoll set(interest list) 中。
- ready list : epoll set(interest list) 的 subset。
- Argument
1. epfd:`epoll_create` 回傳的 fd,fd 為一個在 kernel 中的 epoll instance
2. op:要對 fd 執行的操作,有以下三種:
* Register fd (`EPOLL_CTL_ADD`):將 fd 加入 epoll instance
* Delete fd (`EPOLL_CTL_DEL`):將 fd 從 epoll instance 移除,則 processor 不會再得到任何關於此 fd evevnt 的通知
* Modify fd (`EPOLL_CTL_MOD`):修改 fd 監控中的 event
3. fd:想要加入 epoll set (interest list) 的 fd
4. event:指向 structure:`epoll_event` 的 pointer,該 structure 會儲存想監控的 event
```c
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
```
等待 epoll instance:`epdf` 內事件發生。
- Argument
1. epfd:要等待事件的 instance
2. events:紀錄事件已發生的 fd
3. maxevents:需 `>0`
4. timeout:epoll_wait() block 的時間 (milliseconds)
- Return value:回傳準備好執行 I/O 動作的 fd 數量;錯誤則回傳 `-1`。
#### timerfd
透過 `timerfd_create()` 將時間變成 `fd` 的狀態,一旦 `fd` 超時就讀取檔案內容,此法將計時器轉化為 `fd` 操作,只要時間到就自動進行 `read` 的動作,以下為一些基本的 `API` :
```c
int timerfd_create(int clockid, int flags);
```
建立一個 `timerfd`,回傳 `file descriptor`
```c
int timerfd_settime(int ufd, int flags, const struct itimerspec * utmr, struct itimerspec * otmr);
```
用於啟動與停止定時器
```c
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds */
};
struct itimerspec {
struct timespec it_interval; /* Interval for periodic timer */
struct timespec it_value; /* Initial expiration */
};
```
用於設定時間的參數 `*utmr` 與 `*otmr`
```c
int timerfd_gettime(int fd, struct itimerspec *curr_value);
```
用於取得 `timerfd` 距離下次超時所剩下的時間,若已 `timeout` 且定時器為循環模式,則重啟定時器
[timerfd 結合 epoll](https://kelele67.github.io/2017/08/06/Epoll+Timerfd%E5%AE%9E%E7%8E%B0%E5%AE%9A%E6%97%B6%E5%99%A8/) 可以實踐 `timerfd` 在 `timeout` 觸發事件發生時,使用者透過 `epoll` 去檢視 `fd` 狀態,達到輪詢目的
#### sendfile
[sendfile](https://man7.org/linux/man-pages/man2/sendfile.2.html) 主要目的是在兩個 `file descriptor` 之間傳輸資料,將 `fd` 複製到另外一個 `fd` ,因為 `copy` 這動作是在 `kernel` 完成,若直接在 `kernel` 進行這樣的行為,相較於 `read/write` 需將資料反覆 `copy` 到 `user space`,較不需要進行繁瑣的 `context switch` ,以下是 `sendfile` 的 `API`
```c
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
```
`in_fd` 為要被 `read` 的 `fd` ,而 `out_fd` 則為將被 `write` 的 `fd` , `offset` 決定 `in_fd` 從何開始讀取,若設為 `NULL` 則從 `in_fd` 的初始位址開始讀取,而 `count` 則為 欲複製的 `bytes`
`read/write` 流程圖
![](https://i.imgur.com/AaUAbcd.png)
主要需要以下步驟 :
1. 使用 `read()` ,資料被 `copy` 到 `kernel buffer`
2. 再資料從 `kernel buffer` 再 `copy` 到 `user buffer` 並 `return`
3. 使用 `write()` ,從 `user buffer` 將資料 `copy` 到 `socket buffer`
4. 最後資料從 `socket buffer` 被 `copy` 到相關的協議引擎
`sendfile` 流程圖
![](https://i.imgur.com/QeoGOJa.png)
主要需要以下步驟 :
1. 資料被 `copy` 到 `kernel buffer`
2. 將 `kernel buffer` 資料直接 `copy` 到 `socket buffer`
3. 最後資料從 `socket buffer` 被 `copy` 到相關協議引擎
從上述流程圖比較中不難發現, `read/write` 需要在 `user context` 與 `kernel context` 中反覆進行 `context switch` 的動作,而 `sendfile` 直接將接收到的資料直接 `copy` 到 `socket buffer` 進行回覆,無須返回 `user space` 就能完成這樣的動作。
#### io_uring
:::info
將原本使用到的系統呼叫 `epoll` 以 `io_uring` 改寫
:::
[io_uring 詳細介紹](https://hackmd.io/@ptzling310/ByOfdvzsv#liburing-library)
* 架構圖
![](https://i.imgur.com/uhdCNbG.png)
* 函式庫
- `<liburing.h>` 函式庫,同時在 `fio` 中提供了 `ioengine=io_uring` 的支持,透過 `liburing` 可以更加快速的使用 `io_uring`
- 需將 `linux kernel` 更新至 `v5.5` 以上
- 透過以下方法,快速安裝 [liburing](https://github.com/axboe/liburing) 到 `library`
```
$ git clone https://github.com/axboe/liburing
$ cd liburing
$ ./configure && sudo make install
```
- 編譯使用 `liburing.h` 方法
```
$ gcc main.c -o main.o -luring
```
* API 及基本結構
建立一個 `io_uring ring`:
```c
struct io_uring ring ;
```
設定並初始化 `ring`:
```c
io_uring_queue_init(ENTRIES, &ring, 0);
```
當要使用 `sqe` 監控 `fd` 狀態,必須從 `ring` 拿出一個 `sqe`:
```c
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
```
然後設定 `sqe` 的狀態:
```c
io_uring_prep_rw(int op, struct io_uring_sqe *sqe, int fd, const void *addr, unsigned len,__u64 offset);
```
設定完 `sqe` 狀態後,必須將 `sqe` 提交回 `ring` ,更動 `ring` 的狀態:
```c
io_uring_submit(&ring);
```
然後等待 kernel 將完成的事件放到 CQ 中:
```c
io_uring_wait_cqe(&ring, &cqe);
```
最後增加 CQ 的 head,使下次完成的事件能放到正確的位置:
```c
io_uring_cqe_seen(&ring, cqe);
```
### sehttpd 程式
> [GitHub](https://github.com/sysprog21/sehttpd/tree/master/src)
**mainloop**
```cpp=
int main()
{
/* when a fd is closed by remote, writing to this fd will cause system
* send SIGPIPE to this process, which exit the program
*/
if (sigaction(SIGPIPE,
&(struct sigaction){.sa_handler = SIG_IGN, .sa_flags = 0},
NULL)) {
log_err("Failed to install sigal handler for SIGPIPE");
return 0;
}
int listenfd = open_listenfd(PORT);
int rc UNUSED = sock_set_non_blocking(listenfd);
assert(rc == 0 && "sock_set_non_blocking");
/* create epoll and add listenfd */
int epfd = epoll_create1(0 /* flags */);
assert(epfd > 0 && "epoll_create1");
struct epoll_event *events = malloc(sizeof(struct epoll_event) * MAXEVENTS);
assert(events && "epoll_event: malloc");
http_request_t *request = malloc(sizeof(http_request_t));
init_http_request(request, listenfd, epfd, WEBROOT);
struct epoll_event event = {
.data.ptr = request,
.events = EPOLLIN | EPOLLET,
};
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event);
timer_init();
printf("Web server started.\n");
while(1) {
...
/* epoll wait loop */
...
}
}
```
將程式依序解析 :
```c=6
if (sigaction(SIGPIPE,
&(struct sigaction){.sa_handler = SIG_IGN, .sa_flags = 0},
NULL)) {
log_err("Failed to install sigal handler for SIGPIPE");
return 0;
}
```
* `int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);`
當 process 收到 specific signal 就改變 action。
```c=13
open_listenfd(int port)
```
* 將 `bind`,`listen` 包裝而成的函式,若 `error` 則回傳 `-1`
```c=14
int rc UNUSED = sock_set_non_blocking(listenfd);
assert(rc == 0 && "sock_set_non_blocking");
```
* 將 `fd` 設為 `non_blocking` 狀態,若 `rc == 0` 代表設置成功,程式往下執行
```c=18
int epfd = epoll_create1(0 /* flags */);
assert(epfd > 0 && "epoll_create1");
```
* `epoll_create1(flag = 0)` 等效於 `epoll_create`
* 建立 `epoll` ,若 `epfd > 0` 代表建立成功,程式往下執行
```c=21
struct epoll_event *events = malloc(sizeof(struct epoll_event) * MAXEVENTS);
assert(events && "epoll_event: malloc");
```
* 分配 `epoll_event` 空間,透過 `*events` 指向空間位址
* 若空間分配成功, `events != NULL` ,程式往下執行
```c=24
http_request_t *request = malloc(sizeof(http_request_t));
init_http_request(request, listenfd, epfd, WEBROOT);
struct epoll_event event = {
.data.ptr = request,
.events = EPOLLIN | EPOLLET,
};
timer_init();
```
* 分配 `http_request_t` 空間並初始化
* 設定監測事件為 `request`
* `EPOLLIN` : 允許 `read() operations`
* `EPOLLET` : 偵測發生條件為 `edge-triggered`
**epoll_wait loop**
```c=37
while (1) {
int time = find_timer();
debug("wait time = %d", time);
int n = epoll_wait(epfd, events, MAXEVENTS, time);
handle_expired_timers();
for (int i = 0; i < n; i++) {
http_request_t *r = events[i].data.ptr;
int fd = r->fd;
if (listenfd == fd) {
/* we hava one or more incoming connections */
while (1) {
socklen_t inlen = 1;
struct sockaddr_in clientaddr;
int infd = accept(listenfd, (struct sockaddr *) &clientaddr,
&inlen);
if (infd < 0) {
if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {
/* we have processed all incoming connections */
break;
}
log_err("accept");
break;
}
rc = sock_set_non_blocking(infd);
assert(rc == 0 && "sock_set_non_blocking");
request = malloc(sizeof(http_request_t));
if (!request) {
log_err("malloc");
break;
}
init_http_request(request, infd, epfd, WEBROOT);
event.data.ptr = request;
event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
epoll_ctl(epfd, EPOLL_CTL_ADD, infd, &event);
add_timer(request, TIMEOUT_DEFAULT, http_close_conn);
}
} else {
if ((events[i].events & EPOLLERR) ||
(events[i].events & EPOLLHUP) ||
(!(events[i].events & EPOLLIN))) {
log_err("epoll error fd: %d", r->fd);
close(fd);
continue;
}
do_request(events[i].data.ptr);
}
}
}
```
將程式依序解析 :
```c=38
int time = find_timer();
debug("wait time = %d", time);
int n = epoll_wait(epfd, events, MAXEVENTS, time);
handle_expired_timers();
```
* 在沒有任何連線進入下 `time` 會設為 `-1` ,也就是 `block` 住直到有事件就緒
* 一旦有事件就緒,回傳 `n` 就緒數量,並進入 `for` 迴圈處理
```c=44
http_request_t *r = events[i].data.ptr;
int fd = r->fd;
if (listenfd == fd){
while (1) {
...
}
}
```
* 記錄下 `event[i]` 中, `request` 的資訊,並在後續做出 `respond`
* 若 `listenfd == fd` ,代表 `I/O` 事件為新的連線進入要求 `accept`
```c=49
socklen_t inlen = 1;
struct sockaddr_in clientaddr;
int infd = accept(listenfd, (struct sockaddr *) &clientaddr,
&inlen);
if (infd < 0) {
if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {
/* we have processed all incoming connections */
break;
}
log_err("accept");
break;
}
```
* 接受 `client` 端連線,若 `infd < 0` 代表連線失敗, `errno` 將記錄錯誤事件發生
* [errno](https://man7.org/linux/man-pages/man3/errno.3.html) 回報連線錯誤 :
> EAGAIN - Resource temporarily unavailable (may be the same value as EWOULDBLOCK)
```c=62
rc = sock_set_non_blocking(infd);
assert(rc == 0 && "sock_set_non_blocking");
request = malloc(sizeof(http_request_t));
if (!request) {
log_err("malloc");
break;
}
init_http_request(request, infd, epfd, WEBROOT);
event.data.ptr = request;
event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
epoll_ctl(epfd, EPOLL_CTL_ADD, infd, &event);
```
* 一但 `client` 連線成功,將其 `I/O` 狀態設為 `non_blocking`
* 分配 `http_request_t` 空間給這個連線使用,並將 `request` 加入 `epoll` 監控事件
* `EPOLLONESHOT` 允許 `socket` 在處理完這個事件後,還能繼續處理其他事件的發生
```c=87
do_request(events[i].data.ptr);
```
* 針對每個 `fd` 狀態去處理
* Flow chart
![](https://i.imgur.com/mjC9lQW.jpg =295x720)
---
## II. [開發紀錄](https://hackmd.io/@jT29vQ4-SxmlBU5UMj1TGA/sehttpd)
---
## III. 問題與討論
1. 無法直接在 20.04 LTS 上按照 H10: sehttpd 步驟安裝所需套件,原因是因為 bcc-tools 無法在直接在 20.04 LTS 上安裝。