1. 引言
Nginx 以其高性能和高并发能力著称,其 HTTP 请求解析模块是理解其高效性的关键入口。当客户端发送一个 HTTP 请求时,Nginx 并不是一次性读取所有数据,而是采用事件驱动和状态机的方式,边读取、边解析。这种方式极大地提高了内存利用率和处理效率。
本文将深入 Nginx 1.24.0 的源代码,详细拆解 HTTP 请求解析过程中最核心的两个阶段:解析请求行(Request Line) 和 解析请求头(Request Headers)。我们将通过流程图和核心代码片段,逐步揭示状态机是如何在这些阶段之间流转的。
2. 核心数据结构:ngx_http_request_t
在深入解析流程之前,我们需要了解贯穿整个解析过程的核心数据结构——ngx_http_request_t。它代表一个 HTTP 请求,其中包含了与解析状态相关的关键字段。
// src/http/ngx_http_request.h
typedef struct ngx_http_request_s ngx_http_request_t;
struct ngx_http_request_s {
// ... 其他字段
// 当前解析状态,这是状态机的核心
ngx_uint_t state;
// 解析后的请求行信息
ngx_http_request_line_t request_line;
// 指向当前正在解析的请求头的链表
ngx_list_t headers_in;
// 请求行原始数据
u_char *request_start;
u_char *request_end;
u_char *uri_start;
u_char *uri_end;
u_char *schema_start;
u_char *schema_end;
u_char *host_start;
u_char *host_end;
u_char *port_start;
u_char *port_end;
u_char *method_start;
u_char *method_end;
u_char *key_start;
u_char *key_end;
u_char *value_start;
u_char *value_end;
// ... 其他字段
};
state 字段是状态机的核心,它的值决定了 Nginx 在接收到新数据时应该执行哪个解析逻辑。
3. 解析请求行:从零开始解析 HTTP 协议
HTTP 请求行的格式如下:
METHOD URI HTTP/VERSION\r\n
例如:GET /index.html HTTP/1.1\r\n
Nginx 解析请求行的状态机定义在 ngx_http_parse_request_line 函数中(位于 src/http/ngx_http_parse.c)。
3.1 状态机定义
解析请求行的状态枚举如下:
// src/http/ngx_http_parse.c
// 解析请求行的状态
#define sw_start 0
#define sw_method 1
#define sw_spaces_before_uri 2
#define sw_schema 3
#define sw_schema_slash 4
#define sw_schema_slash_slash 5
#define sw_host 6
#define sw_port 7
#define sw_host_end 8
#define sw_after_slash_in_uri 9
#define sw_check_uri 10
#define sw_uri 11
#define sw_http_09 12
#define sw_http_H 13
#define sw_http_HT 14
#define sw_http_HTT 15
#define sw_http_HTTP 16
#define sw_http_major 17
#define sw_http_dot 18
#define sw_http_minor 19
#define sw_http_end 20
#define sw_almost_we_are_done 21
3.2 解析流程图
3.3 核心代码解析
以下是 ngx_http_parse_request_line 函数的核心逻辑片段,展示了状态机如何根据当前状态和读取到的字符进行流转。
// src/http/ngx_http_parse.c (简化与注释)
ngx_int_t
ngx_http_parse_request_line(ngx_http_request_t *r, ngx_buf_t *b)
{
u_char ch, *p;
ngx_uint_t state;
state = r->state; // 从请求对象中获取当前状态
for (p = b->pos; p < b->last; p++) {
ch = *p;
switch (state) {
case sw_start:
// 状态 0:开始
if (ch == CR || ch == LF) {
// 忽略空行前的 CR/LF
break;
}
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) {
// 遇到字母,开始解析 Method
state = sw_method;
r->request_start = p; // 记录请求开始位置
r->method_start = p; // 记录 Method 开始位置
break;
}
// 非法字符
return NGX_HTTP_PARSE_INVALID_METHOD;
case sw_method:
// 状态 1:解析 Method
if (ch == ' ') {
// 遇到空格,Method 解析完毕
r->method_end = p;
state = sw_spaces_before_uri;
break;
}
if ((ch < 'A' || ch > 'Z') && (ch < 'a' || ch > 'z')) {
// Method 只能包含字母
return NGX_HTTP_PARSE_INVALID_METHOD;
}
// 继续积累 Method
break;
case sw_spaces_before_uri:
// 状态 2:跳过 Method 后的空格
if (ch == '/') {
// 遇到 '/',开始解析 URI 路径
r->uri_start = p;
state = sw_after_slash_in_uri;
break;
}
if (ch == ' ') {
// 继续跳过空格
break;
}
// 如果遇到字母,可能是绝对路径 (http://...)
if (ch == 'h') {
r->schema_start = p;
state = sw_schema;
break;
}
return NGX_HTTP_PARSE_INVALID_REQUEST;
// ... 中间状态 (sw_after_slash_in_uri, sw_uri, sw_http_H 等) 省略 ...
case sw_http_end:
// 状态 19:解析完次版本号后
if (ch == CR) {
// 遇到 CR,进入结束前状态
state = sw_almost_we_are_done;
break;
}
return NGX_HTTP_PARSE_INVALID_REQUEST;
case sw_almost_we_are_done:
// 状态 20:等待最后的 LF
r->request_end = p; // 记录请求结束位置
state = sw_start; // 重置状态,为下一个解析做准备
// 解析完成,返回成功
return NGX_OK;
}
}
// 数据读取完毕但解析未完成,保存状态并等待更多数据
r->state = state;
return NGX_AGAIN;
}
关键点:
NGX_AGAIN:这是 Nginx 事件驱动的精髓。当缓冲区数据不够时,函数返回NGX_AGAIN,并将当前state保存回r->state。当新数据到达时,解析器会从上次中断的state继续执行,而不是从头开始。- 指针记录:通过
method_start、uri_start等指针精确记录各个部分的起始和结束位置,避免了大量的内存拷贝。
4. 解析请求头:键值对的收集
请求行解析完成后,Nginx 进入解析请求头阶段。请求头的格式为:
Header-Name: Header-Value\r\n
并以一个空行 \r\n 结束。
4.1 状态机定义
解析请求头的状态定义在 ngx_http_parse_header_line 函数中。
// src/http/ngx_http_parse.c
// 解析请求头的状态
#define sw_header_start 22
#define sw_header_before_colon 23
#define sw_header_after_colon 24
#define sw_header_value 25
#define sw_header_done 26
4.2 解析流程图
4.3 核心代码解析
// src/http/ngx_http_parse.c (简化与注释)
ngx_int_t
ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b,
ngx_uint_t allow_underscores)
{
u_char ch, *p;
ngx_uint_t state;
ngx_table_elt_t *header;
state = r->state;
for (p = b->pos; p < b->last; p++) {
ch = *p;
switch (state) {
case sw_header_start:
// 状态 22:开始解析一个新的 Header
if (ch == CR) {
// 遇到 CR,说明是空行,Header 解析结束
state = sw_header_done;
break;
}
if (ch == LF) {
// 忽略 LF
break;
}
// 记录 Key 的开始位置
r->header_name_start = p;
state = sw_header_before_colon;
break;
case sw_header_before_colon:
// 状态 23:解析 Header Key,直到遇到 ':'
if (ch == ':') {
r->header_name_end = p;
state = sw_header_after_colon;
break;
}
if (ch == CR) {
// 不合法的 Header
return NGX_HTTP_PARSE_INVALID_HEADER;
}
// 检查 Key 中是否包含下划线(根据配置决定是否允许)
if (ch == '_' && !allow_underscores) {
return NGX_HTTP_PARSE_INVALID_HEADER;
}
// 继续积累 Key
break;
case sw_header_after_colon:
// 状态 24:跳过冒号后的空格
if (ch == ' ') {
break;
}
if (ch == CR) {
// Value 为空
state = sw_header_done;
break;
}
// 记录 Value 的开始位置
r->header_value_start = p;
state = sw_header_value;
break;
case sw_header_value:
// 状态 25:解析 Header Value
if (ch == CR) {
// 遇到 CR,Value 解析完毕
r->header_value_end = p;
state = sw_header_done;
break;
}
// 继续积累 Value
break;
case sw_header_done:
// 状态 26:一个 Header 解析完成
if (ch == LF) {
// 将解析好的 Header 存入链表
header = ngx_list_push(&r->headers_in.headers);
if (header == NULL) {
return NGX_ERROR;
}
header->key.data = r->header_name_start;
header->key.len = r->header_name_end - r->header_name_start;
header->value.data = r->header_value_start;
header->value.len = r->header_value_end - r->header_value_start;
header->lowcase_key = ngx_pnalloc(r->pool, header->key.len);
// ... 转换为小写 key 以便快速查找 ...
// 重置状态,准备解析下一个 Header
state = sw_header_start;
break;
}
// 处理续行 (obsolete line folding)
if (ch == ' ' || ch == '\t') {
// 续行,回到 sw_header_value 状态继续读取
state = sw_header_value;
break;
}
return NGX_HTTP_PARSE_INVALID_HEADER;
}
}
// 数据不够,保存状态
r->state = state;
return NGX_AGAIN;
}
关键点:
ngx_table_elt_t:每个解析出的 Header 被存储为一个ngx_table_elt_t结构体,并挂载到r->headers_in.headers这个链表中。该结构体包含key、value以及用于快速查找的lowcase_key。- 续行处理:虽然 HTTP/1.1 标准已废弃续行(将多行 Header 折叠为一行),但 Nginx 为了兼容性,仍然在
sw_header_done状态中处理了以空格或 Tab 开头的续行。
5. 总结
Nginx 的 HTTP 请求解析是一个精巧的有限状态机实现。通过将解析过程分解为多个细粒度的状态,Nginx 能够以流式的方式处理网络数据,无需等待完整的请求到达即可开始解析。
- 事件驱动:解析函数在数据不足时返回
NGX_AGAIN,保存当前状态,等待事件循环通知新数据到来。 - 零拷贝:通过指针记录各个字段在缓冲区中的位置,避免了大量的内存分配和数据拷贝。
- 健壮性:状态机对每个字符进行严格检查,能够快速识别并拒绝畸形的 HTTP 请求。
理解这个状态机模型,是深入理解 Nginx 高性能架构的第一步。希望本文的流程图和代码注释能帮助你更好地掌握这一核心机制。

355

被折叠的 条评论
为什么被折叠?



