基于 Reactor 模式的 HTTP 协议扩展实现 - 详解
为让Reactor模式下的TCP服务器具备网页服务能力,通过集成HTTP协议,对其进行网页端功能扩展,使其升级为可响应浏览器请求的Web服务器。
TCP服务器实现部分Reactor 模式实现:从 epoll 到高并发调试-CSDN博客
1.功能实现
将底层I/O与上层业务进行分离,提高I/O性能,更适配高并发,也便于维护和扩展
1.1全局变量部分
conn结构体中加入状态机,共三个阶段
0(生成并发送响应头) 服务器向客户端发送数据的准备阶段,将响应头写入缓冲区,修改缓冲区大小,后进入1阶段
1(发送响应体) 向客户端发送数据的发送阶段,若数据长度过大,分段多次发送,发送完毕后进入2阶段
2(清空缓存) 发送数据的结束阶段,缓冲区置为空,长度置为0,进入0阶段
1.2上层业务部分
webserver.c文件,接收新数据前的准备函数
int http_request(struct conn *c){
//打印提示
printf("request: %s\n", c->rbuffer);
//清空数据,长度归零
memset(c->wbuffer, 0, BUFFER_LENGTH);
c->wlength = 0;
//设置状态为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: 82\r\n"
"Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n"
"Avogado6 Avogado6
\r\n\r\n");
//将html文件映射到网页中
#elif 0
//创建文件描述符只读模式获取文件内容
int filefd = open("index.html", O_RDONLY);
//定义文件状态结构体,用于获取文件长度
struct stat stat_buf;
//获取文件数据
fstat(filefd, &stat_buf);
if(c->status == 0){//准备阶段
//生成响应头并写入缓冲区
//printf("c->status == 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: %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){//发送阶段
//调用sendfile函数,一次性映射完整文件(简化操作)
int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size);
//映射失败时的处理
if(ret == -1){
printf("send file error: %d\n", errno);
}
//发送完成,更新状态至清空缓存
c->status = 2;
}else if(c->status == 2){//清空缓存
//缓冲区数据置空,长度置0
c->wlength = 0;
memset(c->wbuffer, 0, BUFFER_LENGTH);
//发送数据结束,更新状态至准备阶段,准备下一次发送数据
c->status = 0;
}
//发送完成,关闭文件描述符
close(filefd);
//将图片文件映射到网页中
#elif 0
//创建文件描述符只读模式获取图片内容
int filefd = open("Avogado6.jpg", 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: image/jpg\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){//发送阶段
//调用sendfile函数,一次性映射完整文件
int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size);
//映射失败时的处理
if(ret == -1){
printf("send file error: %d\n", errno);
}
//发送完成,更新状态至清空缓存
c->status = 2;
}else if(c->status == 2){//清空缓存
//缓冲区数据置空,长度置0
c->wlength = 0;
memset(c->wbuffer, 0, BUFFER_LENGTH);
//发送数据结束,更新状态至准备阶段,准备下一次发送数据
c->status = 0;
}
//发送完成,关闭文件描述符
close(filefd);
#endif
return c->wlength;
}
1.3底层I/O处理部分
recv_cb函数中调用http_request函数,对当前连接执行http请求的初始化操作
http_request(&conn_list[fd]);
send_cb先调用http_response函数对当前连接执行http的生成响应内容操作
http_response(&conn_list[fd]);
再通过状态机,对不同阶段进行不同处理
if(conn_list[fd].status == 1){//状态为发送数据阶段时
//调用send函数将发送缓冲区中的数据发送至客户端fd
count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
//调用set_event函数将该fd连接信息中的关注事件修改为可写,以便继续发送剩余数据
set_event(fd, EPOLLOUT, 0);
}else if(conn_list[fd].status == 2){//状态为清空缓存阶段时
//调用set_event函数将该fd连接信息中的关注事件修改为可写,准备执行清空缓存操作
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连接信息中的关注事件修改为可读,准备接收数据
set_event(fd, EPOLLIN, 0);
}
2.功能测试
2.1固定 HTML 响应生成
运行该部分
//生成响应头并写入缓冲区
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: 82\r\n"
"Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n"
"Avogado6 Avogado6
\r\n\r\n");
运行结果,网页端访问该端口

对该功能进行并发性能测试(简化输出)
wrk -c50 -t10 -d10s http://192.168.147.130:2000
50个并发连接,10个线程,持续10s的压力测试
测试结果
Running 10s test @ http://192.168.147.130:2000
10 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 542.43us 145.23us 8.75ms 80.37%
Req/Sec 9.11k 0.89k 13.51k 72.42%
914084 requests in 10.10s, 178.71MB read
Requests/sec: 90504.27
Transfer/sec: 17.69MB
共处理90万次,平均延迟低于半毫秒,延迟波动小
2.2HTML 文件响应
运行该部分
//创建文件描述符只读模式获取文件内容
int filefd = open("index.html", O_RDONLY);
//定义文件状态结构体,用于获取文件长度
struct stat stat_buf;
//获取文件数据
fstat(filefd, &stat_buf);
if(c->status == 0){//准备阶段
//生成响应头并写入缓冲区
//printf("c->status == 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: %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){//发送阶段
//调用sendfile函数,一次性映射完整文件(简化操作)
int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size);
//映射失败时的处理
if(ret == -1){
printf("send file error: %d\n", errno);
}
//发送完成,更新状态至清空缓存
c->status = 2;
}else if(c->status == 2){//清空缓存
//缓冲区数据置空,长度置0
c->wlength = 0;
memset(c->wbuffer, 0, BUFFER_LENGTH);
//发送数据结束,更新状态至准备阶段,准备下一次发送数据
c->status = 0;
}
//发送完成,关闭文件描述符
close(filefd);
运行结果,网页端访问该端口

2.3图片文件响应
运行该部分
//创建文件描述符只读模式获取图片内容
int filefd = open("Avogado6.jpg", 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: image/jpg\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){//发送阶段
//调用sendfile函数,一次性映射完整文件
int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size);
//映射失败时的处理
if(ret == -1){
printf("send file error: %d\n", errno);
}
//发送完成,更新状态至清空缓存
c->status = 2;
}else if(c->status == 2){//清空缓存
//缓冲区数据置空,长度置0
c->wlength = 0;
memset(c->wbuffer, 0, BUFFER_LENGTH);
//发送数据结束,更新状态至准备阶段,准备下一次发送数据
c->status = 0;
}
//发送完成,关闭文件描述符
close(filefd);
运行结果,网页端访问该端口

3.方法总结
整个web服务器遵循底层I/O与上层业务分离的原则,通过Reactor模式处理I/O事件+状态机管理HTTP响应流程的方法,实现高效、可扩展的web服务器的相应功能,若需扩展功能,只需修改http业务部分
http_request函数进行http请求的初始化,作为业务逻辑的入口
http_response函数通过条件编译实现三种业务功能
固定HTML响应,生成响应头后直接写入缓冲区
HTML文件/图片响应, 读取文件后,根据状态机阶段,分别进行生成响应头写入缓冲区,发送完整数据,清空缓存三个阶段
调用send_file函数进行发送数据操作,提升文件传输效率
send_cb函数根据状态机的不同阶段,实现向客户端发送数据的操作
4.完整代码
server.h
#ifndef __SERVER_H__
#define __SERVER_H__
#define BUFFER_LENGTH 1024
//声明处理事件的回调函数类型,统一接口便于分发时间
typedef int (*RCALLBACK)(int fd);
//连接信息结构体
struct conn{
//套接字,客户端fd或者监听fd
int fd;
//设置状态机,共三个阶段
//0(生成并发送响应头) 服务器向客户端发送数据的准备阶段,将响应头写入缓冲区,修改缓冲区大小,后进入1阶段
//1(发送响应体) 向客户端发送数据的发送阶段,若数据长度过大,分段多次发送,发送完毕后进入2阶段
//2(清空缓存) 发送数据的结束阶段,缓冲区置为空,长度置为0,进入0阶段
int status;
//读写数据的缓冲区数组和大小
char rbuffer[BUFFER_LENGTH];
int rlength;
char wbuffer[BUFFER_LENGTH];
int wlength;
//把回调函数的指针加入结构体
RCALLBACK send_callback;
//互斥状态的两个回调函数指针,共同体的形式加入结构体(客户端fd调用recv_callback,监听fd调用accept_callback)
//共同体,所有成员公用同一块内存空间,节省内存
union{
RCALLBACK recv_callback;
RCALLBACK accept_callback;
} r_action;
};
int http_request(struct conn *c);
int http_response(struct conn *c);
#endif
reactor.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "server.h"
//宏定义设定缓冲区和连接列表的大小
#define CONNECTION_SIZE 1048576
#define MAX_PORTS 20
//计算耗时
#define TIME_SUB_MS(tv1, tv2) ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)
//epoll实例的全局变量,各函数中操作同一个epoll
int epfd = 0;
//具体回调函数的声明
int recv_cb(int fd);
int accept_cb(int fd);
int send_cb(int fd);
//创建开始时时间结构体变量
struct timeval begin;
//用数组存储所有连接
struct conn conn_list[CONNECTION_SIZE] = {0};
//添加/修改epoll事件
int set_event(int fd, int event, int flag){
//flag非零时,添加epoll事件
if(flag){//no-zero add
//创建epoll_event类型变量
struct epoll_event ev;
//设定关注的事件类型
ev.events = event;
//将当前fd存入ev的data.fd对象中
ev.data.fd = fd;
//添加到epfd的epoll实例中
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
}else{//zero mod
struct epoll_event ev;
ev.events = event;
ev.data.fd = fd;
//修改epfd中的epoll实例
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
}
//为epoll实例中添加新的客户端连接
int event_register(int fd, int event){
if(fd < 0) return -1;
//初始化连接信息,绑定fd,设置回调函数
conn_list[fd].fd = fd;
//添加连接,共同体中选择recv_cb回调函数
conn_list[fd].r_action.recv_callback = recv_cb;
conn_list[fd].send_callback = send_cb;
//初始化缓冲区
conn_list[fd].rlength = 0;
conn_list[fd].wlength = 0;
//调用set_event函数添加事件,并监控可读事件
set_event(fd, EPOLLIN, 1);
}
//listen(sockfd) --> EPOLLIN --> accept_cb
//接收客户端连接请求,创建客户端fd并完成初始化
int accept_cb(int fd){
//定义客户端地址结构体,计算长度
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
//调用accept函数,从监听fd接收数据并创建对应地址的客户端fd
int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);
//printf("accept finished: %d\n", clientfd);
if(clientfd < 0){
printf("accept error: %d\n", errno);
return -1;
}
//调用event_register函数初始化连接信息,关注可读事件
event_register(clientfd, EPOLLIN);
if(clientfd % 1000 == 0){
//获取每建立1000个连接时的时间
struct timeval current;
gettimeofday(¤t, NULL);
//计算耗时
int time_used = TIME_SUB_MS(current, begin);
//更新每次时间开始值
memcpy(&begin, ¤t, sizeof(struct timeval));
printf("accept finished: %d, time_used: %d\n", clientfd, time_used);
}
return 0;
}
//客户端fd触发EPOLLIN事件的回调函数,接收客户端发送的数据
int recv_cb(int fd){
memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH );
//调用recv接收客户端数据,存入该连接的接收缓冲区,通过接收数据长度判断接收状态
int count = recv(fd, conn_list[fd].rbuffer, BUFFER_LENGTH, 0);
//状态为零时客户端主动断开连接
if(count == 0){
printf("clientfd disconnect: %d\n", fd);
//关闭断开的客户端fd
close(fd);
//将该fd从epfd的epoll实例中删除,不再监控
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);// 无需ev结构体
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;
//打印接收数据
//printf("RRECV: %s\n", conn_list[fd].rbuffer);
#if 0 //echo 回声模式开关,1开启
//将接收缓冲区的数据和数据长度存储到发送缓冲区中,用于send函数
conn_list[fd].wlength = conn_list[fd].rlength;
memcpy(conn_list[fd].wbuffer, conn_list[fd].rbuffer, conn_list[fd].wlength);
printf("[%d]recv: %s\n", conn_list[fd].rlength, conn_list[fd].rbuffer);
#else
//对当前连接执行http请求的初始化操作
http_request(&conn_list[fd]);
#endif
//调用set_event函数修改该fd的关注事件为EPOLLOUT可写
//让epoll后续触发可写事件,调用send_cb发送缓冲区中数据
set_event(fd, EPOLLOUT, 0);
return count;
}
//客户端fd触发可写事件后,发送缓冲区中的数据
int send_cb(int fd){
#if 1
//对当前连接执行http的生成响应内容操作
http_response(&conn_list[fd]);
#endif
int count = 0;
if(conn_list[fd].status == 1){//状态为发送数据阶段时
//调用send函数将发送缓冲区中的数据发送至客户端fd
count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
//调用set_event函数将该fd连接信息中的关注事件修改为可写,以便继续发送剩余数据
set_event(fd, EPOLLOUT, 0);
}else if(conn_list[fd].status == 2){//状态为清空缓存阶段时
//调用set_event函数将该fd连接信息中的关注事件修改为可写,准备执行清空缓存操作
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连接信息中的关注事件修改为可读,准备接收数据
set_event(fd, EPOLLIN, 0);
}
return count;
}
//创建服务器,开启监听fd
int init_server(unsigned short port){
//创建TCP流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
//设置服务器地址信息,IPv4,绑定所有本地网卡,绑定端口
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //0.0.0.0
servaddr.sin_port = htons(port); //0-1023
//绑定套接字到服务器地址和端口
if(-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))){
printf("bind failed: %s\n", strerror(errno));
}
//开启监听套接字,最大等待队列为10
listen(sockfd, 4096);
//printf("listen finished: %d\n", sockfd);
return sockfd;
}
int main(){
//设置端口
unsigned short port = 2000;
//调用epoll_create函数创建一个epoll实例
epfd = epoll_create(1);
for(int i = 0;i < MAX_PORTS;i ++){
//初始化服务器,创建套接字,绑定地址,开始监听
//返回监听套接字
int sockfd = init_server(port + i);
//将监听套接字存入连接列表中
conn_list[sockfd].fd = sockfd;
//设置accept_cb为共同体回调函数的处理,即监听到可读事件后调用accept_cb函数
conn_list[sockfd].r_action.recv_callback = accept_cb;
//调用set_event函数将监听套接字加入到epoll实例中,监控其可读事件
set_event(sockfd, EPOLLIN, 1);
}
//获取开始时的时间
gettimeofday(&begin,NULL);
//主循环,处理新连接,收发客户端数据
while(1){//mainloop
//创建数组存储就绪事件,初始化为0
struct epoll_event events[1024] = {0};
//调用epoll_wait阻塞等待,直到监控的fd触发了就绪事件,并统计数量
int nready = epoll_wait(epfd, events, 1024, -1);
//循环遍历所有就绪事件(时间复杂度为O(k))
int i = 0;
for(i = 0;i < nready;i ++){
//获取当前就绪事件对应的fd,存入connfd中,简化操作
int connfd = events[i].data.fd;
//当就绪事件触发可读事件时,执行连接列表中该fd的recv_callback回调函数,添加新连接或读取数据
if(events[i].events & EPOLLIN){
conn_list[connfd].r_action.recv_callback(connfd);
}
//当就绪事件触发可写事件时,执行连接列表中该fd的send_callback回调函数,将发送缓冲区的数据发送至客户端
if(events[i].events & EPOLLOUT){
conn_list[connfd].send_callback(connfd);
}
}
}
}
webserver.c
#include
#include
#include
#include
#include
#include
#include
#include "server.h"
//接收新数据前的准备
int http_request(struct conn *c){
//printf("request: %s\n", c->rbuffer);
//清空数据,长度归零
memset(c->wbuffer, 0, BUFFER_LENGTH);
c->wlength = 0;
//设置状态为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: 82\r\n"
"Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n"
"Avogado6 Avogado6
\r\n\r\n");
//将html文件映射到网页中
#elif 0
//创建文件描述符只读模式获取文件内容
int filefd = open("index.html", O_RDONLY);
//定义文件状态结构体,用于获取文件长度
struct stat stat_buf;
//获取文件数据
fstat(filefd, &stat_buf);
if(c->status == 0){//准备阶段
//生成响应头并写入缓冲区
//printf("c->status == 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: %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){//发送阶段
//调用sendfile函数,一次性映射完整文件(简化操作)
int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size);
//映射失败时的处理
if(ret == -1){
printf("send file error: %d\n", errno);
}
//发送完成,更新状态至清空缓存
c->status = 2;
}else if(c->status == 2){//清空缓存
//缓冲区数据置空,长度置0
c->wlength = 0;
memset(c->wbuffer, 0, BUFFER_LENGTH);
//发送数据结束,更新状态至准备阶段,准备下一次发送数据
c->status = 0;
}
//发送完成,关闭文件描述符
close(filefd);
//将图片文件映射到网页中
#elif 0
//创建文件描述符只读模式获取图片内容
int filefd = open("Avogado6.jpg", 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: image/jpg\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){//发送阶段
//调用sendfile函数,一次性映射完整文件
int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size);
//映射失败时的处理
if(ret == -1){
printf("send file error: %d\n", errno);
}
//发送完成,更新状态至清空缓存
c->status = 2;
}else if(c->status == 2){//清空缓存
//缓冲区数据置空,长度置0
c->wlength = 0;
memset(c->wbuffer, 0, BUFFER_LENGTH);
//发送数据结束,更新状态至准备阶段,准备下一次发送数据
c->status = 0;
}
//发送完成,关闭文件描述符
close(filefd);
#endif
return c->wlength;
}
浙公网安备 33010602011771号