如何基于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;
}

这里省略了http_request中的代码,本文要关注的重点是如何在基于事件的网络模型中插入业务代码,你可能会觉得迷糊,HTTP服务不是也是基础服务吗?怎么能叫业务代码呢?这是相对而言的,对于TCP协议来说,HTTP实际上只是TCP的一个应用,当然算得上是业务代码了。

上面代码中定义了一个connection_t类型的参数,实际上这个参数的原型就是conn_channel,其定义如下:

typedef struct conn_channel connection_t;

到这里,不知道你有没有一种感觉,对于上层的业务代码来讲,它并不关心下层的网络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);
            }
        }
    }
}

上面的代码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,甚至将各种变量类型都讲出花来了,但就是不告诉你一个完整的项目该如何写。

责任编辑:武晓燕 来源: 程序员班吉
相关推荐

2024-08-16 21:30:00

IO网络网络通信

2023-12-05 17:44:24

reactor网络

2024-04-18 09:34:28

Reactor项目异步编程

2019-01-15 10:54:03

高性能ServerReactor

2020-12-01 07:08:23

Linux网络I

2023-09-13 14:45:14

性能测试开发

2011-05-16 14:13:04

模型测试

2021-09-21 09:01:19

网络安全疫情数据

2009-12-30 10:31:04

配置NAT网络

2016-11-07 18:26:39

IT可视化

2021-06-02 10:00:30

云网络性能测试

2009-03-27 09:59:00

2022-05-17 08:53:26

TPS性能测试

2024-04-15 13:51:03

模型LLMLLMs

2022-01-04 11:11:32

Redis单线程Reactor

2020-06-17 16:38:22

Rust业务架构

2012-05-08 13:36:55

2012-07-27 15:28:09

SingleSON华为

2018-09-05 09:32:42

高性能网络模型

2022-05-26 10:12:21

前端优化测试
点赞
收藏

51CTO技术栈公众号