一、基础知识
1. epoll
- 创建内核事件表:
int epoll_create(int size);
size:不起作用,只是给内核一个提示,告诉它事件表需要多大;
- 返回值:内核事件表的文件描述符;
- 修改内核事件表监控的文件描述符上的事件:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:内核事件表的文件描述符;
op:表示三种操作:注册(EPOLL_CTL_ADD)、修改(EPOLL_CTL_MOD)、删除(EPOLL_CTL_DEL);
event:需要监听的事件:
标识符 | 事件类型 |
EPOLLIN | 可读、对端 socket 关闭 |
EPOLLOUT | 可写 |
EPOLLPRI | 带外数据 |
EPOLLERR | 错误 |
EPOLLHUP | 文件描述符被挂断 |
EPOLLET | 边缘触发模式 |
EPOLLONESHOT | 只监听一次事件,每次见听完如需再次监听需重置 |
标识符 | 事件类型 |
EPOLLIN | 可读、对端 socket 关闭 |
EPOLLOUT | 可写 |
EPOLLPRI | 带外数据 |
EPOLLERR | 错误 |
EPOLLHUP | 文件描述符被挂断 |
EPOLLET | 边缘触发模式 |
EPOLLONESHOT | 只监听一次事件,每次见听完如需再次监听需重置 |
- 返回值:成功时返回 0 ,失败时返回 -1 并设置 errno 。
- 监听事件发生:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
events:存储内核中发生的事件;
maxevents:events的容量;
timeout:超时时间:-1 表示阻塞,0 表示立即返回(非阻塞),大于 0 表示毫秒;
- 返回值:就绪的文件描述符个数,超时时返回 0 ,出错时返回 -1 。
2. 再谈 I/O 复用
select:使用线性表描述文件描述符集合,存在上限,每次调用需要将所有文件描述符拷贝到内核态,需要遍历判断就绪事件,适用于少量活跃的 fd;
poll:使用链表描述文件描述符集合,不存在上限,每次调用需要将所有文件描述符拷贝到内核态,需要遍历判断就绪事件,适用于少量活跃的 fd;
epoll:使用红黑树描述文件描述符集合,存在上限,通过epoll_ctl将要监听的文件描述符注册到红黑树上,会将就绪事件存放在新建的链表中,适用于大量不活跃的 fd;
3. 触发模式和 EPOLLONESHOT
- LT :水平触发模式,当检测到就绪事件时,将其通知给应用程序,应用程序可以不立即处理该事件,等到下次调用
epoll_wait时会再次报告该事件;
- ET :边缘触发模式,当检测到就绪事件时,将其通知给应用程序,应用程序必须立即处理,并且需要一次性处理完;
- EPOLLONESHOT :当某个 socket 的数据分两次到达时,系统可能会唤醒两个不同的线程来进行处理,若开启 EPOLLONESHOT ,则对于某个 socket 来说,只会有一个线程处理其事件,其他线程不能插手,完成一次处理后需要重置 EPOLLONESHOT 。
4. HTTP 报文
HTTP 报文分为请求报文和响应报文,前者由浏览器发送给服务器,后者由服务器应答浏览器,请求报文又分为 GET 和 POST 两种:
- 第 1 行:请求行,用来说明请求类型、要访问的资源、所使用的 HTTP 版本;
- 第 2 - 8 行:请求头部,通常包含如下信息:
- Host :服务器所在的域名;
- User-Agent :HTTP 客户端程序的信息,由浏览器定义并自动发送;
- Accept :说明用户代理可处理的媒体类型;
- Accept-Encoding :说明用户代理支持的内容编码;
- Accept-Language :说明用户代理能够处理的自然语言集;
- Content-Type :说明实现主体的媒体类型;
- Content-Length:说明实现主题的大小;
- Connection :连接管理,可以是 Keep-Alive 或 close ;
- 第 9 行:空行;
- 第 10 行:请求数据,也叫主体,可以添加任意其他数据;
bash {.line-numbers} 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
- GET 的请求数据通常为空,POST 则包含要请求的信息。
响应报文主要有四个部分组成:状态行、消息报头、空行、响应正文:
- 第 1 行:状态行,由 HTTP 协议版本号、状态码、状态消息组成;
- 第 2 - 3 行:消息报头,用来说明客户端需要使用的附加信息:
- Date :生成响应的日期和时间;
- Content-Type :指定了 MIME 类型的 HTML ,编码类型是 UTF-8 ;
- 第 4 行:空行;
- 第 5 - 10 行:响应正文,为 HTML 语言。
5. HTTP 状态码
- 1xx :指示信息,表示请求已接收,继续处理;
- 2xx :成功,表示请求正常处理完毕:
- 200 OK :客户端请求被正常处理;
- 206 Partial Content :客户端进行了范围请求;
- 3xx :重定向,要完成请求需要进一步操作:
- 301 Moved Permanently :永久重定向,返回新的 URL ;
- 302 Found :临时重定向,返回临时的 URL ;
- 4xx :客户端错误:
- 400 Bad Request :语法错误;
- 403 Forbidden :请求被服务器拒绝;
- 404 Not Found :请求不存在,服务器上找不到请求的资源;
- 5xx :服务器端错误:
- 500 Internal Server Error :服务器执行时出错。
6. 有限状态机
有限状态机是一种抽象的理论模型,使用选择语句来实现。模型要求代码存在 n 个状态,使用当前状态
cur_state 来进行标记,每次处理完任务后都对其进行更改以实现状态跳转,代码如下:7. 主从状态机
主从状态机也是一种抽象的理论模型,在本项目中从状态机负责读取 HTTP 请求的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机,流程图如下:

流程图[2]
从模型的角度来看,从状态机负责通用的操作处理,主状态机负责特定的操作处理,主状态机需要使用从状态机提供的数据,从状态机需要被主状态机调用,每个状态机又是一个有限状态机。本项目中主从状态机各有三种状态:
- 主状态机:标识解析位置
- CHECK_STATE_REQUESTLINE :解析请求行;
- CHECK_STATE_HEADER :解析请求头;
- CHECK_STATE_CONTENT :解析消息体,仅用于解析 POST 请求;
- 从状态机:标识解析一行的读取状态:
- LINE_OK :完整读取一行;
- LINE_BAD :报文语法错误;
- LINE_OPEN :读取的行不完整。
8. HTTP_CODE
标识了 HTTP 请求的处理结果:
- NO_REQUEST :请求不完整,需要继续读取请求报文,跳转主程序继续检测可读事件;
- GET_REQUEST :获得了完整的请求,调用 do_request 完成请求资源映射;
- NO_RESOURCE :请求资源不存在,跳转 process_write 完成响应报文;
- BAD_REQUEST :语法错误或请求资源为目录,跳转 process_write 完成响应报文;
- FORBIDDEN_REQUEST :请求资源禁止访问,跳转 process_write 完成响应报文;
- FILE_REQUEST :请求资源可以正常访问,跳转 process_write 完成响应报文;
- INTERNAL_ERROR :服务器内部错误。
9. HTTP 处理流程

- 浏览器发出 HTTP 连接请求;
- 主线程创建 HTTP 对象,接收请求并将所有数据读入对应的缓存区;
- 主线程将 HTTP 对象插入任务队列;
- 工作线程从任务队列中取出一个任务;
- 工作线程调用 process_read 函数,通过主从状态机解析请求报文;
- 解析完成后,跳转 do_request 函数生成响应报文;
- 通过 process_write 写入缓存区;
- 发送数据给浏览器。
二、代码解析
1. HTTP 类
2. 读取客户数据
3. epoll 事件相关
主要有四个:设置非阻塞模式、注册事件、删除事件、重置 EPOLLONESHOT 事件:
4. 接收 HTTP 请求
浏览器发出 HTTP 连接,主线程创建 HTTP 对象以接受请求,并将所有数据读入对应的缓存区,然后将该对象插入工作队列,工作线程从工作队列中取出一个任务进行处理:
5. HTTP 报文解析
HTTP 报文解析流程如下:
process_read函数:通过 while 循环,将主从状态机进行封装,对报文的每一行进行处理:
- 将从状态设为
LINE_OK,作为循环的入口条件;
- 首先在循环中会解析请求行;
- 然后解析请求头,若是 GET 请求则到此为止,若是 POST 请求则继续;
- 解析消息体,同时处理从状态防止再次解析;
- 循环的判断条件有特殊含义,注释中已给出说明;
parse_line函数:从状态机,负责解析一行数据:
- 在 HTTP 报文中,每一行数据由 作为结束,据此可以判断行;
- 每次找到并处理 后,将其置为 \0\0 ;
- 读取中,若当前字符为 可能出现三种情况:
- 下一个字符为 ,将 m_checked_idx 指向下一行的开头,返回 LINE_OK ;
- 读到了缓存区末尾,标识还需要继续接收数据,返回 LINE_OPEN ;
- 其他,标识语法错误,返回 LINE_BAD ;
- 下一个字符为 ,将 m_checked_idx 指向下一行的开头,返回 LINE_OK ;
- 读到了缓存区末尾,标识还需要继续接收数据,返回 LINE_OPEN ;
- 其他,标识语法错误,返回 LINE_BAD ;
- 若当前字节为 ,则意味着中途没接收完整,对应上一种情况的第二条,此时判断前一个字符是否为 可;
- 若当前字符不为上述两种情况,则接收不完整,返回 LINE_OPEN ;
- 由于已将 改为了 \0\0 ,因此主状态机可以直接进行字符串处理;
get_line函数:用于将指针向后偏移,指向未处理的字符,即处理一行数据;
parse_request_line函数:主状态机,负责解析 HTTP 请求的请求行:
- 初始状态为 CHECK_STATE_REQUESTLINE ;
- 从
m_read_buf中解析 HTTP 请求行,获取请求方法、目标 URL 和 HTTP 版本号;
- 将状态改为 CHECK_STATE_HEADER ;
parse_headers函数:主状态机,负责解析 HTTP 请求的请求头:
- 由于请求头和空行的处理使用的同一个函数,因此需要通过根据当前的 text 首位是不是 \0 来判断是空行还是请求头;
- 请求头有多行,因此需要根据具体含义进行解析;
- 如果是 GET 请求,则到此为止;如果是 POST 请求,则还需解析消息体(将状态改为 CHECK_STATE_CONTENT);
parse_content函数:主状态机,负责解析 HTTP 请求的消息体;
6. HTTP 请求响应
HTTP 报文响应流程如下:
do_request函数:对解析后的请求进行分析,并对 URL 进行处理,返回请求的状态:
- / :GET 请求,跳转 judge.html ,即欢迎页面;
- /0 :POST 请求,跳转 register.html ,即注册页面;
- /1 :POST 请求,跳转 log.html ,即登录页面;
- /2 :POST 请求,进行登录校验,成功跳转 welcome.html ,即资源请求成功页面;失败跳转 logError.html ,即登陆失败页面;
- /3 :POST 请求,进行注册校验,跳转同上;
- /5 :POST 请求,跳转 picture.html ,即图片请求页面;
- /6 :POST 请求,跳转 video.html ,即视频请求页面;
- /7 :POST 请求,跳转 fans.html ,即关注页面;
- 若资源存在且访问正常,就将其映射到内存中准备发送;
add_response函数:构造响应报文的公共接口,被各类消息报头构造函数调用;
process_write函数:向m_write_buf中写入响应报文,响应报文分两种:
- 一种是文件存在,通过 io 向量机制 iovec 声明 2 个 iovec ,第一个指向
m_write_buf,第二个指向 mmap 的地址m_file_address;
- 第二种是请求出错,只申请一个 iovec ;
- 注册 EPOLLOUT 事件,服务器主线程监测到事件后调用
write函数;
write函数:将响应报文发送给浏览器端:
- 根据已发送数据大小判断发送是否完成;
- 根据 EAGAIN 判断缓冲区是否已满;
- 每次发送数据后需要更新已发送字数;
- 发送完成后需要重置 HTTP 对象并重置 EPOLLONESHOT 事件。
参考文献
原创存证 · 本地哈希 · ba54d3d66706...
算法:SHA-256
时间:2026-10-16
哈希:
ba54d3d6670630a94fb7d3c33b04697541167c19d9db000495d6b2aa3855f15c该信息用于辅助证明当前内容版本,不等同于法律公证或版权登记。
- 作者:BeZer0
- 链接:https://blog.bezer0.cn/%E8%80%83%E7%A0%94/3fb0bc13-b7ac-80e9-970d-fb23e11b6c2b
- 声明:本文为原创文章,采用 CC BY-NC-SA 4.0 协议,转载请注明作者和原文链接。

