基于 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""<html><head><title>Avogado6</title></head><body><h1>Avogado6</h1></body></html>\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){//清空缓存//缓冲区数据置空,长度置0c->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){//清空缓存//缓冲区数据置空,长度置0c->wlength = 0;memset(c->wbuffer, 0, BUFFER_LENGTH);//发送数据结束,更新状态至准备阶段,准备下一次发送数据c->status = 0;}//发送完成,关闭文件描述符close(filefd);#endifreturn 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函数将发送缓冲区中的数据发送至客户端fdcount = 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""<html><head><title>Avogado6</title></head><body><h1>Avogado6</h1></body></html>\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){//清空缓存//缓冲区数据置空,长度置0c->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){//清空缓存//缓冲区数据置空,长度置0c->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或者监听fdint 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<stdio.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<pthread.h>
#include<unistd.h>
#include<poll.h>
#include<sys/epoll.h>
#include<sys/time.h>#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 modstruct 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接收数据并创建对应地址的客户端fdint 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);//关闭断开的客户端fdclose(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]);#endifint count = 0;if(conn_list[fd].status == 1){//状态为发送数据阶段时//调用send函数将发送缓冲区中的数据发送至客户端fdcount = 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.0servaddr.sin_port = htons(port); //0-1023//绑定套接字到服务器地址和端口if(-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))){printf("bind failed: %s\n", strerror(errno));}//开启监听套接字,最大等待队列为10listen(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//创建数组存储就绪事件,初始化为0struct 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<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<sys/sendfile.h>
#include<errno.h>
#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""<html><head><title>Avogado6</title></head><body><h1>Avogado6</h1></body></html>\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){//清空缓存//缓冲区数据置空,长度置0c->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){//清空缓存//缓冲区数据置空,长度置0c->wlength = 0;memset(c->wbuffer, 0, BUFFER_LENGTH);//发送数据结束,更新状态至准备阶段,准备下一次发送数据c->status = 0;}//发送完成,关闭文件描述符close(filefd);#endifreturn c->wlength;
}