llhttp
http_parser 到 llparse 的移植。
为什么?
让我们面对现实,http_parser 实际上已无法维护。即使 引入一个新的方法也会导致大量的代码变动。
本项目旨在:
- 使其可维护
- 可验证
- 在可能的情况下改进基准测试
更多详情请参阅 Fedor Indutny 在 JSConf EU 2019 上的演讲
如何?
随着时间的推移,人们尝试了各种改进 http_parser 代码库的方法。 然而,由于导致显著的性能下降,它们都失败了。
本项目是将 http_parser 移植到 TypeScript。使用 llparse 生成输出的 C 源文件,该文件可以被编译并与宿主程序(如 Node.js)链接。
性能
到目前为止,llhttp 的性能优于 http_parser:
| 输入大小 | 带宽 | 每秒请求数 | 时间 | |
|---|---|---|---|---|
| llhttp | 8192.00 mb | 1777.24 mb/s | 3583799.39 req/sec | 4.61 s |
| http_parser | 8192.00 mb | 694.66 mb/s | 1406180.33 req/sec | 11.79 s |
llhttp 的速度大约快 156%。
维护
llhttp 项目大约有 1400 行 TypeScript 代码用于描述解析器 本身,以及大约 450 行 C 代码和头文件用于提供辅助方法。 整个 http_parser 大约用 2500 行 C 代码实现,以及 436 行头文件。
llhttp 中的所有优化和多字符匹配都是自动生成的,因此不会增加任何额外的维护成本。相反, http_parser 的大部分代码都是手工优化和展开的。维护者不应描述 “如何”解析 HTTP 请求/响应,而应谨慎地在 http_parser 中实现新功能, 考虑可能的性能下降并手动优化新代码。
验证
状态机图在 llhttp 中被显式编码。llparse 会自动检查图中是否存在循环,以及输入范围(span)(如头部名称和值)的报告是否正确。未来, 可以执行额外的检查,以实现对 llhttp 更严格的验证。
用法
#include "stdio.h"
#include "llhttp.h"
#include "string.h"
int handle_on_message_complete(llhttp_t* parser) {
fprintf(stdout, "Message completed!\n");
return 0;
}
int main() {
llhttp_t parser;
llhttp_settings_t settings;
/*Initialize user callbacks and settings */
llhttp_settings_init(&settings);
/*Set user callback */
settings.on_message_complete = handle_on_message_complete;
/*Initialize the parser in HTTP_BOTH mode, meaning that it will select between
*HTTP_REQUEST and HTTP_RESPONSE parsing automatically while reading the first
*input.
*/
llhttp_init(&parser, HTTP_BOTH, &settings);
/*Parse request! */
const char* request = "GET / HTTP/1.1\r\n\r\n";
int request_len = strlen(request);
enum llhttp_errno err = llhttp_execute(&parser, request, request_len);
if (err == HPE_OK) {
fprintf(stdout, "Successfully parsed!\n");
} else {
fprintf(stderr, "Parse error: %s %s\n", llhttp_errno_name(err), llhttp_get_error_reason(&parser));
}
}
有关 API 用法的更多信息,请参阅 src/native/api.h。
API
llhttp_settings_t
设置对象包含解析器将调用的回调列表。
以下回调可以返回 0(正常继续)、-1(错误)或 HPE_PAUSED(暂停解析器):
on_message_begin: 当新的请求/响应开始时调用。on_message_complete: 当请求/响应被完全解析后调用。on_url_complete: 在 URL 解析完成后调用。on_method_complete: 在 HTTP 方法解析完成后调用。on_protocol_complete: 在协议解析完成后调用。on_version_complete: 在 HTTP 版本解析完成后调用。on_status_complete: 在状态码解析完成后调用。on_header_field_complete: 在头部名称解析完成后调用。on_header_value_complete: 在头部值解析完成后调用。on_chunk_header: 在新的 chunk 开始时调用。当前 chunk 长度存储在parser->content_length中。on_chunk_extension_name_complete: 在 chunk 扩展名称开始时调用。on_chunk_extension_value_complete: 在 chunk 扩展值开始时调用。on_chunk_complete: 在接收到新的 chunk 后调用。on_reset: 当在同一解析器上接收到新消息时,在on_message_complete之后且在on_message_begin之前调用。对于解析器的第一条消息,此回调不会被调用。
以下回调可以返回 0(正常继续)、-1(错误)或 HPE_USER(来自回调的错误):
on_url: 当接收到 URL 的另一个字符时调用。on_status: 当接收到 status 的另一个字符时调用。on_method: 当接收到 method 的另一个字符时调用。 当 parser 使用HTTP_BOTH创建且输入为 response 时,对于第一条消息的序列HTTP/也会调用此回调。on_protocol: 当接收到 protocol 的另一个字符时调用。on_version: 当接收到 version 的另一个字符时调用。on_header_field: 当接收到 header name 的另一个字符时调用。on_header_value: 当接收到 header value 的另一个字符时调用。on_chunk_extension_name: 当接收到 chunk extension name 的另一个字符时调用。on_chunk_extension_value: 当接收到 extension value 的另一个字符时调用。
回调 on_headers_complete 在 headers 完成时被调用,可以返回:
0: 正常继续。1: 假设 request/response 没有 body,并继续解析下一条消息。2: 假设没有 body(同上),并使llhttp_execute()返回HPE_PAUSED_UPGRADE。-1: 错误HPE_PAUSED: 暂停 parser。
void llhttp_init(llhttp_t* parser, llhttp_type_t type, const llhttp_settings_t* settings)
使用特定的类型和用户设置初始化解析器。
uint8_t llhttp_get_type(llhttp_t* parser)
返回解析器的类型。
uint8_t llhttp_get_http_major(llhttp_t* parser)
返回当前请求/响应的 HTTP 协议主版本号。
uint8_t llhttp_get_http_minor(llhttp_t* parser)
返回当前请求/响应的 HTTP 协议的次要版本号。
uint8_t llhttp_get_method(llhttp_t* parser)
返回当前请求的方法。
int llhttp_get_status_code(llhttp_t* parser)
返回当前响应的方法。
uint8_t llhttp_get_upgrade(llhttp_t* parser)
如果请求包含 Connection: upgrade 标头,则返回 1。
void llhttp_reset(llhttp_t* parser)
将已初始化的解析器重置回起始状态,同时保留现有的解析器类型、回调设置、用户数据以及宽松标志。
void llhttp_settings_init(llhttp_settings_t* settings)
初始化设置对象。
llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len)
解析完整或部分请求/响应,并在过程中调用用户回调。
如果 llhttp_data_cb 中任何一个返回的 errno 不等于 HPE_OK,则解析中断,
并且该 errno 将从 llhttp_execute() 返回。如果使用了 HPE_PAUSED 作为 errno,
则可以通过调用 llhttp_resume() 恢复执行。在这种情况下,输入应前进到解析器最后处理的字节,
该字节可通过 llhttp_get_error_pos() 获取。
在 CONNECT/Upgrade 请求/响应的特殊情况下,HPE_PAUSED_UPGRADE 会在
完全解析请求/响应后返回。如果用户希望继续解析,
他们需要调用 llhttp_resume_after_upgrade()。
如果此函数返回非暂停类型的错误,它将持续返回
相同的错误,直到调用 llhttp_init() 为止。
如果此函数返回 HPE_OK,则表示所有输入均已被消耗并解析。
llhttp_errno_t llhttp_finish(llhttp_t* parser)
当对端没有更多字节可发送时,应调用此方法(例如,关闭 TCP 连接的可读端)。
没有 Content-Length 的请求和其他消息可能需要将所有传入字节视为正文的一部分,直到连接的最后一个字节。
如果请求被安全终止,此方法将调用 on_message_complete() 回调。否则将返回一个错误代码。
int llhttp_message_needs_eof(const llhttp_t* parser)
如果传入的消息已解析至最后一个字节,且必须通过调用 llhttp_finish() 在 EOF 时完成,则返回 1。
int llhttp_should_keep_alive(const llhttp_t* parser)
如果最后成功解析的消息之后可能还有其他消息,则返回 1。
void llhttp_pause(llhttp_t* parser)
对 llhttp_execute() 的后续调用将返回 HPE_PAUSED 并设置
适当的错误原因。
不要从用户回调中调用此函数!如果需要暂停,用户回调必须返回
HPE_PAUSED。
void llhttp_resume(llhttp_t* parser)
可能在用户回调中的暂停之后被调用以恢复执行。
有关详细信息,请参阅上文 llhttp_execute()。
仅当 llhttp_execute() 返回 HPE_PAUSED 时调用此函数。
void llhttp_resume_after_upgrade(llhttp_t* parser)
可能在用户回调暂停后被调用以恢复执行。
有关详细信息,请参阅上文 llhttp_execute()。
仅当 llhttp_execute() 返回 HPE_PAUSED_UPGRADE 时调用此函数
llhttp_errno_t llhttp_get_errno(const llhttp_t* parser)
返回最新的错误。
const char* llhttp_get_error_reason(const llhttp_t* parser)
返回最近一次返回错误的文字说明。
用户回调在返回错误时应设置错误原因。详见
llhttp_set_error_reason()。
void llhttp_set_error_reason(llhttp_t* parser, const char* reason)
为返回的错误分配口头描述。必须在用户回调中调用,且需在返回 errno 之前立即调用。
HPE_USER 错误代码在用户回调中可能有用。
const char* llhttp_get_error_pos(const llhttp_t* parser)
返回指向在返回错误之前最后一个已解析字节的指针。该指针相对于 llhttp_execute() 的 data 参数。
此方法可能有助于统计已解析字节的数量。
const char* llhttp_errno_name(llhttp_errno_t err)
返回错误代码的文本名称。
const char* llhttp_method_name(llhttp_method_t method)
返回 HTTP 方法的文本名称。
const char* llhttp_status_name(llhttp_status_t status)
返回 HTTP 状态码的文本名称。
void llhttp_set_lenient_headers(llhttp_t* parser, int enabled)
启用/禁用宽松的头字段值解析(默认禁用)。 宽松解析会禁用头字段值的令牌检查,将 llhttp 的 协议支持扩展到高度不合规的客户端/服务器。
当宽松解析为“开启”时,
不会因错误的头字段值而抛出 HPE_INVALID_HEADER_TOKEN。
启用此标志可能会带来安全问题,因为您将暴露于请求走私攻击之下。请谨慎使用!
void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled)
启用/禁用对冲突的 Transfer-Encoding 和
Content-Length 头的宽松处理(默认禁用)。
通常,当 Transfer-Encoding 与
Content-Length 同时存在时,llhttp 会报错。
此错误对于防止 HTTP 请求走私至关重要,但在涉及少量遗留服务器的情况下可能不太理想。
启用此标志可能会带来安全问题,因为您将暴露于请求走私攻击之下。请谨慎使用!
void llhttp_set_lenient_keep_alive(llhttp_t* parser, int enabled)
启用/禁用对 Connection: close 和 HTTP/1.0
请求响应的宽松处理。
通常 llhttp 会在带有 Connection: close 和 Content-Length 的请求/响应之后
对 HTTP 请求/响应报错。
这对于防止缓存投毒攻击至关重要, 但可能与过时且不安全的客户端产生不良交互。
启用此标志后,额外的请求/响应将被正常解析。
启用此标志可能存在安全问题,因为您将暴露于投毒攻击之下。请谨慎使用!
void llhttp_set_lenient_transfer_encoding(llhttp_t* parser, int enabled)
启用/禁用对 Transfer-Encoding 头的宽松处理。
通常,当 llhttp 遇到 Transfer-Encoding 具有 chunked 值且其后还有另一个值时(无论是在单个头中,还是在多个头中,这些头的值在内部使用 , 连接),会报错。
这是规范所要求的,以便可靠地确定请求体大小,从而避免请求走私。
启用此标志后,额外的值将被正常解析。
启用此标志可能会带来安全问题,因为您将面临请求走私攻击的风险。请谨慎使用!
void llhttp_set_lenient_version(llhttp_t* parser, int enabled)
启用/禁用对 HTTP 版本的宽松处理。
通常,当请求或状态行中的 HTTP 版本不是 0.9、1.0、1.1 或 2.0 时,llhttp 会报错。
启用此标志后,额外的值将被正常解析。
启用此标志可能会带来安全问题,因为您将允许不支持的 HTTP 版本。请谨慎使用!
void llhttp_set_lenient_data_after_close(llhttp_t* parser, int enabled)
启用/禁用对消息结束后接收到的额外数据的宽松处理, 且 keep-alive 已禁用的情况。
通常,当消息包含值为 close 的 Connection 头时,llhttp 在接收到额外的意外数据时会报错。
启用此标志后,额外数据将被丢弃,而不会抛出错误。
启用此标志可能存在安全问题,因为您将面临投毒攻击的风险。请谨慎使用!
void llhttp_set_lenient_optional_lf_after_cr(llhttp_t* parser, int enabled)
启用/禁用对不完整 CRLF 序列的宽松处理。
通常,当请求行、状态行、头部或块头部的 CR 后未跟随 LF 时,llhttp 会报错。
启用此标志后,仅需要一个 CR 即可终止此类部分。
启用此标志可能存在安全风险,因为您将暴露于请求走私攻击之下。请谨慎使用!
void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled)
启用/禁用对行分隔符的宽松处理。
通常,当请求行、状态行、头部、块头或块数据以 LF 结尾且前面没有 CR 时,llhttp 会报错。
启用此标志后,仅需要一个 LF 即可终止这些部分。
启用此标志可能存在安全风险,因为您将暴露于请求走私攻击之下。请谨慎使用!
void llhttp_set_lenient_optional_crlf_after_chunk(llhttp_t* parser, int enabled)
启用/禁用对未通过 CRLF 分隔的 chunk 的宽松处理。
通常,llhttp 会在 chunk 数据之后、开始新 chunk 之前缺少 CRLF 时报错。
启用此标志后,新 chunk 可以紧接在上一 chunk 之后立即开始。
启用此标志可能存在安全问题,因为您将面临请求走私攻击的风险。请谨慎使用!
void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled)
启用/禁用对块大小后空格的宽松处理。
通常,当块大小后跟随一个或多个空格而非 CRLF 或 ; 时,llhttp 会报错。
启用此标志后,该检查将被禁用。
启用此标志可能存在安全风险,因为您将面临请求走私攻击。请谨慎使用!
void llhttp_set_lenient_header_value_relaxed(llhttp_t* parser, int enabled)
启用/禁用对头部值中控制字符的宽松处理。
通常,当头部值包含有效字符集(HTAB、SP、VCHAR、OBS_TEXT)之外的字符时,llhttp 会报错。启用此标志后,头部值中将接受控制字符(NULL、CR 和 LF 除外)。
这不会造成任何已知的安全问题,但会允许 RFC 9110 中视为“无效”的内容,因此默认情况下应避免使用。
构建说明
请确保已安装 Node.js、npm 和 npx。然后在项目目录下运行:
npm ci
make
与其他语言的绑定
- Lua: MunifTanjim/llhttp.lua
- Python: pallas/pyllhttp
- Ruby: metabahn/llhttp
- Rust: JackLiar/rust-llhttp
在 CMake 中使用
如果你想在 CMake 项目中将此库作为共享库使用,可以使用下面的代码片段。
FetchContent_Declare(llhttp
URL "https://github.com/nodejs/llhttp/archive/refs/tags/release/v8.1.0.tar.gz")
FetchContent_MakeAvailable(llhttp)
# Link with the llhttp_shared target
target_link_libraries(${EXAMPLE_PROJECT_NAME} ${PROJECT_LIBRARIES} llhttp_shared ${PROJECT_NAME})
如果你想在 CMake 项目中将此库作为静态库使用,你可以先设置一些缓存变量。
FetchContent_Declare(llhttp
URL "https://github.com/nodejs/llhttp/archive/refs/tags/release/v8.1.0.tar.gz")
set(LLHTTP_BUILD_SHARED_LIBS OFF CACHE INTERNAL "")
set(LLHTTP_BUILD_STATIC_LIBS ON CACHE INTERNAL "")
FetchContent_MakeAvailable(llhttp)
# Link with the llhttp_static target
target_link_libraries(${EXAMPLE_PROJECT_NAME} ${PROJECT_LIBRARIES} llhttp_static ${PROJECT_NAME})
如果使用 9.3.0 之前的版本,LLHTTP_BUILD_SHARED_LIBS 和 LLHTTP_BUILD_STATIC_LIBS 选项被称为 BUILD_SHARED_LIBS 和 BUILD_STATIC_LIBS,应改用这些选项。
请注意,直接使用 git 仓库(例如,通过 git 仓库 URL 和标签)将无法与 FetchContent_Declare 配合使用,因为 CMakeLists.txt 在构建之前需要字符串替换(例如,_RELEASE_)。
在 Windows 上构建
安装
choco install gitchoco install nodechoco install llvm(或从 Visual Studio 2019 安装程序中安装C++ Clang tools for Windows可选包)choco install make(或者如果你已安装 MinGW,则已捆绑提供)
- Ensure that
Clangandmakeare in your system path. - Using Git Bash, clone the repo to your preferred location.
- Cd into the cloned directory and run
npm ci - Run
make - Your
repo/builddirectory should now havelibllhttp.aandlibllhttp.sostatic and dynamic libraries. - When building your executable, you can link to these libraries. Make sure to set the build folder as an include path when building so you can reference the declarations in
repo/build/llhttp.h.
一个使用库进行链接的简单示例:
假设你在当前工作目录中有一个可执行文件 main.cpp,你会运行:clang++ -Os -g3 -Wall -Wextra -Wno-unused-parameter -I/path/to/llhttp/build main.cpp /path/to/llhttp/build/libllhttp.a -o main.exe。
如果你遇到 unresolved external symbol 链接器错误,你很可能是试图在未将其与来自 api.c 和 http.c 的目标文件进行链接的情况下构建 llhttp.c。
许可证
本软件采用 MIT 许可证授权。
版权所有 Fedor Indutny,2018 年。
特此授予免费许可,允许任何获得本软件及相关文档文件(以下简称“软件”)副本的人, 不受限制地处理该软件,包括但不限于使用、复制、修改、合并、发布、 分发、再许可和/或销售软件副本的权利,并允许向获得软件的人 授予上述权利,但须遵守以下条件:
上述版权声明和本许可声明应包含在软件的所有副本或重要部分中。
软件按“原样”提供,不提供任何形式的明示或暗示保证, 包括但不限于对适销性、特定用途适用性和非侵权的保证。在任何情况下, 作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是基于合同、 侵权或其他原因,均因软件或与软件的使用或其他交易有关而产生。