用基于epoll的reactor网络模型实现http server

用基于epoll的reactor网络模型实现http server(web server)

今天来实现一下 C++ 选手人手一个的 web server,这个其实很简单,一共就两层实现,下层是网络 io 的实现,这次使用基于 epoll 的 reactor 网络模型,协议层为 http,当然也可以称为业务层,只是更多的就是协议而已

网络层的代码就用上次实现的 reactor 模型,稍微修改即可,要实现 http server,核心就是实现 http_request 和 http_response,分别是请求和响应,实现了这两个,也就可以在此基础上实现大部分 http 业务

首先写一个 server.h 的头文件,主要是把 reactoc.c 和 webserver.c 的公共部分提取出来,在 conn 类中增加一个 status 变量,在后面实现状态机的时候有用

#ifndef __SERVER_H__
#define __SERVER_H__

#define BUFFER_LENGTH 1024

typedef int (*RCALLBACK)(int fd);

struct conn
{
    int fd;

    char rbuffer[BUFFER_LENGTH];
    int rlength;
    char wbuffer[BUFFER_LENGTH];
    int wlength;

    RCALLBACK send_callback;

    union
    {
        RCALLBACK recv_callback;
        RCALLBACK accept_callback;
    } r_action;

    int status;
};

int http_request(struct conn *c);
int http_response(struct conn *c);

#endif

http_request 和 http_response 分别对应 reactor 中的 recv_cb 和 send_cv,recv_cb()函数修改如下:

int recv_cb(int fd)
{
    memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH);
    int count = recv(fd, conn_list[fd].rbuffer, BUFFER_LENGTH, 0);
    if (count == 0)
    {
        printf("client disconnect: %d\n", fd);
        close(fd);
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
        return 0;
    }
    else if (count < 0)
    {
        printf("count: %d, errno: %d, %s\n", count, errno, strerror(errno));
        close(fd);
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
        return 0;
    }
    conn_list[fd].rlength = count;

    http_request(&conn_list[fd]); // 增加的部分

    set_event(fd, EPOLLOUT, 0);
    return count;
}

增加的http_request()函数实现如下:

int http_request(struct conn *c) {
	memset(c->wbuffer, 0, BUFFER_LENGTH);
	c->wlength = 0;
	c->status = 0;
}

主要就是初始化缓冲区,将 status 设为 0

本次实现的 webserver 比较简单,主要就是实现可以显示字符、文件和照片

(1)显示字符

这个比较简单,有基本的 http 知识即可,代码如下,具体的 http 基础知识后续会更新

int http_response(struct conn *c) {
    c->wlength = sprintf(c->wbuffer, 
		"HTTP/1.1 200 OK\r\n"
		"Content-Type: text/html\r\n"
		"Accept-Ranges: bytes\r\n"
		"Content-Length: 91\r\n"
		"Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n"
		"zcx-create

zcx-create

\r\n\r\n"
); return c->wlength; }

测试效果如下:

用基于epoll的reactor网络模型实现http server_第1张图片

(2)显示文件/图片

文件和图片可能会因为过大而需要多次发送,这里需要实现状态机,代码如下:

// webserver.c
int http_response(struct conn *c) {
    int filefd = open("index.html", O_RDONLY);

	struct stat stat_buf;
	fstat(filefd, &stat_buf);
	
	if (c->status == 0) {
		c->wlength = sprintf(c->wbuffer, 
			"HTTP/1.1 200 OK\r\n"
			"Content-Type: text/html\r\n"
			"Accept-Ranges: bytes\r\n"
			"Content-Length: %ld\r\n"
			"Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n", 
			stat_buf.st_size);

		c->status = 1;
	} else if (c->status == 1) {
		int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size);
		if (ret == -1) {
			printf("errno: %d\n", errno);
		}
        
		c->status = 2;
	} else if (c->status == 2) {
		c->wlength = 0;
		memset(c->wbuffer, 0, BUFFER_LENGTH);
        
		c->status = 0;
	}
	close(filefd);
    return c->wlength;
}

// reactor.c中的send_cb函数
int send_cb(int fd)
{
    http_response(&conn_list[fd]);
    
    int count = 0;
    if (conn_list[fd].status == 1)
    {
        // printf("SEND: %s\n", conn_list[fd].wbuffer);
        count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
        set_event(fd, EPOLLOUT, 0);
    }
    else if (conn_list[fd].status == 2)
    {
        set_event(fd, EPOLLOUT, 0);
    }
    else if (conn_list[fd].status == 0)
    {
        if (conn_list[fd].wlength != 0)
        {
            count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
        }
        set_event(fd, EPOLLIN, 0);
    }

    return count;
}

下面我们详解整个流程:

(1)进入 send_cb 函数

​ 当 send_cb 函数被调用时,它首先调用 http_response 函数来准备或发送HTTP响应。

(2)http_response 函数处理

​ ①状态 0 (c->status == 0)

​ 函数行为:函数打开“index.html”文件,获取文件大小,并准备HTTP响应头

​ 状态变更:状态从0变更为1,预备发送文件内容。
(3)返回到 send_cb

​ 回到 send_cb:返回到 send_cb 函数,此时状态已变为1。

​ 发送头部:检查状态为1,然后发送准备好的HTTP响应头(存储在 c->wbuffer 中)。

​ 设置事件:设置socket为EPOLLOUT,表示关注可写事件,系统会在socket可写时再次调用send_cb(重点)。

(4)再次进入 send_cb 函数

​ socket已准备好写入,send_cb 函数将再次被调用。

​ ①状态 1 (c->status == 1)

​ 函数行为:此时 http_response 会被再次调用。

​ 发送文件数据:http_response 中的 sendfile 将文件内容发送到网络。状态从1变更为2,表示数据已发送。

​ ②返回到 send_cb

​ 回到 send_cb:返回到 send_cb 函数,此时状态为2。

​ 设置事件:设置socket为EPOLLOUT,期待socket能够继续处理更多的写入操作。

(5)再次进入 send_cb 函数

​ ①状态 2 (c->status == 2)

​ 函数行为:http_response 再次被调用。

​ 清理:清空写缓冲区,状态重置为0,等待新的请求或关闭连接。

​ ②返回到 send_cb

​ 回到 send_cb:如果写缓冲区内容非空(通常是0),可能发生发送剩余数据。

​ 设置事件:设置socket为EPOLLIN,表示关注可读事件,系统在有新数据到来时将调用相应的读取回调函数。

测试结果如下:

显示文件

在这里插入图片描述

显示照片

最后再总结一下,webserver的核心代码就只有400行不到,在此基础上可以实现大多数的http请求功能,这个烂大街的C++项目之所以不受到面试官的欢迎,会因为确实比较简单,并且很多时候都不能称之为项目,更像是个demo,就算是网络层的实现,除了epoll之外,还可以用协程,io_uring,基于dpdk的用户态协议栈等,这些内容后续都会更新,将http认为是协议层,在此基础上比如说实现问卷调查等业务功能,才算是一个完整的项目

因为代码不长,就直接放在这里

// reactor.c
#include "server.h"

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define CONNECTION_SIZE 1024

int accept_cb(int fd);
int recv_cb(int fd);
int send_cb(int fd);

int epfd = 0;

struct conn conn_list[CONNECTION_SIZE] = {0};

int set_event(int fd, int event, int flag)
{
    if (flag)
    {
        struct epoll_event ev;
        ev.events = event;
        ev.data.fd = fd;
        epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
    }
    else
    {
        struct epoll_event ev;
        ev.events = event;
        ev.data.fd = fd;
        epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
    }
}

int event_register(int fd, int event)
{
    if (fd < 0)
        return -1;
    conn_list[fd].fd = fd;
    conn_list[fd].r_action.recv_callback = recv_cb;
    conn_list[fd].send_callback = send_cb;

    memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH);
    conn_list[fd].rlength = 0;

    memset(conn_list[fd].wbuffer, 0, BUFFER_LENGTH);
    conn_list[fd].wlength = 0;

    set_event(fd, event, 1);
}

int accept_cb(int fd)
{
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);

    int clientfd = accept(fd, (struct sockaddr *)&clientaddr, &len);
    printf("accept finished: %d\n", clientfd);

    if (clientfd < 0)
    {
        printf("accept errno: %d --> %s\n", errno, strerror(errno));
        return -1;
    }
    event_register(clientfd, EPOLLIN);
    return 0;
}

int recv_cb(int fd)
{
    memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH);
    int count = recv(fd, conn_list[fd].rbuffer, BUFFER_LENGTH, 0);
    if (count == 0)
    {
        printf("client disconnect: %d\n", fd);
        close(fd);
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
        return 0;
    }
    else if (count < 0)
    {
        printf("count: %d, errno: %d, %s\n", count, errno, strerror(errno));
        close(fd);
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
        return 0;
    }
    conn_list[fd].rlength = count;

    http_request(&conn_list[fd]);

    set_event(fd, EPOLLOUT, 0);
    return count;
}

int send_cb(int fd)
{
    http_response(&conn_list[fd]);
    
    int count = 0;
    if (conn_list[fd].status == 1)
    {
        // printf("SEND: %s\n", conn_list[fd].wbuffer);
        count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
        set_event(fd, EPOLLOUT, 0);
    }
    else if (conn_list[fd].status == 2)
    {
        set_event(fd, EPOLLOUT, 0);
    }
    else if (conn_list[fd].status == 0)
    {
        if (conn_list[fd].wlength != 0)
        {
            count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
        }
        set_event(fd, EPOLLIN, 0);
    }

    return count;
}

int init_server(unsigned short port)
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(port);

    if (-1 == bind(sockfd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr)))
    {
        printf("bind failed: %s\n", strerror(errno));
    }

    listen(sockfd, 10);
    printf("listen finshed: %d\n", sockfd);
    return sockfd;
}

int main()
{
    unsigned short port = 2000;
    epfd = epoll_create(1);
    int sockfd = init_server(port);
    conn_list[sockfd].fd = sockfd;
    conn_list[sockfd].r_action.recv_callback = accept_cb;
    set_event(sockfd, EPOLLIN, 1);

    while (1)
    {
        struct epoll_event events[1024] = {0};
        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)
                conn_list[connfd].r_action.recv_callback(connfd);
            if (events[i].events & EPOLLOUT)
                conn_list[connfd].send_callback(connfd);
        }
    }
}
// webserver.c
#include "server.h"

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define WEBSERVER_ROOTDIR	"./"

int http_request(struct conn *c) {
	memset(c->wbuffer, 0, BUFFER_LENGTH);
	c->wlength = 0;
	c->status = 0;
}

int http_response(struct conn *c) {
// 显示字符
#if 1
	c->wlength = sprintf(c->wbuffer, 
		"HTTP/1.1 200 OK\r\n"
		"Content-Type: text/html\r\n"
		"Accept-Ranges: bytes\r\n"
		"Content-Length: 91\r\n"
		"Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n"
		"zcx-create

zcx-create

\r\n\r\n"
); // 显示文件 #elif 0 int filefd = open("index.html", O_RDONLY); struct stat stat_buf; fstat(filefd, &stat_buf); c->wlength = sprintf(c->wbuffer, "HTTP/1.1 200 OK\r\n" "Content-Type: text/html\r\n" "Accept-Ranges: bytes\r\n" "Content-Length: %ld\r\n" "Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n", stat_buf.st_size); int count = read(filefd, c->wbuffer + c->wlength, BUFFER_LENGTH - c->wlength); c->wlength += count; close(filefd); // 显示文件/图片(多次发送) #else int filefd = open("index.html", O_RDONLY); struct stat stat_buf; fstat(filefd, &stat_buf); if (c->status == 0) { c->wlength = sprintf(c->wbuffer, "HTTP/1.1 200 OK\r\n" "Content-Type: text/html\r\n" "Accept-Ranges: bytes\r\n" "Content-Length: %ld\r\n" "Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n", stat_buf.st_size); c->status = 1; } else if (c->status == 1) { int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size); if (ret == -1) { printf("errno: %d\n", errno); } c->status = 2; } else if (c->status == 2) { c->wlength = 0; memset(c->wbuffer, 0, BUFFER_LENGTH); c->status = 0; } close(filefd); #endif return c->wlength; }

更多资料可以查看:学习资料

你可能感兴趣的:(网络,网络,http)