16. 深入 Nginx 源码:HTTP 请求解析之旅

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 解析流程图

sw_start(0)

是字母 (A-Z, a-z)

是 CR ('\\r')

其他字符

是字母

是空格 (' ')

其他字符

是空格

是 '/'

是字母 (如 http://)

遇到空格

遇到 CR

依次匹配 H->HT->HTT->HTTP

读取主版本号

读取 '.'

读取次版本号

遇到 CR

遇到 LF

开始解析请求行

检查 state

读取第一个字符

进入 sw_method 状态
记录 method_start

空请求,返回错误

非法请求,返回错误

继续读取字符

继续积累 Method

记录 method_end
进入 sw_spaces_before_uri 状态

非法 Method,返回错误

读取 URI 前的空格

跳过

进入 sw_after_slash_in_uri 状态
记录 uri_start

进入 sw_schema 状态
记录 schema_start

解析 URI 路径

记录 uri_end
进入 sw_http_H 状态

HTTP/0.9 请求
进入 sw_http_09 状态

解析 HTTP 版本号

进入 sw_http_major 状态

进入 sw_http_dot 状态

进入 sw_http_minor 状态

进入 sw_http_end 状态

进入 sw_almost_we_are_done 状态

请求行解析完成!

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_starturi_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 解析流程图

sw_header_start(22)

是 CR ('\\r')

是 LF ('\
')

是空格或 Tab

其他字符

是 LF

其他

是 ':'

是 CR

其他字符

是空格或 Tab

是 CR

其他字符

是 CR

其他字符

是 LF

是空格或 Tab

其他

开始解析请求头

检查 state

读取第一个字符

进入 sw_header_done 状态

忽略,继续

处理续行 (folded header)
已废弃,但兼容

记录 key_start
进入 sw_header_before_colon 状态

检查下一个字符

所有请求头解析完成!
返回 NGX_OK

解析错误

读取 Header Key

记录 key_end
进入 sw_header_after_colon 状态

非法 Header,返回错误

继续积累 Key

跳过冒号后的空格

跳过

空 Header Value,进入 sw_header_done

记录 value_start
进入 sw_header_value 状态

读取 Header Value

记录 value_end
进入 sw_header_done 状态

继续积累 Value

检查下一个字符

一个 Header 解析完成!
保存到 headers_in 链表
状态重置为 sw_header_start

续行,回到 sw_header_value 状态

解析错误

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 这个链表中。该结构体包含 keyvalue 以及用于快速查找的 lowcase_key
  • 续行处理:虽然 HTTP/1.1 标准已废弃续行(将多行 Header 折叠为一行),但 Nginx 为了兼容性,仍然在 sw_header_done 状态中处理了以空格或 Tab 开头的续行。

5. 总结

Nginx 的 HTTP 请求解析是一个精巧的有限状态机实现。通过将解析过程分解为多个细粒度的状态,Nginx 能够以流式的方式处理网络数据,无需等待完整的请求到达即可开始解析。

  • 事件驱动:解析函数在数据不足时返回 NGX_AGAIN,保存当前状态,等待事件循环通知新数据到来。
  • 零拷贝:通过指针记录各个字段在缓冲区中的位置,避免了大量的内存分配和数据拷贝。
  • 健壮性:状态机对每个字符进行严格检查,能够快速识别并拒绝畸形的 HTTP 请求。

理解这个状态机模型,是深入理解 Nginx 高性能架构的第一步。希望本文的流程图和代码注释能帮助你更好地掌握这一核心机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chexus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值