OpenSSL QUIC 服务端与客户端示例全解析:demos/quic 实战指南
2026/9/11 18:23:49 网站建设 项目流程

OpenSSL QUIC 服务端与客户端示例全解析:demos/quic 实战指南

【免费下载链接】opensslGeneral purpose TLS and crypto library项目地址: https://gitcode.com/GitHub_Trending/ope/openssl

OpenSSL 从 3.2 起引入 QUIC 支持,而 demos/quic 目录则是官方为开发者准备的 QUIC API 实战示例集。本文以该目录为线索,完整讲解如何编译、运行一个基于 UDP 的 QUIC 服务器,如何用openssl s_client -quic与之联调,并深入剖析阻塞式单连接服务器(server/)与非阻塞式SSL_poll多连接服务器(poll-server/)的源码实现,最后顺带梳理关联的 HTTP/3 客户端示例(demos/http3)与 QUIC 客户端 DDD 示例(doc/designs/ddd)。读完后,你将掌握 OpenSSL QUIC 服务器端从建 CTX、绑 UDP 端口、接受连接到收发流的完整调用链,并能把示例改造成自己的 QUIC 服务。

demos/quic 目录结构概览

demos/quic 下包含两类可直接构建的示例程序:

子目录程序说明
serverserver一次只接受并处理一个连接的 QUIC 服务器,演示阻塞式QUIC 服务端 API 用法
poll-serverquic-server-ssl-poll-http基于SSL_poll(3ossl)非阻塞QUIC HTTP/1.0 服务器,可同时处理多个连接

两个示例共享同一套核心调用骨架:SSL_CTX_new(OSSL_QUIC_server_method())创建 QUIC 服务端上下文 → 加载证书与私钥 → 创建 UDP socket 并bindSSL_new_listener()创建监听器 →SSL_accept_connection()接受连接 → 在连接上读写流。区别仅在于 I/O 模式:server用阻塞循环,poll-serverSSL_poll()事件驱动。

一、编译并运行阻塞式单连接 QUIC 服务器

1.1 构建与运行

进入 demos/quic/server,直接make即可构建,make run则会用测试证书在 4444 端口启动服务器:

$ cd demos/quic/server $ make $ make run

make run实际执行的命令(见 demos/quic/server/Makefile):

LD_LIBRARY_PATH=../../.. ./server 4444 \ ../../../test/certs/servercert.pem \ ../../../test/certs/serverkey.pem

要点:

  • 示例默认动态链接libcryptolibssl,因此需要把构建产物所在目录加入库搜索路径(LD_LIBRARY_PATH=../../..),Makefile 顶部注释也明确提示了这一点;
  • 证书与私钥直接复用仓库自带的测试证书 test/certs/servercert.pem 与 test/certs/serverkey.pem;
  • 编译选项为CFLAGS += -I../../../include -g -Wall -Wsign-compare,即头文件指向仓库根目录的 include(OpenSSL 3.x 的 QUIC 相关声明位于 include/openssl/quic.h.in)。

1.2 命令行用法

./server <port-number> <certificate-file> <key-file>

对应main()中的参数解析(demos/quic/server/server.c):

  • <port-number>:UDP 监听端口,通过strtoul解析后要求满足0 < port <= UINT16_MAX,否则报invalid port退出;
  • <certificate-file>:服务器证书(PEM);
  • <key-file>:服务器私钥(PEM)。

1.3 用 s_client 验证服务器

服务器 README(demos/quic/server/README.md)给出的客户端测试命令:

openssl s_client -quic -alpn ossltest -connect 127.0.0.1:<port-number>

其中-quic让 s_client 以 QUIC 客户端模式运行,-alpn ossltest与服务器协商的应用层协议必须与服务端 ALPN 列表一致(详见下文select_alpn)。Makefile 中还提供了现成的s_client目标:

$ make s_client

它等价于执行LD_LIBRARY_PATH=../../.. ../../../apps/openssl s_client -quic -quiet -alpn ossltest -connect 127.0.0.1:4444。连接成功后,服务器会在标准错误输出=> Received connection,并向客户端写入hello\n后关闭连接。

二、源码剖析:一个完整的阻塞式 QUIC 服务器

demos/quic/server/server.c 约 240 行,结构清晰,可拆成四个阶段阅读。

2.1 create_ctx:创建 QUIC 服务端上下文

ctx = SSL_CTX_new(OSSL_QUIC_server_method());
  • OSSL_QUIC_server_method()返回 QUIC 服务端方法(与客户端的OSSL_QUIC_client_method()相对,后者声明于 include/openssl/quic.h.in);
  • 之后依次完成三件事:
    1. SSL_CTX_use_certificate_chain_file()加载证书链文件(PEM 中第一张必须是叶子证书,其后可跟中间 CA);
    2. SSL_CTX_use_PrivateKey_file(ctx, key_path, SSL_FILETYPE_PEM)加载私钥,随后用SSL_CTX_check_private_key()校验私钥与叶子证书匹配;
    3. SSL_CTX_set_alpn_select_cb(ctx, select_alpn, NULL)注册 ALPN 选择回调。

select_alpn回调使用SSL_select_next_proto()在客户端提供的协议列表与服务端期望的alpn_ossltest之间做协商:

static const unsigned char alpn_ossltest[] = { 0x08, 0x6f, 0x73, 0x73, 0x6c, 0x74, 0x65, 0x73, 0x74 /* "\x08ossltest" */ };

首字节0x08是协议名长度(8 字节),随后是ossltest的 ASCII 码——这是 QUIC(与 TLS)ALPN 的标准 TLV 编码。协商失败返回SSL_TLSEXT_ERR_ALERT_FATAL直接终止握手,成功返回SSL_TLSEXT_ERR_OK注意:客户端s_client若不携带-alpn ossltest,握手将失败

2.2 create_socket:创建 UDP 套接字

QUIC 底层是 UDP,因此与 TCP 服务器不同,这里创建的是数据报套接字:

fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); sa.sin_family = AF_INET; sa.sin_port = htons(port); bind(fd, (const struct sockaddr *)&sa, sizeof(sa));

socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)创建 IPv4 UDP 套接字;htons(port)将端口号转成网络字节序后bind。出错时用BIO_closesocket(fd)关闭(该 API 同时兼容 Windows 的closesocket与 POSIX 的close)。

2.3 run_quic_server:监听与接受连接

这是 QUIC 服务端区别于传统 TLS 服务的核心部分:

/* 1. 创建 QUIC 监听器 */ listener = SSL_new_listener(ctx, 0); /* 2. 把 UDP 套接字交给监听器 */ SSL_set_fd(listener, fd); /* 3. 开始监听 */ SSL_listen(listener); /* 4. 显式设置阻塞模式(其实监听器默认就是阻塞的) */ SSL_set_blocking_mode(listener, 1); for (;;) { /* 5. 阻塞等待新连接,语义类似 accept(2) */ conn = SSL_accept_connection(listener, 0); ... }

代码注释特别强调了两个关键行为:

  • 阻塞模式可继承:QUIC 对象(监听器、连接、流)默认工作于阻塞模式,且该模式会被子对象继承。代码里SSL_set_blocking_mode(listener, 1)是冗余的(注释原话 "The configured behaviour is inherited by child objects"),真正的用意是演示该 API 存在;
  • SSL_listen()可选:注释指出SSL_accept_connection()会隐式启动监听,SSL_listen()仅当服务器想"先确认开始收包但暂不 accept"时才需要调用。

在循环中还可以对每个 accept 出来的连接单独调用SSL_set_blocking_mode()关闭阻塞,示例代码以注释形式预留了这一可选项。

2.4 run_quic_conn:处理单条连接

if (!SSL_write_ex2(conn, "hello\n", 6, SSL_WRITE_FLAG_CONCLUDE, &written) || written != 6) { ... } if (SSL_shutdown(conn) != 1) { ... }

这段代码演示了 QUIC 流 API 的两个新特性:

  • SSL_write_ex2()是带扩展标志的写接口,第二个标志参数SSL_WRITE_FLAG_CONCLUDE(定义于 include/openssl/ssl.h.in)表示写完这批数据后立即结束流(发送 FIN)。注释原话是 "This demonstrates the use of SSL_write_ex2 for optimised FIN generation"——即把"写数据 + 结束流"合并为一次系统调用级别的优化,避免单独的SSL_stream_conclude()往返;
  • 由于连接继承监听器的阻塞模式,这里的SSL_write_ex2SSL_shutdown都是阻塞调用,SSL_shutdown返回 1 表示关闭完成。

注意:这里写的是默认流(default stream,QUIC 中由握手直接建立的双向流),没有显式调用SSL_new_stream()创建新流,因此是最简路径。

2.5 错误处理与资源释放

  • 所有SSL_*调用失败后统一调用ERR_print_errors_fp(stderr)输出错误队列;
  • main()结尾依次SSL_CTX_free(ctx)BIO_closesocket(fd)释放资源;
  • run_quic_serverconn每次循环处理完即SSL_free(conn),保证单连接模型下无泄漏。

三、非阻塞进阶:基于 SSL_poll 的多连接 HTTP/1.0 服务器

demos/quic/poll-server/quic-server-ssl-poll-http.c(约 1960 行)是同一主题的非阻塞进阶版:所有 I/O 均为非阻塞,通过SSL_poll()单线程同时服务多个连接,并实现了极简 HTTP/1.0 协议。构建运行方式与 server 一致:

$ cd demos/quic/poll-server $ make $ make run # 等价于 ./quic-server-ssl-poll-http 4444 ../../../test/certs/servercert.pem ../../../test/certs/serverkey.pem

也可以手动指定参数:./quic-server-ssl-poll-http <port> <cert> <key>

3.1 理解 SSL_poll 事件模型

SSL_poll()是 OpenSSL 3.2 引入的轮询接口(见 doc/man3/SSL_poll.pod),允许应用把监听器、连接、流统一注册进一个SSL_POLL_ITEM数组统一轮询,事件位定义在 include/openssl/ssl.h.in:

事件宏含义
SSL_POLL_EVENT_F通用失败
SSL_POLL_EVENT_EL监听器异常
SSL_POLL_EVENT_EC/SSL_POLL_EVENT_ECD连接异常 / 连接关闭已排空
SSL_POLL_EVENT_ER/SSL_POLL_EVENT_EW读异常 / 写异常
SSL_POLL_EVENT_R/SSL_POLL_EVENT_W可读 / 可写
SSL_POLL_EVENT_IC有入站连接
SSL_POLL_EVENT_ISB/SSL_POLL_EVENT_ISU有入站双向流 / 单向流
SSL_POLL_EVENT_OSB/SSL_POLL_EVENT_OSU可创建出站双向流 / 单向流

demo 用这些位组合出三个上层宏:

#define SSL_POLL_ERROR (SSL_POLL_EVENT_F | SSL_POLL_EVENT_EL | SSL_POLL_EVENT_EC \ | SSL_POLL_EVENT_ECD | SSL_POLL_EVENT_ER | SSL_POLL_EVENT_EW) #define SSL_POLL_IN (SSL_POLL_EVENT_R | SSL_POLL_EVENT_IC | SSL_POLL_EVENT_ISB | SSL_POLL_EVENT_ISU) #define SSL_POLL_OUT (SSL_POLL_EVENT_W | SSL_POLL_EVENT_OSB | SSL_POLL_EVENT_OSU)

3.2 事件驱动的连接管理

demo 自建了一个极简poll_manager:内部维护poll_event链表,当链表变化时把每个事件拷贝进SSL_poll()所需的SSL_POLL_ITEM数组(rebuild_poll_set(),按POLL_GROW/POLL_DOWNSIZ增量伸缩)。主循环:

rebuild_poll_set(pm); ok = SSL_poll((SSL_POLL_ITEM *)pm->pm_poll_set, pm->pm_event_count, sizeof(struct poll_event), NULL, 0, &poll_items);

每个poll_eventpe_type分为PE_LISTENERPE_CONNECTIONPE_STREAM(双向)、PE_STREAM_UNI_IN/OUT(单向收发),并挂接pe_cb_in/pe_cb_out/pe_cb_error三个回调,分别由SSL_POLL_INSSL_POLL_OUTSSL_POLL_ERROR触发。事件分发逻辑为:

  • 监听器收到SSL_POLL_EVENT_ICapp_accept_qconn()SSL_accept_connection()接受连接并注册连接事件(监听ISB/ISU/EC/ECD);
  • 连接收到SSL_POLL_EVENT_ISB/ISUapp_accept_stream_cb()SSL_accept_stream(qconn, SSL_STREAM_FLAG_UNI)接受入站流(注:demo 中双向流也以SSL_STREAM_FLAG_UNI接受,SSL_STREAM_FLAG_UNI定义于 include/openssl/ssl.h.in);
  • 连接收到SSL_POLL_EVENT_OSB/OSUapp_new_stream_cb()SSL_new_stream(qconn, ...)创建出站流用于回写响应;
  • 流上可读/可写 →app_read_cb()/app_write_cb()分别用SSL_read_ex()/SSL_write_ex()读写,数据写尽后调SSL_stream_conclude()结束流(对应 doc/man3/SSL_new_stream.pod 与 doc/man3/SSL_accept_stream.pod 所述语义)。

3.3 流状态的显式管理

demo 通过SSL_get_stream_read_state()/SSL_get_stream_write_state()(见 doc/man3/SSL_get_stream_read_state.pod)显式检查流状态,状态常量定义于 include/openssl/ssl.h.in:SSL_STREAM_STATE_NONE/OK/WRONG_DIR/FINISHED/RESET_LOCAL/RESET_REMOTE/CONN_CLOSED。收到FINISHED(对端结束流)时禁用读事件继续等待,其余错误状态则销毁对应poll_eventSSL对象。示例中 ALPN 协商的协议为http/1.0hq-interop两个候选(与server示例的ossltest不同),客户端需相应携带-alpn http/1.0

四、关联示例:HTTP/3 客户端与 QUIC 客户端 DDD 系列

demos/quic 的 README(demos/quic/README.md)还链向两组关联示例。

4.1 HTTP/3 客户端演示(demos/http3)

demos/http3 演示如何用 OpenSSL QUIC 加第三方 HTTP/3 库 nghttp3 发起 HTTP/3 请求,代码分为两层:

  • 适配层ossl-nghttp3.c:把 nghttp3 绑定到 OpenSSL 的 QUIC 实现;
  • 应用层ossl-nghttp3-demo.c:基于适配层发起单个 HTTP/3 请求。

构建需先安装 nghttp3 开发包(Ubuntu 上为libnghttp3-dev),随后:

$ make $ LD_LIBRARY_PATH=../.. ./ossl-nghttp3-demo www.example.com:443

输出为文本形式的 HTTP 响应头加响应体。必要时用SSL_CERT_FILESSL_CERT_DIR环境变量指定信任根 CA 位置。

4.2 QUIC 客户端 DDD 演示(doc/designs/ddd)

doc/designs/ddd 是 OpenSSL 官方以"演示驱动设计"(Demo-Driven Design)方法论维护的 API 用法示例集,用于在 API 演进时评估对真实应用的影响。其中与 QUIC 客户端直接相关的是ddd-01~ddd-06六个示例(完整清单见 doc/designs/ddd/README.md):

示例模式说明
ddd-01-conn-blocking.cS-BIOc基于BIO_s_connect的阻塞客户端
ddd-02-conn-nonblocking.cA-BIOc非阻塞 + 缓冲 BIO
ddd-03-fd-blocking.cS-AOSFSSL_set_fd阻塞客户端(对应 mutt、nginx 等真实应用模式)
ddd-04-fd-nonblocking.cA-AOSFSSL_set_fd非阻塞客户端
ddd-05-mem-nonblocking.cA-BIOm内存 BIO 非阻塞,把 libssl 当纯状态机
ddd-06-mem-uv.cA-BIOm内存 BIO + libuv 异步事件库

Ubuntu 上运行ddd-06需安装libuv1-dev;若系统默认证书库不可用,可设置SSL_CERT_DIR

五、QUIC 服务端 API 调用链速查

综合两个示例,可归纳出 OpenSSL QUIC 服务端最小调用链(对应文档参见 doc/man3/SSL_new_listener.pod、doc/man3/SSL_set_blocking_mode.pod、doc/man3/SSL_write.pod):

SSL_CTX_new(OSSL_QUIC_server_method()) ├─ SSL_CTX_use_certificate_chain_file() 加载证书链 ├─ SSL_CTX_use_PrivateKey_file() + check 加载并校验私钥 ├─ SSL_CTX_set_alpn_select_cb() 注册 ALPN 回调(QUIC 握手必需) ├─ SSL_CTX_set_verify(SSL_VERIFY_NONE, ...) 服务端示例不要求客户端证书 socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) + bind 创建 UDP 套接字 SSL_new_listener(ctx, 0) SSL_set_fd(listener, fd) 绑定 UDP fd SSL_set_blocking_mode(listener, 0|1) 阻塞/非阻塞(可被子对象继承) SSL_listen(listener) 显式开始监听(可选) 循环: SSL_accept_connection(listener, 0) 接受连接(阻塞版类似 accept(2)) SSL_write_ex2(conn, data, len, SSL_WRITE_FLAG_CONCLUDE, &n) 写流并结束 SSL_shutdown(conn) 关闭连接 SSL_free(conn); SSL_free(listener); SSL_CTX_free(ctx)

非阻塞场景则将"接受连接/新建流"替换为SSL_poll()+SSL_accept_stream()/SSL_new_stream(),并以SSL_get_stream_read_state()等接口跟踪流生命周期。上述示例同时适用于 Windows 与 Linux/Unix(源码中以_WIN32分支切换winsock2.hnetinet/in.h等头文件)。

六、延伸阅读

  • 阻塞式服务端完整源码:demos/quic/server/server.c
  • 非阻塞 SSL_poll 服务端完整源码:demos/quic/poll-server/quic-server-ssl-poll-http.c
  • 两处 Makefile 的构建/运行/s_client目标:demos/quic/server/Makefile、demos/quic/poll-server/Makefile
  • 官方 API 手册:SSL_poll(doc/man3/SSL_poll.pod)、SSL_new_listener(doc/man3/SSL_new_listener.pod)、SSL_set_blocking_mode(doc/man3/SSL_set_blocking_mode.pod)、SSL_write(doc/man3/SSL_write.pod)、OSSL_QUIC_client_method(doc/man3/OSSL_QUIC_client_method.pod)
  • 常量与标志定义:include/openssl/ssl.h.in(SSL_WRITE_FLAG_CONCLUDE位于 L2028,SSL_POLL_EVENT_*位于 L2513-L2534)、include/openssl/quic.h.in

以 demos/quic/server 为起点跑通第一个阻塞式 QUIC 服务器,再对照 demos/quic/poll-server 理解SSL_poll()事件驱动模型,即可覆盖 OpenSSL QUIC 服务端从入门到生产级改造的完整路径。

【免费下载链接】opensslGeneral purpose TLS and crypto library项目地址: https://gitcode.com/GitHub_Trending/ope/openssl

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询