Lazy loaded image
考研
tinyWebServer 学习笔记——二、HTTP 连接处理
字数 9018阅读时长≈ 23 分钟
2026-10-16
2026-10-16

一、基础知识

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 请求的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机,流程图如下:
1
流程图[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 处理流程

2
流程图[3]
  • 浏览器发出 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

该信息用于辅助证明当前内容版本,不等同于法律公证或版权登记。


评论
Loading...