【TinyWebServer】HTTP连接处理

news/2025/7/8 16:53:47

epoll

epoll_create函数

epoll_ctl函数

events

epoll_wait函数

select/poll/epoll

ET、LT、EPOLLONESHOT

HTTP报文格式

请求报文

响应报文

HTTP状态码

有限状态机

http处理流程

http报文处理流程

http类

epoll相关代码

服务器接收http请求

流程图与状态机

主状态机

从状态机

代码分析-http报文解析

HTTP_CODE含义

解析报文整体流程

​​核心流程与关键逻辑​​

​​1. 初始化状态变量​

2. 循环解析行数据​

4. 更新状态机位置​

​​5. 主状态机状态转移(核心解析逻辑)​​

从状态机逻辑

主状态机逻辑

《parse_request_line 函数》:解析HTTP请求行​​

《parse_headers 函数》:解析HTTP请求头​​

代码分析

do_request

process_write

http_conn::write


epoll

epoll_create函数

#include <sys/epoll.h>int epoll_create(int size)

创建一个指示epoll内核事件表的文件描述符,该描述符将用作其他epoll系统调用的第一个参数,size不起作用。

epoll_ctl函数

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

该函数用于操作内核事件表监控的文件描述符上的事件:注册、修改、删除

  • epfd:为epoll_creat的句柄

  • op:表示动作,用3个宏来表示:

    • EPOLL_CTL_ADD (注册新的fd到epfd),

    • EPOLL_CTL_MOD (修改已经注册的fd的监听事件),

    • EPOLL_CTL_DEL (从epfd删除一个fd);

  • event:告诉内核需要监听的事件

events

  • events描述事件类型,其中epoll事件类型有以下几种

    • EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)

    • EPOLLOUT:表示对应的文件描述符可以写

    • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)

    • EPOLLERR:表示对应的文件描述符发生错误

    • EPOLLHUP:表示对应的文件描述符被挂断;

    • EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的

    • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

epoll_wait函数

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

该函数用于等待所监控文件描述符上有事件的产生,返回就绪的文件描述符个数

  • events:用来存内核得到事件的集合,

  • maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,

  • timeout:是超时时间

    • -1:阻塞

    • 0:立即返回,非阻塞

    • >0:指定毫秒

  • 返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1

select/poll/epoll

  • 调用函数

    • select和poll都是一个函数,epoll是一组函数

  • 文件描述符数量

    • select通过线性表描述文件描述符集合,文件描述符有上限,一般是1024,但可以修改源码,重新编译内核,不推荐

    • poll是链表描述,突破了文件描述符上限,最大可以打开文件的数目

    • epoll通过红黑树描述,最大可以打开文件的数目,可以通过命令ulimit -n number修改,仅对当前终端有效

  • 将文件描述符从用户传给内核

    • select和poll通过将所有文件描述符拷贝到内核态,每次调用都需要拷贝

    • epoll通过epoll_create建立一棵红黑树,通过epoll_ctl将要监听的文件描述符注册到红黑树上

  • 内核判断就绪的文件描述符

    • select和poll通过遍历文件描述符集合,判断哪个文件描述符上有事件发生

    • epoll_create时,内核除了帮我们在epoll文件系统里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。

    • epoll是根据每个fd上面的回调函数(中断函数)判断,只有发生了事件的socket才会主动的去调用 callback函数,其他空闲状态socket则不会,若是就绪事件,插入list

  • 应用程序索引就绪文件描述符

    • select/poll只返回发生了事件的文件描述符的个数,若知道是哪个发生了事件,同样需要遍历

    • epoll返回的发生了事件的个数和结构体数组,结构体包含socket的信息,因此直接处理返回的数组即可

  • 工作模式

    • select和poll都只能工作在相对低效的LT模式下

    • epoll则可以工作在ET高效模式,并且epoll还支持EPOLLONESHOT事件,该事件能进一步减少可读、可写和异常事件被触发的次数。 

  • 应用场景

    • 当所有的fd都是活跃连接,使用epoll,需要建立文件系统,红黑书和链表对于此来说,效率反而不高,不如selece和poll

    • 当监测的fd数目较小,且各个fd都比较活跃,建议使用select或者poll

    • 当监测的fd数目非常大,成千上万,且单位时间只有其中的一部分fd处于就绪状态,这个时候使用epoll能够明显提升性能

ET、LT、EPOLLONESHOT

  • LT水平触发模式

    • epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序可以不立即处理该事件。

    • 当下一次调用epoll_wait时,epoll_wait还会再次向应用程序报告此事件,直至被处理

  • ET边缘触发模式

    • epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序必须立即处理该事件

    • 必须要一次性将数据读取完,使用非阻塞I/O,读取到出现eagain

  • EPOLLONESHOT

    • 一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket

    • 我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件

HTTP报文格式

HTTP报文分为请求报文和响应报文两种,每种报文必须按照特有格式生成,才能被浏览器端识别。

其中,浏览器端向服务器发送的为请求报文,服务器处理后返回给浏览器端的为响应报文。

请求报文

HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。

其中,请求分为两种,GET和POST,具体的:

  • GET

GET /562f25980001b1b106000338.jpg HTTP/1.1
Host:img.mukewang.com
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept:image/webp,image/*,*/*;q=0.8
Referer:http://www.imooc.com/
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8请求数据为空

  • POST
POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive
空行
name=Professional%20Ajax&publisher=Wiley
  • 请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本。
    GET说明请求类型为GET,/562f25980001b1b106000338.jpg(URL)为要访问的资源,该行的最后一部分说明使用的是HTTP1.1版本。

  • 请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息。

    • HOST,给出请求资源所在服务器的域名。

    • User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等。

    • Accept,说明用户代理可处理的媒体类型。

    • Accept-Encoding,说明用户代理支持的内容编码。

    • Accept-Language,说明用户代理能够处理的自然语言集。

    • Content-Type,说明实现主体的媒体类型。

    • Content-Length,说明实现主体的大小。

    • Connection,连接管理,可以是Keep-Alive或close。

  • 空行,请求头部后面的空行是必须的即使第四部分的请求数据为空,也必须有空行。

  • 请求数据也叫主体,可以添加任意的其他数据。

响应报文

HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

HTTP/1.1 200 OK
Date: Fri, 22 May 2009 06:07:21 GMT
Content-Type: text/html; charset=UTF-8
空行
<html><head></head><body><!--body goes here--></body>
</html>
  • 状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。
    第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为OK。

  • 消息报头,用来说明客户端要使用的一些附加信息。
    第二行和第三行为消息报头,Date:生成响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8。

  • 空行,消息报头后面的空行是必须的。

  • 响应正文,服务器返回给客户端的文本信息。空行后面的html部分为响应正文。

HTTP状态码

HTTP有5种类型的状态码,具体的:

  • 1xx:指示信息--表示请求已接收,继续处理。

  • 2xx:成功--表示请求正常处理完毕。

    • 200 OK:客户端请求被正常处理。

    • 206 Partial content:客户端进行了范围请求。

  • 3xx:重定向--要完成请求必须进行更进一步的操作。

    • 301 Moved Permanently:永久重定向,该资源已被永久移动到新位置,将来任何对该资源的访问都要使用本响应返回的若干个URI之一。

    • 302 Found:临时重定向,请求的资源现在临时从不同的URI中获得。

  • 4xx:客户端错误--请求有语法错误,服务器无法处理请求。

    • 400 Bad Request:请求报文存在语法错误。

    • 403 Forbidden:请求被服务器拒绝。

    • 404 Not Found:请求不存在,服务器上找不到请求的资源。

  • 5xx:服务器端错误--服务器处理请求出错。

    • 500 Internal Server Error:服务器在执行请求时出现错误。

有限状态机

有限状态机,是一种抽象的理论模型,它能够把有限个变量描述的状态变化过程,以可构造可验证的方式呈现出来。比如,封闭的有向图。

有限状态机可以通过if-else,switch-case和函数指针来实现,从软件工程的角度看,主要是为了封装逻辑。

带有状态转移的有限状态机示例代码。

STATE_MACHINE() {State cur_State = type_A;while (cur_State != type_C) {Package _pack = getNewPackage();switch (cur_State) { case type_A:process_pkg_state_A(_pack);cur_State = type_B;break;case type_B:process_pkg_state_B(_pack);cur_State = type_C;break;}}
}

该状态机包含三种状态:type_A,type_B和type_C。其中,type_A是初始状态,type_C是结束状态。

状态机的当前状态记录在cur_State变量中,逻辑处理时,状态机先通过getNewPackage获取数据包,然后根据当前状态对数据进行处理,处理完后,状态机通过改变cur_State完成状态转移。

有限状态机一种逻辑单元内部的一种高效编程方法,在服务器编程中,服务器可以根据不同状态或者消息类型进行相应的处理逻辑,使得程序逻辑清晰易懂。

http处理流程

首先对http报文处理的流程进行简要介绍,然后具体介绍http类的定义和服务器接收http请求的具体过程。

http报文处理流程

  • 浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。

  • 工作线程取出任务后,调用process_read函数,通过主、从状态机对请求报文进行解析。

  • 解析完之后,跳转do_request函数生成响应报文,通过process_write写入buffer,返回给浏览器端。

http类

这一部分代码在TinyWebServer/http/http_conn.h中,主要是http类的定义。

class http_conn{public://设置读取文件的名称m_real_file大小static const int FILENAME_LEN=200;//设置读缓冲区m_read_buf大小static const int READ_BUFFER_SIZE=2048;//设置写缓冲区m_write_buf大小static const int WRITE_BUFFER_SIZE=1024;//报文的请求方法,本项目只用到GET和POSTenum METHOD{GET=0,POST,HEAD,PUT,DELETE,TRACE,OPTIONS,CONNECT,PATH};//主状态机的状态enum CHECK_STATE{CHECK_STATE_REQUESTLINE=0,CHECK_STATE_HEADER,CHECK_STATE_CONTENT};//报文解析的结果enum HTTP_CODE{NO_REQUEST,GET_REQUEST,BAD_REQUEST,NO_RESOURCE,FORBIDDEN_REQUEST,FILE_REQUEST,INTERNAL_ERROR,CLOSED_CONNECTION};//从状态机的状态enum LINE_STATUS{LINE_OK=0,LINE_BAD,LINE_OPEN};public:http_conn(){}~http_conn(){}public://初始化套接字地址,函数内部会调用私有方法initvoid init(int sockfd,const sockaddr_in &addr);//关闭http连接void close_conn(bool real_close=true);void process();//读取浏览器端发来的全部数据bool read_once();//响应报文写入函数bool write();sockaddr_in *get_address(){return &m_address;  }//同步线程初始化数据库读取表void initmysql_result();//CGI使用线程池初始化数据库表void initresultFile(connection_pool *connPool);private:void init();//从m_read_buf读取,并处理请求报文HTTP_CODE process_read();//向m_write_buf写入响应报文数据bool process_write(HTTP_CODE ret);//主状态机解析报文中的请求行数据HTTP_CODE parse_request_line(char *text);//主状态机解析报文中的请求头数据HTTP_CODE parse_headers(char *text);//主状态机解析报文中的请求内容HTTP_CODE parse_content(char *text);//生成响应报文HTTP_CODE do_request();//m_start_line是已经解析的字符//get_line用于将指针向后偏移,指向未处理的字符char* get_line(){return m_read_buf+m_start_line;};//从状态机读取一行,分析是请求报文的哪一部分LINE_STATUS parse_line();void unmap();//根据响应报文格式,生成对应8个部分,以下函数均由do_request调用bool add_response(const char* format,...);bool add_content(const char* content);bool add_status_line(int status,const char* title);bool add_headers(int content_length);bool add_content_type();bool add_content_length(int content_length);bool add_linger();bool add_blank_line();public:static int m_epollfd;static int m_user_count;MYSQL *mysql;private:int m_sockfd;sockaddr_in m_address;//存储读取的请求报文数据char m_read_buf[READ_BUFFER_SIZE];//缓冲区中m_read_buf中数据的最后一个字节的下一个位置int m_read_idx;//m_read_buf读取的位置m_checked_idxint m_checked_idx;//m_read_buf中已经解析的字符个数int m_start_line;//存储发出的响应报文数据char m_write_buf[WRITE_BUFFER_SIZE];//指示buffer中的长度int m_write_idx;//主状态机的状态CHECK_STATE m_check_state;//请求方法METHOD m_method;//以下为解析请求报文中对应的6个变量//存储读取文件的名称char m_real_file[FILENAME_LEN];char *m_url;char *m_version;char *m_host;int m_content_length;bool m_linger;char *m_file_address;        //读取服务器上的文件地址struct stat m_file_stat;struct iovec m_iv[2];        //io向量机制iovecint m_iv_count;int cgi;                    //是否启用的POSTchar *m_string;                //存储请求头数据int bytes_to_send;          //剩余发送字节数int bytes_have_send;        //已发送字节数
};

 在http请求接收部分,会涉及到init和read_once函数,但init仅仅是对私有成员变量进行初始化,不用过多讲解。

这里,对read_once进行介绍。read_once读取浏览器端发送来的请求报文,直到无数据可读或对方关闭连接,读取到m_read_buffer中,并更新m_read_idx。

 

bool http_conn::read_once()
{if(m_read_idx>=READ_BUFFER_SIZE){return false;}int bytes_read=0;while(true){//从套接字接收数据,存储在m_read_buf缓冲区bytes_read=recv(m_sockfd,m_read_buf+m_read_idx,READ_BUFFER_SIZE-m_read_idx,0);if(bytes_read==-1)    {    //非阻塞ET模式下,需要一次性将数据读完if(errno==EAGAIN||errno==EWOULDBLOCK)break;return false;}else if(bytes_read==0){return false;}//修改m_read_idx的读取字节数m_read_idx+=bytes_read;}return true;
}

epoll相关代码

项目中epoll相关代码部分包括非阻塞模式、内核事件表注册事件、删除事件、重置EPOLLONESHOT事件四种。

  • 非阻塞模式

//对文件描述符设置非阻塞
int setnonblocking(int fd)
{int old_option = fcntl(fd, F_GETFL);int new_option = old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option;
}
  • 内核事件表注册新事件,开启EPOLLONESHOT,针对客户端连接的描述符,listenfd不用开启
void addfd(int epollfd, int fd, bool one_shot)
{epoll_event event;event.data.fd = fd;#ifdef ETevent.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
#endif#ifdef LTevent.events = EPOLLIN | EPOLLRDHUP;
#endifif (one_shot)event.events |= EPOLLONESHOT;epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);setnonblocking(fd);
}
  • 内核事件表删除事件

void removefd(int epollfd, int fd)
{epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);close(fd);
}
  • 重置EPOLLONESHOT事件
void modfd(int epollfd, int fd, int ev)
{epoll_event event;event.data.fd = fd;#ifdef ETevent.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
#endif#ifdef LTevent.events = ev | EPOLLONESHOT | EPOLLRDHUP;
#endifepoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}

服务器接收http请求

步骤1:定义连接对象(http_conn类)​

​目的​​:封装单个客户端连接的状态(如套接字、缓冲区、请求解析状态等),提供初始化、读写、关闭等接口。

​关键成员​​:

  • int m_fd:客户端套接字描述符。
  • char m_read_buf[MAX_BUFFER]:读缓冲区(存储客户端发送的数据)。
  • int m_read_idx:读缓冲区当前有效数据的末尾索引。
  • static int m_epollfd:全局epoll文件描述符(所有连接共享)。
  • static int m_user_count:当前活跃连接数(用于限制最大并发)。

​关键方法​​:

  • init(int fd, sockaddr_in& addr):初始化连接对象(绑定套接字、地址,重置缓冲区等)。
  • read_once():从套接字读取数据到缓冲区(返回是否读取成功,如EOF或错误则返回false)。
  • close_conn():关闭连接并释放资源。

​步骤2:初始化全局资源​

​目的​​:创建epoll实例、监听套接字,并将监听套接字注册到epoll中。

​编码逻辑​​:

// 1. 创建连接对象池(预分配MAX_FD个连接)
http_conn* users = new http_conn[MAX_FD]; // 2. 初始化全局变量(epollfd、连接计数)
http_conn::m_epollfd = -1;
http_conn::m_user_count = 0;// 3. 创建监听套接字(TCP)
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
assert(listenfd != -1);// 4. 设置监听套接字选项(可选:重用端口、非阻塞)
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
fcntl(listenfd, F_SETFL, fcntl(listenfd, F_GETFD, 0) | O_NONBLOCK); // 非阻塞(ET模式需要)// 5. 绑定地址并监听
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
bind(listenfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(listenfd, 5); // 监听队列长度// 6. 创建epoll实例(内核事件表)
int epollfd = epoll_create(5); // 参数为内核事件表的大小(实际可动态扩展)
assert(epollfd != -1);
http_conn::m_epollfd = epollfd; // 关联到连接对象的静态成员// 7. 将监听套接字添加到epoll事件表(LT默认触发)
struct epoll_event ev;
ev.data.fd = listenfd;
ev.events = EPOLLIN | EPOLLRDHUP; // 监听可读事件和对端关闭事件
epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev); // 注册事件

步骤3:主事件循环(epoll_wait)​

​目的​​:阻塞等待epoll事件就绪,循环处理所有触发的事件。

​编码逻辑​​:

bool stop_server = false; // 服务器停止标志(可通过信号触发)
while (!stop_server) {// 等待事件就绪(超时-1表示永久阻塞)int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);if (number < 0 && errno != EINTR) { // 错误处理(EINTR为信号中断)perror("epoll_wait error");break;}// 遍历所有就绪事件for (int i = 0; i < number; ++i) {int sockfd = events[i].data.fd; // 当前事件的套接字// 根据套接字类型分发处理if (sockfd == listenfd) { handle_new_connection(); // 处理新连接} else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) {handle_error_connection(sockfd); // 处理异常连接} else if (events[i].events & EPOLLIN) {handle_read_event(sockfd); // 处理读事件(客户端发送数据)}// 其他事件(如EPOLLOUT写事件,可选)}
}

步骤4:处理新连接(监听套接字触发)​

​目的​​:接受客户端连接,初始化http_conn对象,并根据LT/ET模式调整监听方式。

​关键逻辑(LT模式)​​:

void handle_new_connection() {struct sockaddr_in client_addr;socklen_t client_len = sizeof(client_addr);// 循环接受新连接(LT模式可能一次触发多个,但默认只处理一个)while (true) {int connfd = accept(listenfd, (struct sockaddr*)&client_addr, &client_len);if (connfd < 0) {if (errno == EAGAIN || errno == EWOULDBLOCK) { // LT模式下无更多连接可acceptbreak;}perror("accept error");continue;}// 检查连接数是否超限if (http_conn::m_user_count >= MAX_FD) {show_error(connfd, "Internal server busy"); // 返回错误响应close(connfd); // 关闭多余连接continue;}// 初始化连接对象(绑定套接字、地址,重置缓冲区)users[connfd].init(connfd, client_addr);http_conn::m_user_count++; // 连接数+1}
}

关键逻辑(ET模式)​​:
ET模式需循环accept直到无新连接(否则可能丢失事件):

void handle_new_connection_ET() {struct sockaddr_in client_addr;socklen_t client_len = sizeof(client_addr);while (true) {int connfd = accept(listenfd, (struct sockaddr*)&client_addr, &client_len);if (connfd < 0) break; // ET模式下无更多连接则退出循环// 同样检查连接数超限...users[connfd].init(connfd, client_addr);http_conn::m_user_count++;}
}

步骤5:处理客户端读事件(EPOLLIN触发)​

​目的​​:读取客户端发送的数据,解析HTTP请求,完成后提交到线程池异步处理。

​编码逻辑​​:

void handle_read_event(int sockfd) {// 获取对应的连接对象(假设sockfd是数组索引)http_conn* conn = &users[sockfd];// 读取数据(返回false表示读取失败或EOF)if (!conn->read_once()) {// 读取失败(如连接断开),关闭连接conn->close_conn();http_conn::m_user_count--;return;}// 解析HTTP请求(简化示例,实际需处理完整协议)if (conn->parse_request()) { // 请求解析完成,提交到线程池处理pool->append(conn); // 假设线程池支持任务队列} else {// 数据未读取完,LT模式下epoll会再次通知;ET模式需继续读取(但此处已用read_once)}
}

步骤6:处理异常事件(EPOLLRDHUP/EPOLLHUP/EPOLLERR)​

​目的​​:客户端主动关闭连接、网络错误等场景下的资源释放。

​编码逻辑​​:

void handle_error_connection(int sockfd) {// 查找对应的连接对象(需确保sockfd在数组范围内)if (sockfd < 0 || sockfd >= MAX_FD) return;http_conn* conn = &users[sockfd];// 关闭连接并清理资源conn->close_conn();http_conn::m_user_count--;
}

步骤7:线程池集成(异步处理请求)​

​目的​​:将客户端请求的处理(如解析HTTP、生成响应)放到线程池,避免阻塞epoll主线程。

​关键设计​​:

  • 线程池维护一组工作线程,从任务队列中取出http_conn*对象处理。
  • 处理完成后,若需要返回响应,调用send发送数据;若连接保持(长连接),则重新监听读事件。

流程图与状态机

从状态机负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。

主状态机

三种状态,标识解析位置。

  • CHECK_STATE_REQUESTLINE,解析请求行

  • CHECK_STATE_HEADER,解析请求头

  • CHECK_STATE_CONTENT,解析消息体,仅用于解析POST请求

从状态机

三种状态,标识解析一行的读取状态。

  • LINE_OK,完整读取一行

  • LINE_BAD,报文语法有误

  • LINE_OPEN,读取的行不完整

代码分析-http报文解析

上面中介绍了服务器接收http请求的流程与细节,简单来讲,浏览器端发出http连接请求,服务器端主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列后,工作线程从任务队列中取出一个任务进行处理。

各子线程通过process函数对任务进行处理,调用process_read函数和process_write函数分别完成报文解析与报文响应两个任务。

void http_conn::process()
{HTTP_CODE read_ret = process_read();// NO_REQUEST,表示请求不完整,需要继续接收请求数据if (read_ret == NO_REQUEST){// 注册并监听读事件(等待客户端继续发送数据)modfd(m_epollfd, m_sockfd, EPOLLIN);return;}// 调用process_write完成报文响应(根据读取结果生成响应并发送)bool write_ret = process_write(read_ret);if (!write_ret){close_conn(); // 响应发送失败,关闭连接}// 注册并监听写事件(可能用于后续持续发送数据,如分块传输)modfd(m_epollfd, m_sockfd, EPOLLOUT);
}

HTTP_CODE含义

表示HTTP请求的处理结果,在头文件中初始化了八种情形,在报文解析与响应中只用到了七种。

  • NO_REQUEST

    • 请求不完整,需要继续读取请求报文数据

    • 跳转主线程继续监测读事件

  • GET_REQUEST

    • 获得了完整的HTTP请求

    • 调用do_request完成请求资源映射

  • NO_RESOURCE

    • 请求资源不存在

    • 跳转process_write完成响应报文

  • BAD_REQUEST

    • HTTP请求报文有语法错误或请求资源为目录

    • 跳转process_write完成响应报文

  • FORBIDDEN_REQUEST

    • 请求资源禁止访问,没有读取权限

    • 跳转process_write完成响应报文

  • FILE_REQUEST

    • 请求资源可以正常访问

    • 跳转process_write完成响应报文

  • INTERNAL_ERROR

    • 服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发

解析报文整体流程

process_read通过while循环,将主从状态机进行封装,对报文的每一行进行循环处理。

  • 判断条件

    • 主状态机转移到CHECK_STATE_CONTENT,该条件涉及解析消息体

    • 从状态机转移到LINE_OK,该条件涉及解析请求行和请求头部

    • 两者为或关系,当条件为真则继续循环,否则退出

  • 循环体

    • 从状态机读取数据

    • 调用get_line函数,通过m_start_line将从状态机读取数据间接赋给text

    • 主状态机解析text

//_start_line是行在buffer中的起始位置,将该位置后面的数据赋给text
//此时从状态机已提前将一行的末尾字符\r\n变为\0\0,所以text可以直接取出完整的行进行解析
char* get_line(){return m_read_buf+m_start_line;
}http_conn::HTTP_CODE http_conn::process_read()
{//初始化从状态机状态、HTTP请求解析结果LINE_STATUS line_status=LINE_OK;HTTP_CODE ret=NO_REQUEST;char* text=0;//这里为什么要写两个判断条件?第一个判断条件为什么这样写?//具体的在主状态机逻辑中会讲解。//parse_line为从状态机的具体实现while((m_check_state==CHECK_STATE_CONTENT && line_status==LINE_OK)||((line_status=parse_line())==LINE_OK)){text=get_line();//_start_line是每一个数据行在m_read_buf中的起始位置//m_checked_idx表示从状态机在m_read_buf中读取的位置m_start_line=m_checked_idx;//主状态机的三种状态转移逻辑switch(m_check_state){case CHECK_STATE_REQUESTLINE:{//解析请求行ret=parse_request_line(text);if(ret==BAD_REQUEST)return BAD_REQUEST;break;}case CHECK_STATE_HEADER:{//解析请求头ret=parse_headers(text);if(ret==BAD_REQUEST)return BAD_REQUEST;//完整解析GET请求后,跳转到报文响应函数else if(ret==GET_REQUEST){return do_request();}break;}case CHECK_STATE_CONTENT:{//解析消息体ret=parse_content(text);//完整解析POST请求后,跳转到报文响应函数if(ret==GET_REQUEST)return do_request();//解析完消息体即完成报文解析,避免再次进入循环,更新line_statusline_status=LINE_OPEN;break;}default:return INTERNAL_ERROR;}}return NO_REQUEST;
}

《process_read 方法》:读取数据后的状态机解析​

​功能​

驱动状态机逐行解析读取缓冲区中的HTTP请求数据(请求行→请求头→消息体),根据解析结果更新状态或生成响应。

​核心流程与关键逻辑​
​1. 初始化状态变量​
LINE_STATUS line_status = LINE_OK;  // 从状态机状态(行解析状态)
HTTP_CODE ret = NO_REQUEST;         // 主状态机返回值(请求解析结果)
char* text = 0;                     // 当前行的文本指针
  • LINE_STATUS:从状态机状态,标识当前行的解析状态(如LINE_OK表示行完整,LINE_OPEN表示行未闭合)。
  • HTTP_CODE:主状态机状态,标识HTTP请求的整体解析进度(如NO_REQUEST表示未完成,GET_REQUEST表示请求完成)。
2. 循环解析行数据​
while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK))

循环条件解析​​:

  • ​第一部分​​:m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK
    当主状态机处于CHECK_STATE_CONTENT(消息体解析状态)时,只要从状态机还能提取完整行(LINE_OK),就继续解析消息体(适用于POST请求的长消息体)。

  • ​第二部分​​:((line_status = parse_line()) == LINE_OK)
    其他状态(请求行/请求头解析)下,每次循环先调用parse_line提取新的一行(更新line_status),若提取成功(LINE_OK)则继续解析。

​设计意图​​:

  • 请求行/请求头是固定格式的短数据(每行一个字段),需逐行解析;
  • 消息体(如POST的表单数据)可能很长,需持续解析直到无更多数据(LINE_OPEN表示未闭合)。

​3. 提取当前行文本​

text = get_line();  // 获取当前行的起始地址(已替换\r\n为\0\0)

通过get_line获取当前行的文本指针,由于\r\n已被替换为\0\0text可直接作为字符串使用(如strncasecmp)。

4. 更新状态机位置​
m_start_line = m_checked_idx;  // 记录当前行的起始位置(供下一次循环使用)

m_checked_idx是从状态机已处理到的缓冲区位置,将其赋值给m_start_line,确保下一次循环时get_line能正确定位到下一行的起始位置。

​5. 主状态机状态转移(核心解析逻辑)​

根据m_check_state(主状态机当前状态),调用对应的解析函数处理当前行:

​(1) CHECK_STATE_REQUESTLINE:解析请求行​

case CHECK_STATE_REQUESTLINE:
{ret = parse_request_line(text);  // 解析请求行(提取方法、URL、版本)if (ret == BAD_REQUEST) return BAD_REQUEST;  // 解析失败,返回错误break;
}
  • 调用parse_request_line解析请求行(如GET /index.html HTTP/1.1\r\n);
  • 若解析失败(如格式错误),直接返回BAD_REQUEST
  • 解析成功后,主状态机自动进入下一步(由parse_request_line设置m_check_state = CHECK_STATE_HEADER)。

​(2) CHECK_STATE_HEADER:解析请求头​

case CHECK_STATE_HEADER:
{ret = parse_headers(text);  // 解析请求头(提取Connection、Content-length等)if (ret == BAD_REQUEST) return BAD_REQUEST;  // 解析失败,返回错误else if (ret == GET_REQUEST) return do_request();  // GET请求解析完成,生成响应break;
}
  • 调用parse_headers解析请求头(如Host: www.example.com\r\nContent-length: 1024\r\n);
  • 若解析失败(如未知头部),返回BAD_REQUEST
  • 若为GET请求且解析完成(ret == GET_REQUEST),调用do_request生成响应(无需处理消息体);
  • 若为POST请求,继续等待消息体(m_check_state仍为CHECK_STATE_HEADER,直到Content-length指定长度的数据读取完成)。

​(3) CHECK_STATE_CONTENT:解析消息体​

case CHECK_STATE_CONTENT:
{ret = parse_content(text);  // 解析消息体(提取POST表单数据等)if (ret == GET_REQUEST) return do_request();  // POST消息体解析完成,生成响应line_status = LINE_OPEN;  // 标记消息体未闭合,继续解析break;
}
  • 调用parse_content解析消息体(根据Content-length读取指定长度的数据);
  • 若消息体解析完成(ret == GET_REQUEST),调用do_request生成响应;
  • 若消息体未完全读取(如长数据分多次到达),设置line_status = LINE_OPEN,保持循环继续解析。

​(4) 默认情况​

default:return INTERNAL_ERROR;  // 未知状态,返回内部错误

从状态机逻辑

上一篇的基础知识讲解中,对于HTTP报文的讲解遗漏了一点细节,在这里作为补充。

在HTTP报文中,每一行的数据由\r\n作为结束字符,空行则是仅仅是字符\r\n。因此,可以通过查找\r\n将报文拆解成单独的行进行解析,项目中便是利用了这一点。

从状态机负责读取buffer中的数据,将每行数据末尾的\r\n置为\0\0,并更新从状态机在buffer中读取的位置m_checked_idx,以此来驱动主状态机解析。

  • 从状态机从m_read_buf中逐字节读取,判断当前字节是否为\r

    • 接下来的字符是\n,将\r\n修改成\0\0,将m_checked_idx指向下一行的开头,则返回LINE_OK

    • 接下来达到了buffer末尾,表示buffer还需要继续接收,返回LINE_OPEN

    • 否则,表示语法错误,返回LINE_BAD

  • 当前字节不是\r,判断是否是\n(一般是上次读取到\r就到了buffer末尾,没有接收完整,再次接收时会出现这种情况

    • 如果前一个字符是\r,则将\r\n修改成\0\0,将m_checked_idx指向下一行的开头,则返回LINE_OK

  • 当前字节既不是\r,也不是\n

    • 表示接收不完整,需要继续接收,返回LINE_OPEN

主状态机逻辑

主状态机初始状态是CHECK_STATE_REQUESTLINE,通过调用从状态机来驱动主状态机,在主状态机进行解析前,从状态机已经将每一行的末尾\r\n符号改为\0\0,以便于主状态机直接取出对应字符串进行处理。

  • CHECK_STATE_REQUESTLINE

    • 主状态机的初始状态,调用parse_request_line函数解析请求行

    • 解析函数从m_read_buf中解析HTTP请求行,获得请求方法、目标URL及HTTP版本号

    • 解析完成后主状态机的状态变为CHECK_STATE_HEADER

// 解析HTTP请求行,获得请求方法、目标URL及HTTP版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{// 在HTTP报文中,请求行格式为:方法 URL 版本\r\n,各部分由空格或制表符分隔// 使用strpbrk查找第一个空格或制表符的位置,用于分割方法和URLm_url = strpbrk(text, " \t");// 若未找到分隔符(m_url为NULL),说明请求行格式错误(缺少方法与URL的分隔)if (!m_url){return BAD_REQUEST;}// 将分隔符位置替换为'\0',使text到m_url(不包含)形成独立的请求方法字符串*m_url++ = '\0';// 提取请求方法(text指向方法的起始位置)char *method = text;if (strcasecmp(method, "GET") == 0)m_method = GET;       // 方法为GETelse if (strcasecmp(method, "POST") == 0){m_method = POST;      // 方法为POSTcgi = 1;              // 标记需要CGI处理(POST通常用于表单或动态内容)}elsereturn BAD_REQUEST;   // 不支持的方法,返回错误// 跳过URL前的空格和制表符(HTTP请求行中URL与方法间可能有多个空白)m_url += strspn(m_url, " \t");// 提取HTTP版本号:查找URL与版本间的分隔符(空格或制表符)m_version = strpbrk(m_url, " \t");if (!m_version)return BAD_REQUEST;   // 未找到版本分隔符,格式错误// 将分隔符位置替换为'\0',使m_url到m_version(不包含)形成独立的URL字符串*m_version++ = '\0';// 跳过版本号前的空格和制表符(版本与URL间可能有多个空白)m_version += strspn(m_version, " \t");// 仅支持HTTP/1.1协议,其他版本返回错误if (strcasecmp(m_version, "HTTP/1.1") != 0)return BAD_REQUEST;// 处理URL中的协议前缀(如http://或https://)// 若URL以"http://"开头(前7字符匹配),跳过该前缀if (strncasecmp(m_url, "http://", 7) == 0){m_url += 7;           // 移动指针到协议前缀之后m_url = strchr(m_url, '/');  // 查找第一个'/',定位资源路径起点}// 若URL以"https://"开头(前8字符匹配),跳过该前缀else if (strncasecmp(m_url, "https://", 8) == 0){m_url += 8;           // 移动指针到协议前缀之后m_url = strchr(m_url, '/');  // 查找第一个'/',定位资源路径起点}// 若URL处理后为空或不以'/'开头(如无资源路径),格式错误if (!m_url || m_url[0] != '/')return BAD_REQUEST;// 当URL为根路径"/"时,默认映射到欢迎页面"judge.html"if (strlen(m_url) == 1)strcat(m_url, "judge.html");// 请求行解析完成,主状态机转移到请求头解析状态m_check_state = CHECK_STATE_HEADER;return NO_REQUEST;  // 返回"请求未完成"(需继续解析请求头)
}

《parse_request_line 函数》:解析HTTP请求行​

​功能​

解析HTTP请求的第一行(请求行),提取请求方法(GET/POST)、目标URL和HTTP版本号,并根据解析结果设置状态机状态(转移到请求头解析)。

​关键逻辑步骤​

  1. ​分割方法与URL​​:
    使用strpbrk(text, " \t")找到请求行中第一个空格或制表符的位置(m_url),将其替换为\0,使text指向请求方法(如GET)。

  2. ​识别请求方法​​:

    • 若方法为GET,设置m_method=GET
    • 若为POST,设置m_method=POST并标记cgi=1(需CGI处理动态内容);
    • 否则返回BAD_REQUEST(不支持的方法)。
  3. ​提取并清洗URL​​:

    • 跳过URL前的空格/制表符(strspn(m_url, " \t"));
    • 处理URL中的协议前缀(如http://https://),跳过前缀后定位到资源路径(以/开头);
    • 若URL格式错误(无/或为空),返回BAD_REQUEST
  4. ​验证HTTP版本​​:
    仅支持HTTP/1.1,否则返回BAD_REQUEST

  5. ​状态转移​​:
    解析完成后,状态机转移到CHECK_STATE_HEADER(开始解析请求头)。

解析完请求行后,主状态机继续分析请求头。在报文中,请求头和空行的处理使用的同一个函数,这里通过判断当前的text首位是不是\0字符,若是,则表示当前处理的是空行,若不是,则表示当前处理的是请求头。

  • CHECK_STATE_HEADER

    • 调用parse_headers函数解析请求头部信息

    • 判断是空行还是请求头,若是空行,进而判断content-length是否为0,如果不是0,表明是POST请求,则状态转移到CHECK_STATE_CONTENT,否则说明是GET请求,则报文解析结束。

    • 若解析的是请求头部字段,则主要分析connection字段,content-length字段,其他字段可以直接跳过,各位也可以根据需求继续分析。

    • connection字段判断是keep-alive还是close,决定是长连接还是短连接

    • content-length字段,这里用于读取post请求的消息体长度


// 解析HTTP请求的一个头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{// 判断当前行是否为空行(HTTP请求头以空行\r\n结束)if (text[0] == '\0'){// 空行出现,说明请求头解析完成// 如果是POST请求(m_content_length不为0),需要跳转到消息体处理状态if (m_content_length != 0){m_check_state = CHECK_STATE_CONTENT;  // 状态转移到消息体解析return NO_REQUEST;  // 返回"请求未完成"(需继续读取消息体)}// 如果是GET请求(无消息体),直接返回请求完成标志return GET_REQUEST;}// 解析请求头部字段(格式为:字段名: 值\r\n)// 处理Connection字段(控制连接是否保持)else if (strncasecmp(text, "Connection:", 11) == 0){text += 11;  // 移动指针到字段值的起始位置(跳过"Connection:")// 跳过字段值前的空格和制表符(如" keep-alive"中的空格)text += strspn(text, " \t");// 检查是否为长连接(keep-alive)if (strcasecmp(text, "keep-alive") == 0){m_linger = true;  // 标记为长连接(后续读取数据时不立即关闭套接字)}}// 处理Content-length字段(指定消息体长度,仅POST请求可能有)else if (strncasecmp(text, "Content-length:", 15) == 0){text += 15;  // 移动指针到字段值的起始位置(跳过"Content-length:")// 跳过字段值前的空格和制表符(如" 1024"中的空格)text += strspn(text, " \t");// 将字段值转换为长整型,记录消息体长度(用于后续读取消息体)m_content_length = atol(text);}// 处理Host字段(指定请求的目标主机,HTTP/1.1必须包含)else if (strncasecmp(text, "Host:", 5) == 0){text += 5;  // 移动指针到字段值的起始位置(跳过"Host:")// 跳过字段值前的空格和制表符(如" www.example.com"中的空格)text += strspn(text, " \t");// 记录目标主机名(用于后续日志、路由或虚拟主机处理)m_host = text;}// 未知的请求头字段(实际应用中可扩展处理,如忽略或记录警告)else{printf("Oops unknown header: %s\n", text);  // 打印未知头信息(调试用)}// 请求头解析未完成(继续解析下一行)return NO_REQUEST;
}
```

《parse_headers 函数》:解析HTTP请求头​

​功能​

解析HTTP请求头(多行键值对,以空行\r\n结束),提取关键头部字段(如连接类型、内容长度、主机名),并根据解析结果更新连接状态或准备消息体处理。

​关键逻辑步骤​

  1. ​空行判断(请求头结束)​​:
    若当前行为空(text[0]='\0'),说明请求头解析完成:

    • m_content_length>0(POST请求有消息体),状态机转移到CHECK_STATE_CONTENT(准备解析消息体);
    • 否则(GET请求无消息体),返回GET_REQUEST(请求解析完成,可生成响应)。
  2. ​解析关键头部字段​​:

    • ​Connection字段​​:若值为keep-alive,设置m_linger=true(长连接,不立即关闭套接字);
    • ​Content-length字段​​:提取消息体长度(m_content_length=atol(text)),用于后续读取消息体;
    • ​Host字段​​:记录目标主机名(m_host=text),HTTP/1.1强制要求;
    • 未知头部字段:打印日志(调试用途)。
  3. ​状态保持​​:
    若仍有未解析的头部字段,返回NO_REQUEST(继续解析下一行)。

如果仅仅是GET请求,如项目中的欢迎界面,那么主状态机只设置之前的两个状态足矣。

GET和POST请求报文的区别之一是有无消息体部分,GET请求没有消息体,当解析完空行之后,便完成了报文的解析。

但后续的登录和注册功能,为了避免将用户名和密码直接暴露在URL中,我们在项目中改用了POST请求,将用户名和密码添加在报文中作为消息体进行了封装。

为此,我们需要在解析报文的部分添加解析消息体的模块。


代码分析

do_request

process_read函数的返回值是对请求的文件分析后的结果,一部分是语法错误导致的BAD_REQUEST,一部分是do_request的返回结果.该函数将网站根目录和url文件拼接,然后通过stat判断该文件属性。另外,为了提高访问速度,通过mmap进行映射,将普通文件映射到内存逻辑地址。

为了更好的理解请求资源的访问流程,这里对各种各页面跳转机制进行简要介绍。其中,浏览器网址栏中的字符,即url,可以将其抽象成ip:port/xxxxxx通过html文件的action属性进行设置。

m_url为请求报文中解析出的请求资源,以/开头,也就是/xxx,项目中解析后的m_url有8种情况。

  • /

    • GET请求,跳转到judge.html,即欢迎访问页面

  • /0

    • POST请求,跳转到register.html,即注册页面

  • /1

    • POST请求,跳转到log.html,即登录页面

  • /2CGISQL.cgi

    • POST请求,进行登录校验

    • 验证成功跳转到welcome.html,即资源请求成功页面

    • 验证失败跳转到logError.html,即登录失败页面

  • /3CGISQL.cgi

    • POST请求,进行注册校验

    • 注册成功跳转到log.html,即登录页面

    • 注册失败跳转到registerError.html,即注册失败页面

  • /5

    • POST请求,跳转到picture.html,即图片请求页面

  • /6

    • POST请求,跳转到video.html,即视频请求页面

  • /7

    • POST请求,跳转到fans.html,即关注页面

const char* doc_root="/home/qgy/github/ini_tinywebserver/root";http_conn::HTTP_CODE http_conn::do_request()
{strcpy(m_real_file,doc_root);int len=strlen(doc_root);const char *p = strrchr(m_url, '/'); if(cgi==1 && (*(p+1) == '2' || *(p+1) == '3')){}if(*(p+1) == '0'){char *m_url_real = (char *)malloc(sizeof(char) * 200);strcpy(m_url_real,"/register.html");strncpy(m_real_file+len,m_url_real,strlen(m_url_real));free(m_url_real);}else if( *(p+1) == '1'){char *m_url_real = (char *)malloc(sizeof(char) * 200);strcpy(m_url_real,"/log.html");strncpy(m_real_file+len,m_url_real,strlen(m_url_real));free(m_url_real);}elsestrncpy(m_real_file+len,m_url,FILENAME_LEN-len-1);if(stat(m_real_file,&m_file_stat)<0)return NO_RESOURCE;if(!(m_file_stat.st_mode&S_IROTH))return FORBIDDEN_REQUEST;if(S_ISDIR(m_file_stat.st_mode))return BAD_REQUEST;int fd=open(m_real_file,O_RDONLY);m_file_address=(char*)mmap(0,m_file_stat.st_size,PROT_READ,MAP_PRIVATE,fd,0);close(fd);return FILE_REQUEST;
}
process_write

根据do_request的返回状态,服务器子线程调用process_writem_write_buf中写入响应报文。

  • add_status_line函数,添加状态行:http/1.1 状态码 状态消息

  • add_headers函数添加消息报头,内部调用add_content_length和add_linger函数

    • content-length记录响应报文长度,用于浏览器端判断服务器是否发送完数据

    • connection记录连接状态,用于告诉浏览器端保持长连接

  • add_blank_line添加空行

上述涉及的5个函数,均是内部调用add_response函数更新m_write_idx指针和缓冲区m_write_buf中的内容。

bool http_conn::add_response(const char* format,...)
{//如果写入内容超出m_write_buf大小则报错if(m_write_idx>=WRITE_BUFFER_SIZE)return false;//定义可变参数列表va_list arg_list;//将变量arg_list初始化为传入参数va_start(arg_list,format);//将数据format从可变参数列表写入缓冲区写,返回写入数据的长度int len=vsnprintf(m_write_buf+m_write_idx,WRITE_BUFFER_SIZE-1-m_write_idx,format,arg_list);//如果写入的数据长度超过缓冲区剩余空间,则报错if(len>=(WRITE_BUFFER_SIZE-1-m_write_idx)){va_end(arg_list);return false;}//更新m_write_idx位置m_write_idx+=len;//清空可变参列表va_end(arg_list);return true;
}//添加状态行
bool http_conn::add_status_line(int status,const char* title)
{return add_response("%s %d %s\r\n","HTTP/1.1",status,title);
}//添加消息报头,具体的添加文本长度、连接状态和空行
bool http_conn::add_headers(int content_len)
{add_content_length(content_len);add_linger();add_blank_line();
}//添加Content-Length,表示响应报文的长度
bool http_conn::add_content_length(int content_len)
{return add_response("Content-Length:%d\r\n",content_len);
}//添加文本类型,这里是html
bool http_conn::add_content_type()
{return add_response("Content-Type:%s\r\n","text/html");
}//添加连接状态,通知浏览器端是保持连接还是关闭
bool http_conn::add_linger()
{return add_response("Connection:%s\r\n",(m_linger==true)?"keep-alive":"close");
}
//添加空行
bool http_conn::add_blank_line()
{return add_response("%s","\r\n");
}//添加文本content
bool http_conn::add_content(const char* content)
{return add_response("%s",content);
}

响应报文分为两种,一种是请求文件的存在,通过io向量机制iovec,声明两个iovec,第一个指向m_write_buf,第二个指向mmap的地址m_file_address;一种是请求出错,这时候只申请一个iovec,指向m_write_buf

  • iovec是一个结构体,里面有两个元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放的是writev将要发送的数据。

  • 成员iov_len表示实际写入的长度

bool http_conn::process_write(HTTP_CODE ret)
{switch(ret){case INTERNAL_ERROR:{add_status_line(500,error_500_title);add_headers(strlen(error_500_form));if(!add_content(error_500_form))return false;break;}case BAD_REQUEST:{add_status_line(404,error_404_title);add_headers(strlen(error_404_form));if(!add_content(error_404_form))return false;break;}case FORBIDDEN_REQUEST:{add_status_line(403,error_403_title);add_headers(strlen(error_403_form));if(!add_content(error_403_form))return false;break;}case FILE_REQUEST:{add_status_line(200,ok_200_title);if(m_file_stat.st_size!=0){add_headers(m_file_stat.st_size);m_iv[0].iov_base=m_write_buf;m_iv[0].iov_len=m_write_idx;m_iv[1].iov_base=m_file_address;m_iv[1].iov_len=m_file_stat.st_size;m_iv_count=2;bytes_to_send = m_write_idx + m_file_stat.st_size;return true;}else{const char* ok_string="<html><body></body></html>";add_headers(strlen(ok_string));if(!add_content(ok_string))return false;}}default:return false;}m_iv[0].iov_base=m_write_buf;m_iv[0].iov_len=m_write_idx;m_iv_count=1;return true;
}
http_conn::write

服务器子线程调用process_write完成响应报文,随后注册epollout事件。服务器主线程检测写事件,并调用http_conn::write函数将响应报文发送给浏览器端。

该函数具体逻辑如下:

在生成响应报文时初始化byte_to_send,包括头部信息和文件数据大小。通过writev函数循环发送响应报文数据,根据返回值更新byte_have_send和iovec结构体的指针和长度,并判断响应报文整体是否发送成功。

  • 若writev单次发送成功,更新byte_to_send和byte_have_send的大小,若响应报文整体发送成功,则取消mmap映射,并判断是否是长连接.

    • 长连接重置http类实例,注册读事件,不关闭连接,

    • 短连接直接关闭连接

  • 若writev单次发送不成功,判断是否是写缓冲区满了。

    • 若不是因为缓冲区满了而失败,取消mmap映射,关闭连接

    • 若eagain则满了,更新iovec结构体的指针和长度,并注册写事件,等待下一次写事件触发(当写缓冲区从不可写变为可写,触发epollout),因此在此期间无法立即接收到同一用户的下一请求,但可以保证连接的完整性。

bool http_conn::write()
{int temp = 0;int newadd = 0;//若要发送的数据长度为0//表示响应报文为空,一般不会出现这种情况if(bytes_to_send==0){modfd(m_epollfd,m_sockfd,EPOLLIN);init();return true;}while (1){   //将响应报文的状态行、消息头、空行和响应正文发送给浏览器端temp=writev(m_sockfd,m_iv,m_iv_count);//正常发送,temp为发送的字节数if (temp > 0){//更新已发送字节bytes_have_send += temp;//偏移文件iovec的指针newadd = bytes_have_send - m_write_idx;}if (temp <= -1){//判断缓冲区是否满了if (errno == EAGAIN){//第一个iovec头部信息的数据已发送完,发送第二个iovec数据if (bytes_have_send >= m_iv[0].iov_len){//不再继续发送头部信息m_iv[0].iov_len = 0;m_iv[1].iov_base = m_file_address + newadd;m_iv[1].iov_len = bytes_to_send;}//继续发送第一个iovec头部信息的数据else{m_iv[0].iov_base = m_write_buf + bytes_to_send;m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send;}//重新注册写事件modfd(m_epollfd, m_sockfd, EPOLLOUT);return true;}//如果发送失败,但不是缓冲区问题,取消映射unmap();return false;}//更新已发送字节数bytes_to_send -= temp;//判断条件,数据已全部发送完if (bytes_to_send <= 0){unmap();//在epoll树上重置EPOLLONESHOT事件modfd(m_epollfd,m_sockfd,EPOLLIN);//浏览器的请求为长连接if(m_linger){//重新初始化HTTP对象init();return true;}else{return false;}}}
}

https://dhexx.cn/news/show-5537771.html

相关文章

Prometheus + Grafana 监控 RabbitMQ 实践指南

文章目录 Prometheus Grafana 监控 RabbitMQ 实践教程一、前言二、环境搭建2.1 环境准备2.2 安装 Prometheus2.3 安装 Grafana 三、集成 RabbitMQ Exporter3.1 下载 RabbitMQ Exporter3.2 解压文件3.3 配置环境变量3.4 启动 RabbitMQ Exporter3.6 验证 Exporter 状态 四、Prom…

从零开始搭建现代化 Monorepo 开发模板:TypeScript + Rollup + Jest + 持续集成完整指南

在现代前端开发中&#xff0c;Monorepo&#xff08;单体仓库&#xff09;架构已经成为管理多个相关包的主流方案。无论是 React、Vue、还是 Angular 等知名框架&#xff0c;都采用了 Monorepo 的组织方式。本文将带您从零开始&#xff0c;一步步搭建一个功能完整的 Monorepo 开…

Java设计模式基础问答

面试过程中会让你介绍你项目或实习中使用的设计模式&#xff0c;你该如何说明 工厂模式 工厂模式是把对象创建的逻辑封装到一个工厂类里面&#xff0c;我们用工厂类来创建对象 为什么需要工厂类&#xff08;优点&#xff09;&#xff1a; 可以集中管理对象的创建规则。例如要…

pnpm install 和 npm install 的区别

pnpm install 和 npm install 的区别 pnpm install 和 npm install 都是用于安装项目依赖的命令&#xff0c;但它们在依赖管理机制、磁盘空间占用、安装速度和依赖安全性上有显著区别&#xff1a; 核心区别总结 特性npmpnpm依赖存储平铺结构&#xff08;嵌套改进版&#xff0…

Matlab点云合并函数pcmerge全解析

Matlab 中 pcmerge 函数的详细用法 1. 参数说明2. pcmerge函数调用示例2.1 示例1(对两个点云进行合并)2.2 示例2:对于大量点云数据的适用性分析3 结语1. 参数说明 pcmerge 是 MATLAB 提供的一个用于点云数据合并的函数。其主要作用是将两个点云对象按照指定的空间分辨率进行…

可视化在车间质量管控中的创新应用,提升品质

当某汽车工厂质检员还在用肉眼检查500个零件时&#xff0c;隔壁车间已用上“AI视觉检测系统”&#xff0c;在3秒内完成全车10万检测点筛查&#xff1b;某电子厂因人工漏检导致的百万级召回事件背后&#xff0c;藏着传统质检“看不见”的致命短板。本文将揭穿可视化质检的三大认…

行为设计模式之Memento(备忘录)

行为设计模式之Memento&#xff08;备忘录&#xff09; 前言&#xff1a; 备忘录设计模式&#xff0c;有点像vmware快照可以回滚&#xff0c;idea的提交记录同样可以混滚&#xff0c;流程引擎中流程可以撤销到或者回滚到某个指定的状态。 1&#xff09;意图 在不破坏封装性的…

Mac M4 芯片运行大模型指南,包括模型微调与推理

Mac M4 芯片运行大模型指南&#xff0c;模型微调与推理 背景模型推理 Ollama&#x1f50d; 举例说明&#xff1a;踩坑 模型微调 unsloth 背景 在国补、教育优惠、京东会员500优惠券等众多优惠之下。 我拿下了Macmini M4 16G 内存万兆网卡。在机器到手的第一时间&#xff0c;马…

第四章 RAG 知识库基础

代码仓库地址&#xff1a;https://github.com/Liucc-123/ai-agent 项目目标&#xff1a;通过项目实战掌握AI 大模型应用开发的常见知识&#xff0c;包括有&#xff1a;掌握AI 应用平台使用、AI 大模型接入、开发框架&#xff08;Spring AI LangChain4j&#xff09;、本地部署、…

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路

进入2025年以来&#xff0c;尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断&#xff0c;但全球市场热度依然高涨&#xff0c;入局者持续增加。 以国内市场为例&#xff0c;天眼查专业版数据显示&#xff0c;截至5月底&#xff0c;我国现存在业、存续状态的机器人相关企…