【免费下载链接】libwebsockets
canonical libwebsockets.org networking library
项目地址:
https://gitcode.com/gh_mirrors/li/libwebsockets
点击查看 免费下载
libwebsockets(lws)在 LWS_WITH_HTTP3 开启时自动启用 LWS_WITH_QUIC,其 QUIC 传输栈为原生实现,无需依赖外部 QUIC 协议库。本文以仓库中的 minimal-quic-client-server 示例为核心,完整讲解如何在一个进程中同时实例化 QUIC 服务端与客户端、通过 UDP 完成 128KB 双向批量传输,并深入剖析该示例内置的三项高级测试设施——重排序中继(reordering relay)、杂散包注入(stray packets)与强制 Retry——以及可插拔的拥塞控制器(NewReno / CUBIC)架构。读完本文,你将掌握 lws QUIC 的构建方式、命令行用法、数据有序性校验设计,以及从源码层面理解 QUIC 面对乱序、丢包和握手抗重放攻击时的处理原理。
示例概览:单进程内建 QUIC 服务端与客户端
minimal-quic-client-server 是一个自包含的测试型示例。默认情况下,它在进程内同时完成三件事:
测试通过的判定条件是:每一方都完整、按序地收到对端发来的全部数据。为了验证“按序、无丢失、无重复”,示例设计了一个巧妙的字节模式:每个发送字节的值是其在流中的偏移量(stream offset)的函数,接收方据此逐字节校验(pattern_byte()、pattern_fill()、pattern_ok(),见 main.c)。发送时把 at + i 处的偏移信息写入数据,接收时对比实际字节与预期字节,任何乱序、丢失或重复都会立刻在偏移位置附近暴露出来。
相关代码位置:
- 示例入口与全部逻辑:main.c
- 构建配置与 ctest 用例:CMakeLists.txt
构建与运行前提
示例的 CMakeLists.txt 明确列出了三项编译期硬性要求,通过 require_lws_config() 检查:
| LWS_ROLE_QUIC | 1 | 启用 QUIC 角色 |
| LWS_WITH_CLIENT | 1 | 启用客户端能力 |
| LWS_WITH_SERVER | 1 | 启用服务端能力 |
在满足上述配置的 libwebsockets 构建环境中,编译示例:
$ cmake . && make
需要说明的是,lws 中 QUIC 与 HTTP/3 是绑定关系:LWS_WITH_HTTP3 默认开启,且一旦开启就会自动启用 LWS_WITH_QUIC(详见 READMEs/README.quic.md)。另外,QUIC 依赖 TLS 1.3 及配套的 TLS 后端能力,示例直接使用内置在源码中的自签名测试证书(test_cert / test_key)来满足 TLS 1.3 对合法证书的要求,客户端连接本地测试时通过 LCCSCF_ALLOW_SELFSIGNED | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK 放行自签名证书。
最简单的运行方式:
$ ./lws-minimal-quic-client-server
程序会打印双方收发进度,最终输出 Test completed successfully! Both sides received full data… 并等待 1.5 秒 ACK 后正常退出(返回码 0)。
命令行参数完整解析
示例通过 lws 内置的命令行开关机制(struct lws_switches)解析参数,参数表如下:
| -p <port> | 服务端监听的端口,同时也是客户端连接的端口(默认 7681) |
| –server <address> | 客户端连接的服务端地址(默认 127.0.0.1) |
| -s | 仅服务端模式:不启动客户端,且服务端不主动发送数据 |
| -u <url> | 客户端连接该 URL 指向的远端服务端,而不是运行本地服务端 |
| –relay <port> | 客户端经本进程内运行在该端口上的 UDP 中继连接服务端(详见下文) |
| –stray | 配合 –relay 使用,中继还会向客户端发送另一条连接的杂散数据包 |
| –write-size <n> | 每次 lws_write() 发送的字节数,取值范围 1~1024(默认 1024) |
| –cubic | 使用 CUBIC 拥塞控制器,替代默认的 NewReno |
源码中对参数做了严格校验(见 main.c):
- -p 端口必须是合法 16 位范围(0~65535),否则报错退出;
- –write-size 超出 1~1024 直接报错 –write-size must be 1 to 1024;
- –relay 端口同样校验 16 位范围;
- –stray 必须与 –relay 同时使用,否则报 –stray needs –relay;
- -u 传入的 URL 通过 lws_parse_uri() 解析出协议、地址、端口与路径。
–write-size 对测试意义重大:值越小,每个 UDP 数据包中承载的 STREAM 帧越多越小,从而制造出大量小 STREAM 帧需要重组的情形。默认 1024 字节对应每个数据包一个较大的 STREAM 帧。
数据有序性校验:每个字节都是偏移量的函数
QUIC 面向的是不可靠的 UDP,因此验证乱序重组能力是本示例的核心目的。发送端在 LWS_CALLBACK_CLIENT_WRITEABLE 与 LWS_CALLBACK_SERVER_WRITEABLE 回调中,按 write_size 分片填充数据并调用 lws_write(wsi, &buf[LWS_PRE], to_send, LWS_WRITE_BINARY)(见 main.c)。注意缓冲区前预留了 LWS_PRE 字节,这是 lws 协议栈写入的固定要求。
接收端分别在 LWS_CALLBACK_QT_CLIENT_RECEIVE(QUIC 传输层客户端侧收到负载)与 LWS_CALLBACK_QT_SERVER_RECEIVE(服务端侧)回调中校验(这两个回调枚举值见 lws-callbacks.h):
- 用 pattern_ok(client_rx, in, len) 校验本次到达的数据是否与流偏移 client_rx 处的预期字节一致,不一致立即判定失败并退出事件循环;
- 累计接收字节数并计算 djb2 风格哈希(simple_hash,初始值 5381)做整体校验;
- 当任一侧收满 TOTAL_DATA 后置 client_done / server_done,双方都完成时输出成功日志并调度 1.5 秒后的退出(check_test_completion(),见 main.c)。
主循环还内置了两道超时防线(见 main.c):绝对超时 60 秒,以及 15 秒无任何接收活动的空闲超时,两者任一触发即判失败退出。
重排序中继(–relay):让对端必须在乱序中重组
–relay <port> 让客户端不再直连服务端,而是先连接本进程内的一个 UDP 中继。中继在每个方向把数据报按批缓存,每批最多 4 个,发送时**按逆序(last first)**送出,从而人为制造乱序:
- 握手阶段双方交换的 CRYPTO 数据会乱序到达;
- 流数据(STREAM 帧)同样乱序到达;
- 对端必须把先到的数据暂存,等待其前面的空洞被填满后才能交付给上层。
中继的关键实现常量与逻辑(见 main.c):
#define RELAY_BATCH4 /* 每方向最多缓存 4 个数据报 */
#define RELAY_HOLD_US(2 * LWS_US_PER_MS) /* 首批到达后最多等待 2ms */
#define RELAY_DGRAM_MAX2048 /* 单个数据报最大长度 */
行为规则如下:
使用中继时,–server 必须是数字形式的 IP 地址(如 127.0.0.1),因为中继需要把地址解析为 sockaddr 才能转发(源码用 lws_sa46_parse_numeric_address(),解析失败报 –relay needs a numeric –server address)。典型用法:
$ ./lws-minimal-quic-client-server –server 127.0.0.1 -p 7681 –relay 7682 –write-size 100
较小的 –write-size 会让每个数据包携带许多小 STREAM 帧,进一步加大乱序重组的难度,因此 ctest 的 reorder 用例固定使用 –write-size 100。
中继本身作为一个独立 vhost 实现,协议名为 quic-relay,通过 lws_create_adopt_udp(rvh, ads, relay_port, LWS_CAUDP_BIND, "quic-relay", …) 绑定 UDP 端口,并依赖 lws_sorted_usec_list(sul)实现 2ms 定时器调度,事件驱动模型与 lws 主循环完全一致。
杂散包注入(–stray):握手期就要抗干扰
–stray 与 –relay 配合,让中继在转发给客户端的第一批以及此后每第八批(RELAY_STRAY_EVERY 8)数据之前,额外塞入一个属于另一条 QUIC 连接的长头包(long header packet)。该包的构造逻辑(relay_send_stray(),见 main.c)如下:
- 用 lws_get_random() 生成 57 字节随机内容,作为随机的连接 ID、包号与负载;
- 首字节按 0xf1(QUIC v2)与 0xe1(QUIC v1)交替设置,构成 Handshake 类型长头;
- 写入随机 8 字节 DCID 与 8 字节 SCID、2 字节 varint 长度字段等头部结构;
- 负载是没有任何一方持有密钥的密文。
从语义上讲,这类“别人的连接的数据包混进来”是网络中的常态事件。本测试的关键点在于时序:
- 第一个杂散包先于服务端的第一个 Initial 到达,落在握手进行中;
- 其余杂散包在握手完成后持续到达;
- 客户端必须全部丢弃这些包,且它们绝不能改变自身连接的任何状态。
这正是 RFC 9000 7.2 的要求——服务端的连接 ID 只能取自其第一个通过认证的 Initial 包;同时也是 RFC 9368 的要求——协议版本只能由通过认证的包决定。测试最终必须仍然完成传输。运行方式:
$ ./lws-minimal-quic-client-server –server 127.0.0.1 -p 7681 –relay 7682 –stray
拥塞控制器:默认 NewReno,可选 CUBIC
lws 的 QUIC 拥塞控制采用可插拔架构:struct lws_cc_ops 定义了一组操作钩子(见 lws-quic.h):
struct lws_cc_ops {
void (*init)(struct lws *nwsi);
void (*on_sent)(struct lws *nwsi, size_t bytes);
void (*on_ack)(struct lws *nwsi, size_t bytes_acked, lws_usec_t rtt);
void (*on_loss)(struct lws *nwsi, size_t bytes_lost);
void (*on_discard)(struct lws *nwsi, size_t bytes_discarded);
void (*on_persistent_congestion)(struct lws *nwsi);
int (*can_send)(struct lws *nwsi, size_t bytes);
lws_usec_t (*get_pacing_delay)(struct lws *nwsi, size_t bytes_to_send);
};
lws 导出两个内置实现:lws_cc_ops_newreno(默认)与 lws_cc_ops_cubic。默认使用 NewReno;如需切换,在 struct lws_context_creation_info 中设置 .quic_cc_ops。示例中 –cubic 开关的实现非常简洁(见 main.c):
if (lws_cmdline_option(argc, argv, switches[LWS_SW_CUBIC].sw))
/* both the server and the client in this context use it */
info.quic_cc_ops= &lws_cc_ops_cubic;
同一上下文内的服务端与客户端都会使用所选控制器,因此在 –relay 造成乱序与丢包的前提下,双方都会用 CUBIC 进行在途字节(bytes in flight)记账与丢包恢复:
$ ./lws-minimal-quic-client-server –server 127.0.0.1 -p 7681 –relay 7682 –write-size 100 –cubic
从两个控制器的源码实现(ops-quic-cc-newreno.c 与 ops-quic-cc-cubic.c)可以观察到它们遵循 RFC 9002 的共同骨架:
- 初始窗口:cwnd 默认取 10 个 MTU(MTU 默认 1280,可用 vhost 的 quic_mtu 覆盖),也可通过 context 的 quic_initial_cwnd 覆盖;ssthresh 初始为无穷大((size_t)-1);
- 慢启动:cwnd < ssthresh 时每个 ACK 线性增加 bytes_acked;
- 拥塞避免:NewReno 每 ACK 增加 mtu * bytes_acked / cwnd;CUBIC 则按 RFC 9438 用立方函数计算目标窗口,并对应用受限(app-limited)时段做了 epoch 平移处理,还实现了 TCP 友好性(Reno 近似)与 fast convergence;
- 丢包响应:NewReno 将 ssthresh 减半(cwnd / 2,下限 2 个 MTU);CUBIC 按 cwnd * 0.7 设置 ssthresh 并更新 w_max;
- 持续拥塞(persistent congestion):两者都把 cwnd 塌缩到 2 个 MTU 的最小值;
- PTO 重排队:on_discard 只修正 in-flight 计数,不动 cwnd 与 ssthresh(RFC 9002 6.2.4),避免每次 PTO 永久缩小可用窗口;
- 发送节流(pacing):两者都实现基于“信用”的 pacing——pacing_credit 按 cwnd / srtt 的速率随时间补充,并把单次突发上限封顶为 64 个包(64 * mtu),get_pacing_delay() 返回还需等待的微秒数。
如果你需要定制自己的拥塞控制(例如 BBR),只需实现 struct lws_cc_ops 并在 init() 中把私有状态挂到 nwsi->quic.qn->cc_state,再将实例地址赋给 info.quic_cc_ops 即可(完整接口说明见 lws-quic.h)。
强制 Retry:用环境变量验证 RFC 9000 握手抗重放
QUIC 的 Retry 机制(RFC 9000 8.1.2)用于服务端在握手早期验证客户端地址真实性:服务端对每个不携带 token 的 Initial 回复一个 Retry 包,客户端必须携带 Retry 中给出的 token 重发 Initial,服务端才继续握手。
本示例通过环境变量 LWS_QUIC_FORCE_RETRY 无条件触发该路径:
$ LWS_QUIC_FORCE_RETRY=1 ./lws-minimal-quic-client-server
对应的服务端实现位于 ops-quic.c(QUIC Initial 包处理路径):
同时,客户端侧会校验服务端 transport parameters 是否复用了 Retry 交换中的连接 ID(RFC 9000 7.3),防止降级攻击。ctest 的 retry 用例正是通过 ENVIRONMENT "LWS_QUIC_FORCE_RETRY=1" 注入该环境变量来覆盖这条路径(见 CMakeLists.txt)。
自动化测试矩阵:ctest 与 h3spec
示例的 CMakeLists.txt 把上述所有场景固化为 ctest 用例,可作为理解各参数组合用途的最权威参考:
| lws-minimal-quic-client-server | –server <addr> -p <port> | 基础双向 128KB 传输 |
| lws-minimal-quic-client-server-reorder | –relay <port> –write-size 100 | 乱序 CRYPTO 与众多小 STREAM 帧重组 |
| lws-minimal-quic-client-server-cubic | –relay <port> –write-size 100 –cubic | CUBIC 在乱序与丢包下的 in-flight 记账 |
| lws-minimal-quic-client-server-stray | –relay <port> –stray | 握手期及之后丢弃外部连接的杂散包(RFC 9000 7.2 / RFC 9368) |
| lws-minimal-quic-client-server-retry | 环境变量 LWS_QUIC_FORCE_RETRY=1 | Retry 握手与 transport parameters 校验(RFC 9000 8.1.2 / 7.3) |
此外,若系统 PATH 中存在 h3spec 工具且 lws 启用了 LWS_WITH_HTTP3,ctest 还会自动以 -s 服务端模式拉起本示例作为测试服务器,运行完整的 QUIC/HTTP/3 RFC 合规测试套件(h3spec 127.0.0.1 <port> -n,见 CMakeLists.txt)。关于 QUIC 的 TLS 后端选型(GnuTLS、BoringSSL、AWS-LC、LibreSSL、wolfSSL、schannel 等)与拥塞控制接口的更多说明,可参阅 READMEs/README.quic.md。
小结
minimal-quic-client-server 以不到 800 行的单文件(main.c)演示了 lws 原生 QUIC 栈的完整能力闭环:
- 传输正确性:用“字节=流偏移函数”的模式严格验证按序、无丢失、无重复交付;
- 乱序与丢包韧性:通过重排序中继人为制造乱序与丢弃,验证 QUIC 的重组与重传;
- 握手安全性:通过杂散包注入与强制 Retry,验证连接 ID、版本与地址验证的抗干扰/抗重放语义(RFC 9000 7.2、7.3、8.1.2,RFC 9368);
- 拥塞控制可扩展性:以 lws_cc_ops 接口同时提供 NewReno 与 CUBIC 两种实现,且支持用户自定义算法。
无论你是想用 lws 搭建 QUIC/HTTP3 服务,还是希望深入理解 QUIC 协议机制在真实实现中的落地方式,这个示例及其配套 ctest 用例都是极佳的起点与参照。
赞
【免费下载链接】libwebsockets
canonical libwebsockets.org networking library
项目地址:
https://gitcode.com/gh_mirrors/li/libwebsockets
点击查看 免费下载
相关推荐
Git LFS File Locking 提案解读:为锁创建与列表 API 引入 notes 备注字段
Hindsight三大插件类型详解:Input、Analysis与Output使用教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考





