如何基於Reactor網路模型實現業務並測試效能

2023.12.12

如何基於Reactor網路模型實現業務並測試效能

這篇文章我們透過實作了一個簡單的HTTP服務來說明如何將Reactor網路模型應用到業務中去,這是在學校和網路上10小時入門C語言裡不會講的,但它又非常重要。

對於實作一個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,甚至將各種變數類型都講出花來了,但就是不告訴你一個完整的專案該如何寫。