如何基於Reactor網路模型實現業務並測試效能
如何基於Reactor網路模型實現業務並測試效能
對於實作一個http服務來講,一個http請求正常情況下可以分成request和response兩部分,我們可以隨便開啟一個網頁,例如:
圖片
我們可以看到,一個http請求,包含Request Header和Response Header,針對HTTP協定的這個特點,我們就可以抽像出http_request和http_response兩個方法,它們分別用來處理HTTP的請求和回應。
那麼,最終的流程是從recv_callback接收到資料包,然後呼叫http_request方法解析HTTP協議,處理對應的業務邏輯,處理完之後將回應結果透過http_response寫入到wbuf中,然後觸發EPOLLOUT事件,最終呼叫send_callback將會呼叫wbuf中的資料傳送出去,流程大致如下:
圖片
下面我們就來實作http_request和http_response,程式碼如下:
int http_request(connection_t *conn) {
// TODO parse http headr and body
}
int http_response(connection_t *conn) {
conn->wlen = sprintf(conn->wbuf,
"HTTP/1.1 200 OK\\r\\n"
"Accept-Ranges: bytes\\r\\n"
"Content-Length: 47\\r\\n"
"Content-Type: text/html\\r\\n"
"Date: Sta, 06 Aug 2023 13:16:46 GMT\\r\\n\\r\\n"
"<html><body><h1>Hello Server</h1></body></html>");
return conn->wlen;
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
這裡省略了http_request中的程式碼,本文要關注的重點是如何在基於事件的網路模型中插入業務程式碼,你可能會覺得迷糊,HTTP服務不是也是基礎服務嗎?怎麼能叫業務代碼呢?這是相對而言的,對於TCP協定來說,HTTP其實只是TCP的一個應用,當然算是業務代碼了。
上面程式碼中定義了一個connection_t類型的參數,實際上這個參數的原型就是conn_channel,其定義如下:
typedef struct conn_channel connection_t;
- 1.
到這裡,不知道你有沒有一種感覺,對於上層的業務代碼來講,它並不關心下層的網絡IO,我們在http_request和http_reponse中並沒有看到直接操作網絡IO的地方,它們只管從對應的rbuf和wbuf中讀寫,至於資料是怎麼收進來的,又是怎麼發送出去的並不關心。上一篇文章我們講到網路IO的分離,透過這篇文章相信你會有一個更直觀的理解。
我將完整的程式碼直接貼在下面了
#include <sys/socket.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <error.h>
#define BUFFER_LEN 1024
int epfd = 0;
typedef int(*callback)(int);
struct conn_channel {
int fd;
char wbuf[BUFFER_LEN];
int wlen;
char rbuf[BUFFER_LEN];
int rlen;
union {
callback recv_call;
callback accept_call;
} call_t;
callback send_call;
};
struct conn_channel conn_map[1024] = {0};
typedef struct conn_channel connection_t;
int create_serv(int port) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(struct sockaddr_in));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(port);
int on = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
int ret = bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
if (ret == -1) {
perror("socket-bind-fail");
return -1;
}
listen(sockfd, 1024);
return sockfd;
}
void make_nonblocking(int fd) {
fcntl(fd, F_SETFL, O_NONBLOCK);
}
int http_request(connection_t *conn) {
// TODO parse http headr and body
}
int http_response(connection_t *conn) {
conn->wlen = sprintf(conn->wbuf,
"HTTP/1.1 200 OK\\r\\n"
"Accept-Ranges: bytes\\r\\n"
"Content-Length: 47\\r\\n"
"Content-Type: text/html\\r\\n"
"Date: Sta, 06 Aug 2023 13:16:46 GMT\\r\\n\\r\\n"
"<html><body><h1>Hello Server</h1></body></html>");
return conn->wlen;
}
int recv_callback(int fd) {
char *buffer = conn_map[fd].rbuf;
int idx = conn_map[fd].rlen;
int count = recv(fd, buffer+idx, BUFFER_LEN - idx, 0);
if (count == 0) {
printf("discounnect: %d\\n", fd);
return -1;
}
conn_map[fd].rlen = count;
// do http request and response
http_request(&conn_map[fd]);
http_response(&conn_map[fd]);
struct epoll_event ev;
ev.events = EPOLLOUT;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
return count;
}
int send_callback(int fd) {
int count = send(fd, conn_map[fd].wbuf, conn_map[fd].wlen, 0);
printf("send-count:%d\\n", count);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
return count;
}
int accept_callback(int fd) {
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = clientfd;
conn_map[clientfd].wlen = 0;
conn_map[clientfd].rlen = 0;
conn_map[clientfd].call_t.recv_call = recv_callback;
conn_map[clientfd].send_call = send_callback;
memset(conn_map[clientfd].wbuf, 0, BUFFER_LEN);
memset(conn_map[clientfd].rbuf, 0, BUFFER_LEN);
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
return clientfd;
}
int main() {
int sockfd = create_serv(2048);
if (sockfd == -1) {
perror("sockfd-create-fail");
return -1;
}
make_nonblocking(sockfd);
epfd = epoll_create1(0);
printf("epfd:%d, sockfd: %d\\n", epfd, sockfd);
conn_map[sockfd].wlen = 0;
conn_map[sockfd].rlen = 0;
conn_map[sockfd].call_t.recv_call = accept_callback;
conn_map[sockfd].send_call = send_callback;
memset(conn_map[sockfd].wbuf, 0, BUFFER_LEN);
memset(conn_map[sockfd].rbuf, 0, BUFFER_LEN);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
struct epoll_event events[1024] = {0};
while(1) {
int nready = epoll_wait(epfd, events, 1024, -1);
int i = 0;
for (i = 0; i < nready; i++) {
int connfd = events[i].data.fd;
if (events[i].events & EPOLLIN) {
int ret = conn_map[connfd].call_t.recv_call(connfd);
printf("epollin-ret: %d\\n", ret);
} else if (events[i].events & EPOLLOUT) {
int count = conn_map[connfd].send_call(connfd);
printf("send-count: %d\\n", count);
}
}
}
}
- 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.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
上面的程式碼200行不到,確包含了Reactor網路模型的核心思想,以及實作方式,如果有興趣你可以基於此進一步擴展,你也可以參考我們前面網路程式設計系列文章,有更完整的實作。
程式碼寫完了,效能究竟如何我們需要進一步驗證,這裡推薦一個效能測試工具wrk,wrk是一款開源的效能測試工具,使用C實現,位址:https://github.com/wg/wrk,我準備了兩台機器,配置如下:
ubuntu20.4 8C8G 192.168.56.2
ubuntu20.4-1 4C4G 192.168.56.3
我們的服務跑在8C8G這台機器上,另外一台4C4G的機器用於運行wrk工具進行性能測試。
為了有一個對比,我們先在8C8G的機器上安裝一個Nginx用來作對比,版本是1.25.4
圖片
然後我們在4C4G的機器上將wrk編譯好,下載好的wrk目錄架構如下:
圖片
可以看到已經有寫好的Makefile了,我們只要make一下就可以了,make完之後會編譯出一個wrk的二進位文件
圖片
接著,我們將8C8G這台機器上的Nginx和我們剛剛寫的httpserver都啟動起來,我們的nginx運行在80端口,httpserver運行在2048端口。
接著,我們在4C4G機器上執行wrk先測試nginx,如下:
圖片
接著測試我們自己寫的httpserver
圖片
我們使用wrk開啟50個線程,100個並發持續10秒分別對Nginx和我們自己的httpserver進行了測試,最終的結果我們可以看到,在Nginx沒有任何優化的情況下,我們寫的httpserver明顯比Nginx性能更好。當然,Nginx實際上寫了很多的日誌,我們的httpserver幾乎沒有寫什麼日誌,你可以自己嘗試將Nginx日誌關了再對比一下看看結果。
總結
這篇文章我們透過實作了一個簡單的HTTP服務來說明如何將Reactor網路模型應用到業務中去,這是在學校和網路上10小時入門C語言裡不會講的,但它又非常重要。
在學習C/C++的過程中,相信很多人都會有這麼一種感覺,那就是C/C++語法說起來都會,但就是很難寫出一個完整的項目,個人覺得這是目前國內整個IT教育界的失敗,大部分的課程花了非常大的篇幅講if-else,甚至將各種變數類型都講出花來了,但就是不告訴你一個完整的專案該如何寫。