Linux C/C++ 学习日记(22):Reactor模式(二):实现简易的webserver(响应http请求)

注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。

一、将之前写的reactor模式调整一下封装成库,记为reactor.h

cpp 复制代码
#ifndef __REACTOR_H__
#define __REACTOR_H__

#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <sys/epoll.h>
#include <errno.h>
#include <sys/time.h>
#include <stdlib.h>
#include <fcntl.h>

typedef int (*RCALLBACK)(int fd);

typedef struct conn
{
    int fd;
    char *rbuffer;
    int rlength;

    char *wbuffer;
    int wlength;

#if 1
    // 非阻塞模式变量:接收缓冲区
    char *rlbuffer; // 动态接收缓冲区(拼接所有分片数据)
    int rllength;   // 动态接收缓冲区的实际总长度(避免依赖strlen)

    // 非阻塞模式变量:发送缓冲区
    char *wlbuffer; // 动态发送缓冲区(待发送的完整数据)
    int wllength;   // 动态发送缓冲区的总长度(实际字节数)
    int w_sent;     // 已发送的字节数(跟踪发送进度)

#endif
    RCALLBACK in_callback;
    RCALLBACK out_callback;

} conn;

static int epfd_ = 0;    // 全局变量,方面设置事件,如果用于多线程记得加锁。static 修饰后,仅当前文件可访问
static conn *conn_list_; // static 修饰后,仅当前文件可访问
int init_epfd()
{
    epfd_ = epoll_create(1); // epoll_create只能在程序执行时能调用,这里封装成函数
    return epfd_;
}

conn *get_connlist(int n);

int set_event(int fd, int event, int flag);
int conn_register(int fd, int buffer_length, RCALLBACK in_callback, RCALLBACK out_callback, int flag);
int setNonblock(int fd);
int setReUseAddr(int fd);
int conn_close(int fd);
int close_connlist(conn *clist, int n);

int setNonblock(int fd)
{
    int flags;

    flags = fcntl(fd, F_GETFL, 0);
    if (flags < 0)
        return flags;
    flags |= O_NONBLOCK;
    if (fcntl(fd, F_SETFL, flags) < 0)
        return -1;
    return 0;
}

int setReUseAddr(int fd)
{
    int reuse = 1;
    return setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
}

int set_event(int fd, int event, int flag)
{
    struct epoll_event ev;
    ev.events = event;
    ev.data.fd = fd;

    if (flag == 0) // 还未加入epfd
    {
        epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev);
    }

    else // 已经加入了
    {
        epoll_ctl(epfd_, EPOLL_CTL_MOD, fd, &ev);
    }

    return 0;
}

conn *get_connlist(int n)
{
    conn_list_ = malloc(sizeof(conn) * n);
    memset(conn_list_, 0, sizeof(conn) * n);
    return conn_list_;
}

int conn_register(int fd, int buffer_length, RCALLBACK in_callback, RCALLBACK out_callback, int flag)
{

    conn_list_[fd].fd = fd;

    
    conn_list_[fd].rlength = 0;
    conn_list_[fd].wlength = 0;

    conn_list_[fd].in_callback = in_callback;
    conn_list_[fd].out_callback = out_callback;

    conn_list_[fd].rbuffer = conn_list_[fd].wbuffer = NULL;

    // 给阻塞模式使用的固定长度的buffer分配空间
    if (flag == 1)
    {
        conn_list_[fd].rbuffer = malloc(sizeof(char) * buffer_length);
        memset(conn_list_[fd].rbuffer, 0, buffer_length);

        conn_list_[fd].wbuffer = malloc(sizeof(char) * buffer_length);
        memset(conn_list_[fd].wbuffer, 0, buffer_length);
    }

#if 1
    // 初始化非阻塞模式变量
    conn_list_[fd].rlbuffer = NULL;
    conn_list_[fd].rllength = 0;
    conn_list_[fd].wlbuffer = NULL;
    conn_list_[fd].wllength = 0;
    conn_list_[fd].w_sent = 0;

#endif
    return 0;
}

int conn_close(int fd)
{
    if (conn_list_[fd].fd == 0)
    {
        return 0;
    }
    if (conn_list_[fd].rbuffer != NULL)
    {
        conn_list_[fd].rbuffer = NULL;
        free(conn_list_[fd].rbuffer);
    }
    if (conn_list_[fd].wbuffer != NULL)
    {
        conn_list_[fd].wbuffer = NULL;
        free(conn_list_[fd].wbuffer);
    }
    if (conn_list_[fd].rlbuffer != NULL)
    {
        conn_list_[fd].rlbuffer = NULL;
        free(conn_list_[fd].rlbuffer);
    }
    if (conn_list_[fd].wlbuffer != NULL)
    {
        conn_list_[fd].wlbuffer = NULL;
        free(conn_list_[fd].wlbuffer);
    }

    conn_list_[fd].fd = 0;
    memset(&conn_list_[fd], 0, sizeof(conn));
    epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, NULL);
    close(fd);
    return 0;
}

int close_connlist(conn *clist, int n)
{
    for (int i = 0; i < n; i++)
    {
        if (clist[i].fd != 0)
        {
            conn_close(clist[i].fd);
        }
    }

    free(clist);
    return 0;
}

int recv_noblock(int fd, int n_buffer_length)
{
    if (conn_list_[fd].rlbuffer == NULL)
    {
        conn_list_[fd].rlbuffer = malloc(sizeof(char)); // 初始分配1字节(避免realloc NULL问题)
        memset(conn_list_[fd].rlbuffer, 0, sizeof(char));
    }

    int rlen = 0;
    if (conn_list_[fd].rllength != 0)
    {
        rlen = conn_list_[fd].rllength;
    }
    char *buffer = malloc(n_buffer_length * sizeof(char));

    // 循环读取服务器响应
    while (1)
    {

        // 清空缓冲区,准备接收数据
        memset(buffer, 0, n_buffer_length);
        // 从套接字接收数据
        int count = recv(fd, buffer, n_buffer_length, 0);
        if (count == 0)
        {
            printf("client disconnect: %d\n", fd);
            conn_close(fd);
            return 0;
        }
        else if (count < 0)
        {
            if (errno == EAGAIN || errno == EWOULDBLOCK)
            {
                // 非阻塞下暂时无数据,返回已读字节数
                break;
            }
            else
            {
                // 真正的错误(如连接重置、中断)
                printf("recv errno: %d --> %s\n", errno, strerror(errno));
                conn_close(fd);
                return -1;
            }
        }

        else
        {
            char *new_rlbuffer = realloc(conn_list_[fd].rlbuffer, (rlen + count) * sizeof(char));
            if (new_rlbuffer == NULL)
            {
                printf("realloc failed\n");
                return -1;
            }

            conn_list_[fd].rlbuffer = new_rlbuffer;

            memcpy(conn_list_[fd].rlbuffer + rlen, buffer, count);
            rlen += count;
        }
    }
    free(buffer);

    conn_list_[fd].rllength = rlen;
    return rlen;
}

int reset_r_noblock(int fd)
{
    if (conn_list_[fd].rlbuffer != NULL)
    {
        free(conn_list_[fd].rlbuffer);
        conn_list_[fd].rlbuffer = NULL;
    }
    conn_list_[fd].rllength = 0;
    return 0;
}

int reset_w_noblock(int fd)
{
    if (conn_list_[fd].wlbuffer != NULL)
    {
        free(conn_list_[fd].wlbuffer);
        conn_list_[fd].wlbuffer = NULL;
    }
    conn_list_[fd].wllength = 0;
    conn_list_[fd].w_sent = 0;
    return 0;
}

int add_to_w_noblock(int fd, char *buffer, int length, int flag)
{
    if (buffer != NULL && length != 0)
    {
        if (flag == 1)
        {
            // 追加模式:在wlbuffer末尾追加rlbuffer内容
            if (conn_list_[fd].wlbuffer != NULL)
            {
                char *new_wlbuffer = realloc(conn_list_[fd].wlbuffer, (conn_list_[fd].wllength + length) * sizeof(char));
                if (new_wlbuffer == NULL)
                {
                    printf("realloc failed\n");
                    return -1;
                }
                conn_list_[fd].wlbuffer = new_wlbuffer;
            }
            else
            {

                conn_list_[fd].wlbuffer = malloc(length * sizeof(char));
                conn_list_[fd].wllength = 0;
                memset(conn_list_[fd].wlbuffer, 0, length * sizeof(char));
            }
            memcpy(conn_list_[fd].wlbuffer + conn_list_[fd].wllength, buffer, length);
            conn_list_[fd].wllength += length;
            return conn_list_[fd].wllength;
        }
        else
        {
            // 覆盖模式:清空wlbuffer内容,复制rlbuffer内容
            if (conn_list_[fd].wlbuffer != NULL)
            {
                free(conn_list_[fd].wlbuffer);
                conn_list_[fd].wlbuffer = NULL;
                conn_list_[fd].wllength = 0;
            }
            conn_list_[fd].wlbuffer = malloc(length * sizeof(char));
            conn_list_[fd].wllength = 0;
            memset(conn_list_[fd].wlbuffer, 0, length * sizeof(char));
            memcpy(conn_list_[fd].wlbuffer, buffer, length);
            conn_list_[fd].wllength = length;
            return conn_list_[fd].wllength;
        }
    }
    return -1;
}

int send_noblock(int fd)
{
    // 非阻塞模式

    char *wbuffer = conn_list_[fd].wlbuffer;
    int total_len = conn_list_[fd].wllength;
    int sent = conn_list_[fd].w_sent;

    // 防御:无数据可发送时直接切换回读事件
    if (wbuffer == NULL || total_len == 0)
    {
        set_event(fd, EPOLLIN | EPOLLET, 1);
        return 0;
    }

    // 循环发送剩余数据
    while (sent < total_len)
    {
        // 发送从"已发送位置"开始的剩余数据
        int nsend = send(fd, wbuffer + sent, total_len - sent, 0);

        if (nsend > 0)
        {
            // 成功发送部分数据,更新进度
            sent += nsend;
            conn_list_[fd].w_sent = sent; // 保存当前进度
        }
        else if (nsend == 0)
        {
            // 连接关闭(对方可能断开),清理资源
            printf("client disconnect during send: %d\n", fd);
            conn_close(fd);
            return -1;
        }
        else
        {
            // 发送错误处理
            if (errno == EAGAIN || errno == EWOULDBLOCK)
            {
                return sent;
            }
            else
            {
                // 其他错误(如连接重置),清理资源
                printf("send errno: %d --> %s\n", errno, strerror(errno));
                conn_close(fd);
                return -1;
            }
        }
    }
    // 所有数据发送完成
    return sent;

    return 0;
}
#endif

二、编写代码

1.测试一下库是否能正常调用(LT+阻塞模式):

在浏览器中

cpp 复制代码
#include "reactor.h"
#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <sys/epoll.h>
#include <errno.h>
#include <sys/time.h>
#include <stdlib.h>
#include <fcntl.h>

#define BUFFER_LENGTH 1024

#define CONNECTION_SIZE 100 // 1048576 // 1024 * 1024

#define MAX_PORTS 20

int accept_cb(int fd);
int recv_cb(int fd);
int send_cb(int fd);
int init_server(int sockfd, int port);

conn *conn_list;

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

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

    conn_register(clientfd, BUFFER_LENGTH, recv_cb, send_cb, 1);
    set_event(clientfd, EPOLLIN, 0);
}

int recv_cb(int fd)
{
    // 在浏览器输入 ip:port/path    以我为例: 192.168.248.130:8000/index.html
    // 服务器接收到:
    // GET /index.html HTTP/1.1
    // Host: 192.168.248.130:8000
    // Connection: keep-alive
    // Cache-Control: max-age=0
    // Upgrade-Insecure-Requests: 1
    // User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0
    // Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
    // Accept-Encoding: gzip, deflate
    // Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
    memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH);
    int count = recv(fd, conn_list[fd].rbuffer, BUFFER_LENGTH, 0);

    if (count <= 0)
    {
        conn_close(fd);

        printf("disconnect: %d\n", fd);
        return -1;
    }
    else
    {
        printf("len: %d\n", count);
        printf("RECV: %.*s\n", count, conn_list[fd].rbuffer);

        memset(conn_list[fd].wbuffer, 0, BUFFER_LENGTH);
        memcpy(conn_list[fd].wbuffer, conn_list[fd].rbuffer, count);
        conn_list[fd].wlength = count;
        set_event(fd, EPOLLOUT, 1);
    }
}

int send_cb(int fd)
{
    send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
    printf("SEND: %.*s\n", conn_list[fd].wlength, conn_list[fd].wbuffer);
    set_event(fd, EPOLLIN, 1);
}

int main()
{
    conn_list = get_connlist(CONNECTION_SIZE);
    int epfd = init_epfd();
    printf("epfd: %d\n", epfd);

    struct epoll_event *events = malloc(sizeof(struct epoll_event) * CONNECTION_SIZE);

    int socketfd = socket(AF_INET, SOCK_STREAM, 0);
    init_server(socketfd, 8000);
    set_event(socketfd, EPOLLIN, 0);
    conn_register(socketfd, BUFFER_LENGTH, accept_cb, NULL, 0);

    while (1)
    {
        int nready = epoll_wait(epfd, events, CONNECTION_SIZE, -1);

        for (int i = 0; i < nready; i++)
        {
            int fd = events[i].data.fd;
            if (events[i].events & EPOLLIN)
            {
                conn_list[fd].in_callback(fd);
            }

            if (events[i].events & EPOLLOUT)
            {
                conn_list[fd].out_callback(fd);
            }
        }
    }

    close(epfd);
    free(events);
    close_connlist(conn_list, CONNECTION_SIZE);
    return 0;
}

int init_server(int sockfd, int port)
{
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(port);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    setReUseAddr(sockfd);
    bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    listen(sockfd, 5);
   

    printf("listenfd: %d\n", sockfd);
}

运行代码,在浏览器输入 ip:port/path 以我为例: 192.168.248.130:8000/index.html

有结果:

但是也出现过以下这种结果:

发现一次接收不完(后续收到了.6),观察发现len=512(我们的BUFFER_LENGTH为1024),说明不是我们接收方的问题,而是浏览器分块发送数据了,我们读完一次的时候可能浏览器还没发送第二块,导致收到的http请求不完整。

要想解决该问题,我们可以依据http协议的请求特征,对收到的http请求做一次完整度的判断!,不完整就再执行一次读事件,由于涉及到数据的拼接,我们采用ET+非阻塞模式

2. 优化:采用ET+非阻塞模式(接收浏览器分块发送的http请求)

每次接收完都做一次http请求完整性的判断:is_http_complete

cpp 复制代码
#include "reactor.h"
#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <sys/epoll.h>
#include <errno.h>
#include <sys/time.h>
#include <stdlib.h>
#include <fcntl.h>

#define BUFFER_LENGTH 1024

#define CONNECTION_SIZE 100 // 1048576 // 1024 * 1024

#define MAX_PORTS 20

int accept_cb(int fd);
int recv_cb(int fd);
int send_cb(int fd);
int init_server(int sockfd, int port);

conn *conn_list;

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

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

    conn_register(clientfd, BUFFER_LENGTH, recv_cb, send_cb, 0);
    set_event(clientfd, EPOLLIN | EPOLLET, 0);
#if 1
    setNonblock(clientfd); // 设置非阻塞
#endif
    return 0;
}

int is_http_complete(const char *buf, int len)
{

    // 确保缓冲区长度至少能容纳 "\r\n\r\n"(4 字节)
    if (len >= 4)
    {
        // 定位到末尾 4 字节的起始位置
        const char *end = buf + len - 4;
        // 检查这 4 字节是否严格匹配 "\r\n\r\n"
        printf("检查结尾4字节: %.*s\n", 4, end);
        if (end[0] == '\r' && end[1] == '\n' &&
            end[2] == '\r' && end[3] == '\n')
        {
            return 1; // 请求头完整
        }
    }
    return 0; // 不完整,继续等待
}

int recv_cb(int fd)
{
    // 在浏览器输入 ip:port/path    以我为例: 192.168.248.130:8000/index.html
    // 服务器接收到:
    // GET /index.html HTTP/1.1
    // Host: 192.168.248.130:8000
    // Connection: keep-alive
    // Cache-Control: max-age=0
    // Upgrade-Insecure-Requests: 1
    // User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0
    // Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
    // Accept-Encoding: gzip, deflate
    // Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

    // 处理非阻塞接收
    if (recv_noblock(fd, BUFFER_LENGTH) <= 0)
    {
        return 0;
    }

    if (is_http_complete(conn_list[fd].rlbuffer, conn_list[fd].rllength))
    {
        // 请求头完整,准备发送响应
        printf("请求头完整:\n");
        printf("recvLen: %d\n", conn_list[fd].rllength);
        printf("RECV: %.*s\n", conn_list[fd].rllength, conn_list[fd].rlbuffer);

        add_to_w_noblock(fd, conn_list[fd].rlbuffer, conn_list[fd].rllength, 0);

        reset_r_noblock(fd);
        set_event(fd, EPOLLOUT | EPOLLET, 1);
    }

    else
    {
        printf("请求头不完整,继续等待更多数据:\n");
        printf("recvLen: %d\n", conn_list[fd].rllength);
        printf("RECV: %.*s\n", conn_list[fd].rllength, conn_list[fd].rlbuffer);

        // 请求头不完整,继续等待更多数据
        set_event(fd, EPOLLIN | EPOLLET, 1);
    }
}

int send_cb(int fd)
{
    

    int sent = send_noblock(fd);
    if (sent < 0)
    {
        return -1;
    }
    
    else if (sent < conn_list[fd].wllength)
    {
        // 还有数据未发送完,继续监听写事件
        set_event(fd, EPOLLOUT | EPOLLET, 1);
        return 0;
    }

    else if(sent == conn_list[fd].wllength)
    {
        // 数据发送完毕
        printf("sendLen: %d\n", conn_list[fd].wllength);
        printf("SEND: %.*s\n", conn_list[fd].wllength, conn_list[fd].wlbuffer);

        reset_w_noblock(fd);

        set_event(fd, EPOLLIN | EPOLLET, 1);
        return 0;
    }


}

int main()
{
    conn_list = get_connlist(CONNECTION_SIZE);
    int epfd = init_epfd();
    printf("epfd: %d\n", epfd);

    struct epoll_event *events = malloc(sizeof(struct epoll_event) * CONNECTION_SIZE);
    memset(events, 0, sizeof(struct epoll_event) * CONNECTION_SIZE);

    int socketfd = socket(AF_INET, SOCK_STREAM, 0);
    init_server(socketfd, 8000);
    set_event(socketfd, EPOLLIN, 0);
    conn_register(socketfd, BUFFER_LENGTH, accept_cb, NULL, 0);

    while (1)
    {

        int nready = epoll_wait(epfd, events, CONNECTION_SIZE, -1);

        for (int i = 0; i < nready; i++)
        {
            int fd = events[i].data.fd;
            if (events[i].events & EPOLLIN)
            {
                conn_list[fd].in_callback(fd);
            }

            if (events[i].events & EPOLLOUT)
            {
                conn_list[fd].out_callback(fd);
            }
        }
    }

    close(epfd);
    free(events);
    close_connlist(conn_list, CONNECTION_SIZE);
    return 0;
}

int init_server(int sockfd, int port)
{
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(port);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    setReUseAddr(sockfd); // 启用端口复用,注意一定要放在bind前面。否则端口复用无效,程序断开重启后,bind会失效

    bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    listen(sockfd, 5);

    printf("listenfd: %d\n", sockfd);
}

测试成功:观察到第一次接收http,请求不完整,我们不做数据的处理(提取、发送)。第二次接收,发现请求完整,这时才对数据进行处理。

接下来我们要实现:

简易处理http请求,提取请求头中的resourse(index.html),然后去查找该资源,将该资源返回过去。

3. http_server的实现

http_request:提取请求头的关键信息:如index.html

http_response:根据请求头的信息,去本地(或者数据库)查找该信息填充到body中,构建出需要发送的内容。

3.1 初步实现(不封装成函数)

cpp 复制代码
#include "reactor.h"
#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <sys/epoll.h>
#include <errno.h>
#include <sys/time.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#define BUFFER_LENGTH 1024

#define CONNECTION_SIZE 100 // 1048576 // 1024 * 1024

#define MAX_PORTS 20

int accept_cb(int fd);
int recv_cb(int fd);
int send_cb(int fd);
int init_server(int sockfd, int port);

conn *conn_list;

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

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

    conn_register(clientfd, BUFFER_LENGTH, recv_cb, send_cb, 0);
    set_event(clientfd, EPOLLIN | EPOLLET, 0);
#if 1
    setNonblock(clientfd); // 设置非阻塞
#endif
    return 0;
}

int is_http_complete(const char *buf, int len)
{

    // 确保缓冲区长度至少能容纳 "\r\n\r\n"(4 字节)
    if (len >= 4)
    {
        // 定位到末尾 4 字节的起始位置
        const char *end = buf + len - 4;
        // 检查这 4 字节是否严格匹配 "\r\n\r\n"
        // printf("检查结尾4字节: %.*s\n", 4, end);
        if (end[0] == '\r' && end[1] == '\n' &&
            end[2] == '\r' && end[3] == '\n')
        {
            return 1; // 请求头完整
        }
    }
    return 0; // 不完整,继续等待
}

int recv_cb(int fd)
{
    // 在浏览器输入 ip:port/path    以我为例: 192.168.248.130:8000/index.html
    // 服务器接收到:
    // GET /index.html HTTP/1.1
    // Host: 192.168.248.130:8000
    // Connection: keep-alive
    // Cache-Control: max-age=0
    // Upgrade-Insecure-Requests: 1
    // User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0
    // Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
    // Accept-Encoding: gzip, deflate
    // Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

    // 处理非阻塞接收
    if (recv_noblock(fd, BUFFER_LENGTH) <= 0)
    {
        return 0;
    }

    if (is_http_complete(conn_list[fd].rlbuffer, conn_list[fd].rllength))
    {

        // 请求头完整,处理请求
        printf("请求头完整,准备处理请求:\n");
        printf("recvLen: %d\n", conn_list[fd].rllength);
        printf("RECV: %.*s\n", conn_list[fd].rllength, conn_list[fd].rlbuffer);

        char buffer[BUFFER_LENGTH] = {0};
        int buffer_length = 0;
        int len = conn_list[fd].rllength;
        int i = 5;
        int j = 0;
        for (; i < len; i++, j++)
        {
            if (conn_list[fd].rlbuffer[i] != ' ')
            {
                buffer[j] = conn_list[fd].rlbuffer[i];
                buffer_length++;
            }
            else
            {
                break;
            }
        }

        add_to_w_noblock(fd, buffer, buffer_length, 0);

        reset_r_noblock(fd);
        set_event(fd, EPOLLOUT | EPOLLET, 1);
    }

    else
    {
#if 0
        printf("请求头不完整,继续等待更多数据:\n");
        printf("recvLen: %d\n", conn_list[fd].rllength);
        printf("RECV: %.*s\n", conn_list[fd].rllength, conn_list[fd].rlbuffer);
#endif
        // 请求头不完整,继续等待更多数据
        set_event(fd, EPOLLIN | EPOLLET, 1);
    }
}

int send_cb(int fd)
{
    if (conn_list[fd].wllength == 0 || conn_list[fd].wlbuffer == NULL)
    {
        // 防御:无数据可发送时直接切换回读事件
        set_event(fd, EPOLLIN | EPOLLET, 1);
        return 0;
    }

    // 这里只做html资源的响应,favicon.ico等其他资源不做响应
    // 服务器收到响应后,显示网页内容
    // 响应内容示例:
    // HTTP/1.1 200 OK
    // Date: Wed, 11 Oct 2025 12:00:00 GMT  # 当前GMT时间(需动态生成)
    // Server: MyEpollServer/1.0            # 你的服务器标识(自定义)
    // Connection: keep-alive               # 与请求的Connection: keep-alive对应,保持长连接
    // Content-Type: text/html; charset=utf-8  # 响应体类型(HTML文件,编码UTF-8)
    // Content-Length: 138                  # 响应体的字节数(需根据实际HTML内容计算)

    // <!DOCTYPE html>
    // <html lang="zh-CN">
    // <head>
    //     <meta charset="UTF-8">
    //     <title>Index Page</title>
    // </head>
    // <body>
    //     <h1>Hello! This is the index.html page.</h1>
    // </body>
    // </html>

    if (conn_list[fd].w_sent == 0)
    {
        char filename[BUFFER_LENGTH] = {0};
        for (int i = 0; i < conn_list[fd].wllength; i++)
        {
            filename[i] = conn_list[fd].wlbuffer[i];
        }
        filename[conn_list[fd].wllength] = '\0'; // 确保字符串以 null 结尾

        printf("请求文件: %s\n", filename);

#if 1
        // 适用移动指针获取文件大小

        FILE *fp = fopen(filename, "r"); //
        if (fp == NULL)
        {
            printf("fopen failed\n");

            char response[BUFFER_LENGTH] = {0};
            int len = sprintf(response,
                              "HTTP/1.1 404 Not Found\r\n"
                              "Content-Type: text/html\r\n"
                              "Content-Length: 113\r\n"
                              "Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n"
                              "<html><head><title>404 Not Found</title></head>"
                              "<body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>");
            add_to_w_noblock(fd, response, len, 0);
        }
        // file size
        else
        {
            fseek(fp, 0, SEEK_END);      // 文件的指针放到文件的末尾(初始时文件指针在文件的开头)
            int file_length = ftell(fp); // 获得文件指针的偏移量,大小即为file size
            fseek(fp, 0, SEEK_SET);      // 文件的指针放到文件的开头

            char response[BUFFER_LENGTH] = {0};
            int len = sprintf(response,

                              "HTTP/1.1 200 OK\r\n"
                              "Content-Type: text/html\r\n"
                              "Cache-Control: max-age=3600\r\n" // 新增:告诉浏览器缓存1小时(3600秒)
                              "Accept-Ranges: bytes\r\n"
                              "Content-Length: %d\r\n"
                              "Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n",

                              file_length);

            int count = fread(response + len, 1, BUFFER_LENGTH - len, fp);
            len += count;

            add_to_w_noblock(fd, response, len, 0);

            fclose(fp);
        }

#else
        // 使用stat获取文件大小
        int filefd = open(filename, O_RDONLY);
        if (filefd < 0)
        {
            printf("open failed\n");
            char response[BUFFER_LENGTH] = {0};
            int len = sprintf(response,
                              "HTTP/1.1 404 Not Found\r\n"
                              "Content-Type: text/html\r\n"
                              "Content-Length: 113\r\n"
                              "Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n"
                              "<html><head><title>404 Not Found</title></head>"
                              "<body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>");
            add_to_w_noblock(fd, response, len, 0);
        }

        struct stat stat_buf;
        fstat(filefd, &stat_buf);

        char response[BUFFER_LENGTH] = {0};
        int len = sprintf(response,
                          "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, response + len, BUFFER_LENGTH - len);
        len += count;

        add_to_w_noblock(fd, response, len, 0);

        close(filefd);
    

#endif
    }

        int sent = send_noblock(fd);
        if (sent < 0)
        {
            // 发送过程中出错,连接已关闭
            return -1;
        }

        else if (sent < conn_list[fd].wllength)
        {
            // 还有数据未发送完,继续监听写事件
            set_event(fd, EPOLLOUT | EPOLLET, 1);
            return 0;
        }

        else if (sent == conn_list[fd].wllength)
        {
            // 数据发送完毕
            printf("sendLen: %d\n", conn_list[fd].wllength);
            printf("SEND: %.*s\n", conn_list[fd].wllength, conn_list[fd].wlbuffer);

            reset_w_noblock(fd);

            set_event(fd, EPOLLIN | EPOLLET, 1);
            return 0;
        }
    }

    int main()
    {
        conn_list = get_connlist(CONNECTION_SIZE);
        int epfd = init_epfd();
        printf("epfd: %d\n", epfd);

        struct epoll_event *events = malloc(sizeof(struct epoll_event) * CONNECTION_SIZE);
        memset(events, 0, sizeof(struct epoll_event) * CONNECTION_SIZE);

        int socketfd = socket(AF_INET, SOCK_STREAM, 0);
        init_server(socketfd, 8000);
        set_event(socketfd, EPOLLIN, 0);
        conn_register(socketfd, BUFFER_LENGTH, accept_cb, NULL, 0);

        while (1)
        {

            int nready = epoll_wait(epfd, events, CONNECTION_SIZE, -1);

            for (int i = 0; i < nready; i++)
            {
                int fd = events[i].data.fd;
                if (events[i].events & EPOLLIN)
                {
                    conn_list[fd].in_callback(fd);
                }

                if (events[i].events & EPOLLOUT)
                {
                    conn_list[fd].out_callback(fd);
                }
            }
        }

        close(epfd);
        free(events);
        close_connlist(conn_list, CONNECTION_SIZE);
        return 0;
    }

    int init_server(int sockfd, int port)
    {
        struct sockaddr_in servaddr;
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(port);
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

        setReUseAddr(sockfd); // 启用端口复用,注意一定要放在bind前面。否则端口复用无效,程序断开重启后,bind会失效

        bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
        listen(sockfd, 5);

        printf("listenfd: %d\n", sockfd);
    }

测试成功:

第一次浏览器发送index.html请求,我们在本地打开该文件的资源然后返回过去

浏览器接收到index.html的回复后发送请求favicon.ico文件,我们没有该文件返回"404 Not Found"

(注意:下一次浏览器再发送html请求后,可能不会再询问favicon.ico,因为浏览器有缓存该请求无效,所以不会再询问服务器了)

3.2 将涉及http的操作封装成函数库http_server.h

3.2.1 http_server.h
cpp 复制代码
#ifndef __HTTP_SERVER_H__
#define __HTTP_SERVER_H__

#include "reactor.h"

#define HTTP_RESPONSE_HEADER_LEN 1024
// 判断http请求是否完整,完整则做处理
int is_http_complete(const char *buf, int len)
{

    // 确保缓冲区长度至少能容纳 "\r\n\r\n"(4 字节)
    if (len >= 4)
    {
        // 定位到末尾 4 字节的起始位置
        const char *end = buf + len - 4;
        // 检查这 4 字节是否严格匹配 "\r\n\r\n"
        // printf("检查结尾4字节: %.*s\n", 4, end);
        if (end[0] == '\r' && end[1] == '\n' &&
            end[2] == '\r' && end[3] == '\n')
        {
            return 1; // 请求头完整
        }
    }
    return 0; // 不完整,继续等待
}



// 在浏览器输入 ip:port/path    以我为例: 192.168.248.130:8000/index.html
// 服务器接收到:
// GET /index.html HTTP/1.1
// Host: 192.168.248.130:8000
// Connection: keep-alive
// Cache-Control: max-age=0
// Upgrade-Insecure-Requests: 1
// User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0
// Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
// Accept-Encoding: gzip, deflate
// Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

// 我们简单点处理,只做资源文件的提取!!!

int http_request(conn *c)
{
    // 请求头完整,处理请求
    printf("请求头完整,准备处理请求:\n");
    printf("recvLen: %d\n", c->rllength);
    printf("RECV: %.*s\n", c->rllength, c->rlbuffer);

    char buffer[100] = {0};
    int buffer_length = 0;
    int len = c->rllength;
    int i = 5;
    int j = 0;
    for (; i < len; i++, j++)
    {
        if (c->rlbuffer[i] != ' ')
        {
            buffer[j] = c->rlbuffer[i];
            buffer_length++;
        }
        else
        {
            break;
        }
    }

    add_to_w_noblock(c->fd, buffer, buffer_length, 0);
}

// 这里只做html资源的响应,favicon.ico等其他资源不做响应
// 服务器收到响应后,显示网页内容
// 响应内容示例:
// HTTP/1.1 200 OK
// Date: Wed, 11 Oct 2025 12:00:00 GMT  # 当前GMT时间(需动态生成)
// Server: MyEpollServer/1.0            # 你的服务器标识(自定义)
// Connection: keep-alive               # 与请求的Connection: keep-alive对应,保持长连接
// Content-Type: text/html; charset=utf-8  # 响应体类型(HTML文件,编码UTF-8)
// Content-Length: 138                  # 响应体的字节数(需根据实际HTML内容计算)

// <!DOCTYPE html>
// <html lang="zh-CN">
// <head>
//     <meta charset="UTF-8">
//     <title>Index Page</title>
// </head>
// <body>
//     <h1>Hello! This is the index.html page.</h1>
// </body>
// </html>

// response_max_length必须大于回复头,这个使用者会知道的,但是文件大小使用者不必知道
int http_response(conn *c)
{
    int response_max_length = HTTP_RESPONSE_HEADER_LEN;
    char *response = malloc(response_max_length * sizeof(char));

    char filename[100] = {0};
    for (int i = 0; i < c->wllength; i++)
    {
        filename[i] = c->wlbuffer[i];
    }
    filename[c->wllength] = '\0'; // 确保字符串以 null 结尾

    printf("请求文件: %s\n", filename);

#if 1
    // 适用移动指针获取文件大小

    FILE *fp = fopen(filename, "rb"); //
    if (fp == NULL)
    {
        printf("fopen failed\n");

        int len = sprintf(response,
                          "HTTP/1.1 404 Not Found\r\n"
                          "Content-Type: text/html\r\n"
                          "Content-Length: 113\r\n"
                          "Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n"
                          "<html><head><title>404 Not Found</title></head>"
                          "<body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>");
        add_to_w_noblock(c->fd, response, len, 0);
    }
    // file size
    else
    {
        fseek(fp, 0, SEEK_END);      // 文件的指针放到文件的末尾(初始时文件指针在文件的开头)
        int file_length = ftell(fp); // 获得文件指针的偏移量,大小即为file size
        fseek(fp, 0, SEEK_SET);      // 文件的指针放到文件的开头

        int len = sprintf(response,

                          "HTTP/1.1 200 OK\r\n"
                          "Content-Type: text/html\r\n"
                          "Cache-Control: max-age=3600\r\n" // 新增:告诉浏览器缓存1小时(3600秒)
                          "Accept-Ranges: bytes\r\n"
                          "Content-Length: %d\r\n"
                          "Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n",
                          file_length);

        printf("header_len: %d\n", len);
        int count = fread(response + len, 1, response_max_length - len, fp);
        len += count;

        printf("file_len: %d\n", file_length);

        if (count < file_length)
        {
            char *new_response = realloc(response, (response_max_length + file_length - count) * sizeof(char));
            if (new_response == NULL)
            {
                printf("新response内存分配失败\n");
                return -1;
            }
            response = new_response;
            response_max_length = response_max_length + file_length - count;
            printf("response_max_length: %d\n", response_max_length);
            count = fread(response + len, 1, file_length - count, fp); // 注意文件从文件指针开始读,文件指针现在位于文件没读完的位置
            len += count;
        }

        printf("len: %d\n", len);
        add_to_w_noblock(c->fd, response, len, 0);

        free(response);
        fclose(fp);
    }

#else
    // 使用stat获取文件大小
    int filefd = open(filename, O_RDONLY);
    if (filefd < 0)
    {
        printf("open failed\n");
        char response[response_max_length] = {0};
        int len = sprintf(response,
                          "HTTP/1.1 404 Not Found\r\n"
                          "Content-Type: text/html\r\n"
                          "Content-Length: 113\r\n"
                          "Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n"
                          "<html><head><title>404 Not Found</title></head>"
                          "<body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>");
        add_to_w_noblock(c->fd, response, len, 0);
    }

    struct stat stat_buf;
    fstat(filefd, &stat_buf);

    char response[response_max_length] = {0};
    int len = sprintf(response,
                      "HTTP/1.1 200 OK\r\n"
                      "Content-Type: text/html\r\n"
                      "Cache-Control: max-age=3600\r\n" // 新增:告诉浏览器缓存1小时(3600秒)
                      "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, response + len, response_max_length - len);
    len += count;

    add_to_w_noblock(c->fd, response, len, 0);

    close(filefd);

#endif
}

#endif
3.2.2 reactor.c(主函数)
cpp 复制代码
#include "reactor.h"
#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <sys/epoll.h>
#include <errno.h>
#include <sys/time.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include "http_server.h"

#define BUFFER_LENGTH 1024

#define CONNECTION_SIZE 100 // 1048576 // 1024 * 1024


#define MAX_PORTS 20

int accept_cb(int fd);
int recv_cb(int fd);
int send_cb(int fd);
int init_server(int sockfd, int port);

conn *conn_list;

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

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

    conn_register(clientfd, BUFFER_LENGTH, recv_cb, send_cb, 0);
    set_event(clientfd, EPOLLIN | EPOLLET, 0);
#if 1
    setNonblock(clientfd); // 设置非阻塞
#endif
    return 0;
}

int recv_cb(int fd)
{

    // 处理非阻塞接收
    if (recv_noblock(fd, BUFFER_LENGTH) <= 0)
    {
        return 0;
    }

    if (is_http_complete(conn_list[fd].rlbuffer, conn_list[fd].rllength))
    {

        http_request(&conn_list[fd]);
        reset_r_noblock(fd);
        set_event(fd, EPOLLOUT | EPOLLET, 1);
    }

    else
    {
        // 请求头不完整,继续等待更多数据
        set_event(fd, EPOLLIN | EPOLLET, 1);
    }
}

int send_cb(int fd)
{
    if (conn_list[fd].wllength == 0 || conn_list[fd].wlbuffer == NULL)
    {
        // 防御:无数据可发送时直接切换回读事件
        set_event(fd, EPOLLIN | EPOLLET, 1);
        return 0;
    }

    if (conn_list[fd].w_sent == 0)
    {
        http_response(&conn_list[fd]);
    }

    int sent = send_noblock(fd);
    if (sent < 0)
    {
        // 发送过程中出错,连接已关闭
        return -1;
    }

    else if (sent < conn_list[fd].wllength)
    {
        // 还有数据未发送完,继续监听写事件
        set_event(fd, EPOLLOUT | EPOLLET, 1);
        return 0;
    }

    else if (sent == conn_list[fd].wllength)
    {
        // 数据发送完毕
        printf("sendLen: %d\n", conn_list[fd].wllength);
        printf("SEND: %.*s\n", conn_list[fd].wllength, conn_list[fd].wlbuffer);

        reset_w_noblock(fd);

        set_event(fd, EPOLLIN | EPOLLET, 1);
        return 0;
    }
}

int main()
{
    conn_list = get_connlist(CONNECTION_SIZE);
    int epfd = init_epfd();
    printf("epfd: %d\n", epfd);

    struct epoll_event *events = malloc(sizeof(struct epoll_event) * CONNECTION_SIZE);
    memset(events, 0, sizeof(struct epoll_event) * CONNECTION_SIZE);

    int socketfd = socket(AF_INET, SOCK_STREAM, 0);
    init_server(socketfd, 8000);
    set_event(socketfd, EPOLLIN, 0);
    conn_register(socketfd, BUFFER_LENGTH, accept_cb, NULL, 0);

    while (1)
    {

        int nready = epoll_wait(epfd, events, CONNECTION_SIZE, -1);

        for (int i = 0; i < nready; i++)
        {
            int fd = events[i].data.fd;
            if (events[i].events & EPOLLIN)
            {
                conn_list[fd].in_callback(fd);
            }

            if (events[i].events & EPOLLOUT)
            {
                conn_list[fd].out_callback(fd);
            }
        }
    }

    close(epfd);
    free(events);
    close_connlist(conn_list, CONNECTION_SIZE);
    return 0;
}

int init_server(int sockfd, int port)
{
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(port);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    setReUseAddr(sockfd); // 启用端口复用,注意一定要放在bind前面。否则端口复用无效,程序断开重启后,bind会失效

    bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    listen(sockfd, 5);

    printf("listenfd: %d\n", sockfd);
}

三、总结

1.观察3.2.2的代码,你发现代码结构很清晰,主要做的工作就是网络层的管理(监听、发送、接收),如果有相关的数据处理,我们只需要编写一个数据处理函数(如http_request、http_response),传入连接结构体参数就行。

2.数据处理函数不需要关注数据什么时候发送、只需要处理数据就行(从conn中拿取接收的数据、将需要发送的数据存到conn中)。

3.总的来说rector模式实现了网络层(监听)和业务层(回调函数、数据处理)的分离。conn实现了不同业务间的数据共享,set_event完美控制了下一步该处理什么业务。而epoll(网络层main函数)实现了事件的监听,调用事件对应的业务函数。

相关推荐
BTU_YC3 小时前
CentOS 7 虚拟IP配置指南:使用传统network-scripts实现高可用
linux·tcp/ip·centos
陌路203 小时前
LINUX14 进程间的通信 - 管道
linux·网络
大聪明-PLUS3 小时前
从头开始为 ARM 创建 Ubuntu 映像
linux·嵌入式·arm·smarc
chenzhou__4 小时前
MYSQL学习笔记(个人)(第十五天)
linux·数据库·笔记·学习·mysql
序属秋秋秋5 小时前
《Linux系统编程之入门基础》【Linux基础 理论+命令】(上)
linux·运维·服务器·ubuntu·centos·命令模式
朱嘉鼎6 小时前
C语言之可变参函数
c语言·开发语言
一张假钞9 小时前
Ubuntu SSH 免密码登陆
linux·ubuntu·ssh
Wang's Blog10 小时前
Linux小课堂: 文件操作警惕高危删除命令与深入文件链接机制
linux·运维·服务器
你好,我叫C小白10 小时前
C语言 循环结构(1)
c语言·开发语言·算法·while·do...while