- 物联网
- 消息队列
- 后端
【免费下载链接】mosquitto
Eclipse Mosquitto - An open source MQTT broker
picohttpparser 是一个微型、原始、极快的 HTTP 请求/响应解析器,以"无状态、零内存分配"的独特设计著称,被广泛部署于 Perl 生态的 Plack、Starman、Starlet、Furl 等模块,也是 H2O 服务器的 HTTP/1 解析器。在 Mosquitto 仓库中,它被作为内置 WebSocket 支持的 HTTP 升级握手解析引擎,直接编译进 broker 与客户端库。读完本文,你将掌握 picohttpparser 四个核心 API 的完整调用方式、返回值语义、chunked 解码状态机,并理解其 SSE4.2 加速与抗 slowloris 设计的底层原理,同时看到它在 Mosquitto 中的真实落地代码。
设计哲学:无状态、零内存分配、零拷贝
picohttpparser 与绝大多数 HTTP 解析器的根本区别在于它的"原始"定位:
- 无状态:解析器本身不维护任何跨调用的状态,每次调用都是纯函数式操作;
- 不分配内存:它从不 malloc/free,调用方负责提供缓冲区;
- 零拷贝输出:解析结果不是复制出来的字符串,而是指向输入缓冲区内部位置的指针。调用方只需传入缓冲区指针和一个输出结构体,解析器会在输出结构体中设置指针,指向缓冲区中对应字段(方法、路径、头部名、头部值等)的起始位置与长度。
这意味着解析过程不产生任何中间副本,也天然免疫部分类型的缓冲区管理错误。由于头部名、头部值都带独立的长度字段(name_len/value_len),即便它们不是\0结尾的 C 字符串,也可以安全地用printf("%.*s", (int)len, ptr)方式输出。
该库以Perl License 或 MIT License 双重许可发布,允许在宽松条件下嵌入商业与非商业项目。
核心 API 一览
头文件 picohttpparser.h 对外暴露四个主要函数,外加一个状态查询辅助函数:
| 函数 | 作用 |
|---|---|
phr_parse_request | 解析 HTTP 请求,输出方法、路径、HTTP 次版本号与头部数组 |
phr_parse_response | 解析 HTTP 响应,输出 HTTP 次版本号、状态码、状态消息与头部数组 |
phr_parse_headers | 仅解析头部块(不关心请求行/状态行) |
phr_decode_chunked | 原地解码 chunked 传输编码的数据 |
phr_decode_chunked_is_in_data | 判断 chunked 解码器当前是否处于 chunk 数据区中间 |
三个解析函数的返回值语义
三个解析函数(phr_parse_request/phr_parse_response/phr_parse_headers)的返回值完全一致:
- 正数:解析成功,返回本次解析消耗的字节数(即请求/响应/头部块的长度);
-2:数据不完整(partial),需要继续读取更多字节后再次调用;-1:解析失败(语法错误)。
这一约定让调用方可以用非常简单的循环实现流式解析:读到数据 → 调用解析 → 若返回-2继续读 → 若返回-1报错退出。
输出结构体
/* contains name and value of a header (name == NULL if is a continuing line * of a multiline header */ struct phr_header { const char *name; size_t name_len; const char *value; size_t value_len; };注意头注释中的特殊约定:当一个头部是多行(folded)头部时,续行的name会被置为NULL、name_len为 0,应用层可以通过name == NULL识别续行。另外,尾部空格与水平制表符(SP/HTAB)会被自动剥离(见下文"尾随空白的剥离")。
chunked 解码器状态结构
/* should be zero-filled before start */ struct phr_chunked_decoder { size_t bytes_left_in_chunk; /* number of bytes left in current chunk */ char consume_trailer; /* if trailing headers should be consumed */ char _hex_count; char _state; };使用前必须将整个结构清零(如struct phr_chunked_decoder decoder = {};),bytes_left_in_chunk与consume_trailer是应用可读/可写的字段,_hex_count与_state是内部状态,不应直接操作。
phr_decode_chunked的返回值约定:返回-2表示数据不完整、需要继续喂入新数据;返回非负数表示已到达 chunked 数据末尾,该值是"剩余未解码的字节数"(起始位置由*bufsz给出);返回-1表示解析错误。
实战一:用phr_parse_request解析 HTTP 请求
以下示例完整继承自原文档:从 socket 循环read(2)读取请求,交给phr_parse_request解析,并打印方法、路径、HTTP 版本与所有头部。
char buf[4096], *method, *path; int pret, minor_version; struct phr_header headers[100]; size_t buflen = 0, prevbuflen = 0, method_len, path_len, num_headers; ssize_t rret; while (1) { /* read the request */ while ((rret = read(sock, buf + buflen, sizeof(buf) - buflen)) == -1 && errno == EINTR) ; if (rret <= 0) return IOError; prevbuflen = buflen; buflen += rret; /* parse the request */ num_headers = sizeof(headers) / sizeof(headers[0]); pret = phr_parse_request(buf, buflen, &method, &method_len, &path, &path_len, &minor_version, headers, &num_headers, prevbuflen); if (pret > 0) break; /* successfully parsed the request */ else if (pret == -1) return ParseError; /* request is incomplete, continue the loop */ assert(pret == -2); if (buflen == sizeof(buf)) return RequestIsTooLongError; } printf("request is %d bytes long\n", pret); printf("method is %.*s\n", (int)method_len, method); printf("path is %.*s\n", (int)path_len, path); printf("HTTP version is 1.%d\n", minor_version); printf("headers:\n"); for (i = 0; i != num_headers; ++i) { printf("%.*s: %.*s\n", (int)headers[i].name_len, headers[i].name, (int)headers[i].value_len, headers[i].value); }这段代码演示了 picohttpparser 的标准用法,有几个关键点值得注意:
prevbuflen的作用:这是上一次调用前缓冲区已有的长度。当last_len(即prevbuflen)非零时,解析器会先执行一次"快速完整性检查"(is_complete),只在最后 3 字节内扫描\r\n\r\n结束序列,从而无需重新扫描整个缓冲区即可判断请求是否完成(见 picohttpparser.c 中is_complete的实现)。这既是性能优化,也是对抗 slowloris 慢速攻击的快速计数器措施——源码注释明确写道 "a fast countermeasure against slowloris"。num_headers是输入输出双向参数:调用前传入headers数组的容量,调用后被改写为实际解析出的头部数量。如果头部数量超过容量,会返回-1。minor_version:解析器假定 HTTP 版本为1.x,只输出次版本号x。HTTP/1.1对应minor_version == 1。- 缓冲区复用:数据持续追加到同一块
buf中,配合返回的prevbuflen,可以反复调用而不丢失已读数据。
从实现层面看,phr_parse_request会先清空所有输出参数(*method = NULL、*num_headers = 0等),然后依次执行:跳过首个空行(兼容某些客户端在 POST 内容后追加的 CRLF)、解析请求行(方法 → 路径 → HTTP 版本)、最后解析头部块(见 picohttpparser.c 的parse_request与phr_parse_request)。解析成功后返回(int)(buf - buf_start),即消耗的字节数。
实战二:用phr_decode_chunked原地解码 chunked 数据
phr_decode_chunked的一个突出特性是原地解码(in-place):解码后的数据直接覆盖在输入缓冲区上,去除 chunk 尺寸行、扩展与 CRLF 分隔符,不产生任何额外内存。以下示例完整继承自原文档:
struct phr_chunked_decoder decoder = {}; /* zero-clear */ char *buf = malloc(4096); size_t size = 0, capacity = 4096, rsize; ssize_t rret, pret; /* set consume_trailer to 1 to discard the trailing header, or the application * should call phr_parse_headers to parse the trailing header */ decoder.consume_trailer = 1; do { /* expand the buffer if necessary */ if (size == capacity) { capacity *= 2; buf = realloc(buf, capacity); assert(buf != NULL); } /* read */ while ((rret = read(sock, buf + size, capacity - size)) == -1 && errno == EINTR) ; if (rret <= 0) return IOError; /* decode */ rsize = rret; pret = phr_decode_chunked(&decoder, buf + size, &rsize); if (pret == -1) return ParseError; size += rsize; } while (pret == -2); /* successfully decoded the chunked data */ assert(pret >= 0); printf("decoded data is at %p (%zu bytes)\n", buf, size);使用要点:
- 必须零初始化解码器:
struct phr_chunked_decoder decoder = {};,内部状态机字段从零开始; consume_trailer字段二选一:置 1 则解码器自动丢弃 chunked 尾随头部(trailer);置 0 则尾随头部会保留在缓冲区中,由应用自行调用phr_parse_headers解析;rsize是双向参数:调用前传入本次新读到数据的字节数,调用后被改写为解码后实际写入的有效字节数;- 循环条件:只要返回
-2(chunked 数据尚不完整),就继续读入新数据并再次调用;返回非负数时到达数据末尾,该非负值为剩余未解码字节数。
在实现层面,解码器是一个典型的有限状态机,内部枚举了六个状态(见 picohttpparser.c 的enum):
CHUNKED_IN_CHUNK_SIZE /* 读取 chunk 尺寸行(十六进制) */ CHUNKED_IN_CHUNK_EXT /* 处理 chunk 扩展(; 之后的部分) */ CHUNKED_IN_CHUNK_DATA /* 处于 chunk 数据区 */ CHUNKED_IN_CHUNK_CRLF /* 处理 chunk 数据后的 CRLF */ CHUNKED_IN_TRAILERS_LINE_HEAD CHUNKED_IN_TRAILERS_LINE_MIDDLE配合bytes_left_in_chunk字段跟踪当前 chunk 剩余字节数,解码器逐字节推进;phr_decode_chunked_is_in_data则用于查询当前是否正处于数据区中间(对某些需要立即消费数据的应用场景有用)。
性能背后的底层设计(源码级解析)
picohttpparser 的"快"并非空谈,其源码 picohttpparser.c 中可以看到几类典型的高性能 C 技巧:
1. SSE4.2 字符串指令加速
#ifdef __SSE4_2__ if (likely(buf_end - buf >= 16)) { __m128i ranges16 = _mm_loadu_si128((const __m128i *)ranges); size_t left = (buf_end - buf) & ~15; do { __m128i b16 = _mm_loadu_si128((const __m128i *)buf); int r = _mm_cmpestri(ranges16, ranges_size, b16, 16, _SIDD_LEAST_SIGNIFICANT | _SIDD_CMP_RANGES | _SIDD_UBYTE_OPS); ...findchar_fast在支持 SSE4.2 的平台上使用_mm_cmpestri一次比较 16 字节,通过_SIDD_CMP_RANGES范围比较模式快速定位空白、控制字符等目标;在不支持 SSE4.2 的平台上则回退到"每 8 字节手动展开"的逐字节循环(源码注释称这是"the hottest code"——最热路径)。同时,源码对 GCC 3+ 定义了likely/unlikely分支预测宏,配合__builtin_expect引导 CPU 分支预测。
2. 查找表驱动的 token 校验
static const char *token_char_map = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0..."头部名等 token 的合法性判断使用 256 字节的静态查找表(token_char_map),把"字符是否属于合法 token 字符集"的多次比较转化为一次查表索引,避免逐字符分支判断。
3. 头部解析的细节处理
- 头部名前的空白处理:解析头部名时会先跳过
:之前的空格(源码注释引用了 Mozilla 2006 年的安全公告,指出某些实现允许空格出现在冒号前,解析器对此做了兼容),但冒号后的空格与制表符会被跳过; - 续行识别:若一行以空格或制表符开头,则视为上一头部的续行,
name置NULL; - 尾随空白的剥离:头部值解析到行尾后,会从后向前剥离所有 SP 与 HTAB(见
parse_headers末尾的循环); - 头部数量上限:超出调用方传入的
max_headers容量时返回-1,杜绝越界写。
4. 针对慢速攻击的完整性检查
三个解析函数在last_len != 0时都会先调用is_complete:它从buf + last_len - 3处开始(即只看新追加数据附近的最后 3 字节),快速查找结束序列\r\n\r\n,命中即可判定请求/响应完整、立即返回。这既避免了每次重扫全缓冲区的开销,也让解析器可以尽早识别"头部迟迟未结束"的慢速连接(slowloris)攻击。
在 Mosquitto 仓库中的真实集成
picohttpparser 并非孤立的三方库,它正是Mosquitto 内置 WebSocket 支持(WITH_WEBSOCKETS_BUILTIN)的 HTTP 握手解析引擎:
编译集成方式
在 src/CMakeLists.txt 中,当选择内置 WebSocket(WITH_WEBSOCKETS_BUILTIN)时:
if(WITH_WEBSOCKETS) if(WITH_WEBSOCKETS_BUILTIN) add_definitions("-DWITH_WEBSOCKETS=WS_IS_BUILTIN") set(MOSQ_SRCS ${MOSQ_SRCS} ${mosquitto_SOURCE_DIR}/deps/picohttpparser/picohttpparser.c) else() find_package(libwebsockets) add_definitions("-DWITH_WEBSOCKETS=WS_IS_LWS") endif() endif()即picohttpparser.c 直接作为 broker 源码的一部分参与编译(第 196 行),并将其目录加入头文件搜索路径(第 211-212 行),随后通过#include "picohttpparser.h"使用。这解释了为什么该第三方库被收纳在仓库的deps/目录下。
Broker 端:解析 WebSocket 升级请求
srC/http_serv.c 的http__read是内置 WebSocket listener 的 HTTP 握手入口,它直接调用phr_parse_request:
read_length = phr_parse_request(mosq->http_request, strlen(mosq->http_request), &http_method, &http_method_len, &http_path, &http_path_len, &http_minor_version, http_headers, &http_header_count, 0); // FIXME - deal with partial read ! if(read_length == -2){ // Partial read return MOSQ_ERR_SUCCESS; }else if(read_length == -1){ // Error return MOSQ_ERR_UNKNOWN; }可以看到 Mosquitto 对返回值语义的利用与 README 示例完全一致:-2视为"数据未到齐,等待下次读取",-1视为解析错误。解析成功后,broker 遍历http_headers数组,逐一检查Upgrade: websocket、Connection: upgrade、Sec-WebSocket-Key、Sec-WebSocket-Version: 13、Sec-WebSocket-Protocol: mqtt、Origin(配合websockets_origin配置做来源校验)等握手头,最后回写HTTP/1.1 101 Switching Protocols响应并切换到 WebSocket 上下文。头部数组struct phr_header http_headers[100]与http_header_count的容量/结果双向用法,也正是 README 示例的翻版。此外,http__context_init按配置项websockets_headers_size分配请求缓冲区,保证解析输入有界。
客户端库端:解析升级响应
picohttpparser 不只用于 broker。lib/http_client.c 中,MQTT 客户端库发起 WebSocket 连接后,用phr_parse_response解析 broker 返回的101 Switching Protocols响应:
read_length = phr_parse_response(mosq->http_request, hlen, &http_minor_version, &http_status, &http_msg, &http_msg_len, http_headers, &http_header_count, 0); if(read_length == -2){ // Partial read return MOSQ_ERR_SUCCESS; }else if(read_length == -1){ // Error return MOSQ_ERR_UNKNOWN; }随后同样遍历头部验证Upgrade、Connection、Sec-WebSocket-Accept(与本地计算的 accept key 比对)、Sec-WebSocket-Version与Sec-WebSocket-Protocol: mqtt,全部通过后才继续发送 MQTT CONNECT 报文。这构成了一条完整的证据链:同一个解析器,同时支撑 broker 侧的握手请求解析与客户端侧的握手响应解析。
小结
picohttpparser 以"无状态、零分配、零拷贝"的极简设计,在几百行 C 代码内实现了足以支撑生产级 HTTP/1 解析与 chunked 解码的能力,并通过 SSE4.2 加速、查找表校验、likely/unlikely分支预测和针对 slowloris 的快速完整性检查等手段压榨性能。在 Mosquitto 仓库中,它作为内置 WebSocket 的握手解析引擎被直接编译进 broker(src/http_serv.c)与客户端库(lib/http_client.c),是理解 Mosquitto WebSocket 支持底层机制的关键一环。若需在其基础上继续深挖,可通读 deps/picohttpparser/picohttpparser.c 的完整状态机实现,以及 deps/picohttpparser/picohttpparser.h 的 API 契约注释。
- 物联网
- 消息队列
- 后端
【免费下载链接】mosquitto
Eclipse Mosquitto - An open source MQTT broker
相关推荐
Eclipse Mosquitto 的 WebSocket 握手解析底座:picohttpparser 依赖库原理与实战用法
Eclipse Mosquitto 的 WebSocket 握手解析底座:picohttpparser 依赖库原理与实战用法 本文围绕 deps/picohtt
物联网消息队列后端网络/通信PicoHTTPParser 源码解析:Mosquitto 内置的无状态高性能 HTTP/1 解析器
PicoHTTPParser 源码解析:Mosquitto 内置的无状态高性能 HTTP/1 解析器 导读 本文围绕 Mosquitto 仓库内置的第三方组件
后端消息队列消息路由Eclipse Mosquitto Docker 镜像实战:2.1-ubuntu 的配置、认证、持久化与 HTTP Dashboard 全解析
Eclipse Mosquitto Docker 镜像实战:2.1 ubuntu 的配置、认证、持久化与 HTTP Dashboard 全解析 Eclipse
后端消息队列消息路由
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考