尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

OpenSSL DTLS Listener 实战:用 SSL Listener API 构建多线程 DTLS 回显服务器

OpenSSL DTLS Listener 实战:用 SSL Listener API 构建多线程 DTLS 回显服务器 OpenSSL DTLS Listener 实战用 SSL Listener API 构建多线程 DTLS 回显服务器【免费下载链接】opensslGeneral purpose TLS and crypto library项目地址: https://gitcode.com/GitHub_Trending/ope/openssl本文以 OpenSSL 仓库中的 demos/dtlslistenerecho 示例 为主体完整讲解如何用 SSL Listener APISSL_new_listener/SSL_listen/SSL_accept_connection实现一个支持最多 10 个并发客户端的 DTLS 回显服务器涵盖双栈 UDP 套接字构建、非阻塞模式与SSL_poll()事件轮询、DTLS 1.2/1.3 双协议协商HVR/HRR、握手重传退避封顶、线程每连接thread-per-connection模型、空闲超时回收以及客户端证书主机名校验的完整实现细节读完后你可以复制运行该 Demo 并理解每个关键 API 的底层调用链。1. Demo 定位一个“多客户端 DTLS 回显”控制台应用demos/dtlslistenerecho是一个单文件实现的 DTLS 回显演示程序由命令行参数决定运行模式服务器或客户端不带参数启动会打印用法。目录结构非常精简文件作用main.c全部实现1067 行服务端 客户端cert.pem / key.pem自签名证书与私钥Common Name 为localhostREADME.md功能特性、限制与运行说明其核心行为是服务端基于SSL Listener建立 DTLS Server 对象处理多个客户端连接采用线程每连接模型——每个被接受的连接在自己的原生线程POSIX 下为pthreadWindows 下为CreateThread中被处理客户端发送应用数据服务端原样回显。1.1 特性与限制README 原文完整覆盖特性最多 10 个并发 DTLS 客户端连接MAX_CONNECTIONS使用原生 OS 线程的线程每连接架构每个连接线程内使用SSL_poll()做非阻塞 I/O同时支持 DTLS 1.2HelloVerifyRequestHVR与 DTLS 1.3HelloRetryRequestHRR客户端可选定 DTLS 协议版本dtls12或dtls13通过主动 shutdown 信号机制实现线程的干净终止通过killall命令实现服务端整体关闭限制并发连接数上限 10MAX_CONNECTIONS达到上限后新的连接尝试会被拒绝并向服务端控制台打印错误信息连接不会无限期保持若客户端连续CLIENT_IDLE_TIMEOUT_SEC90 秒没有发送任何数据服务端将放弃该连接并释放线程槽位卡住的握手同样有时间上限而非重试数分钟见main.c中的dtls_timer_cb()这些常量在 main.c 中集中定义static const int server_port 4433; #define MAX_CONNECTIONS 10 #define POLL_TIMEOUT_SEC 5 /* Abandon a connection whose client sends nothing for 90 seconds. */ #define CLIENT_IDLE_TIMEOUT_SEC 90 /* * Cap the DTLS handshake retransmit backoff. The library default doubles the * timeout up to 60s; ... Capping each backoff at 8s bounds the handshake to * about 87s (1 2 4 8 x 10) ... */ #define DTLS_MAX_RETRANSMIT_TIMEOUT_US (8 * 1000000u)服务端口固定为4433UDP轮询超时 5 秒空闲超时 90 秒重传退避单次封顶 8 秒。2. 运行与验证2.1 编译该 Demo 依赖先构建好的libssl/libcrypto。仓库顶层用标准 OpenSSL 构建流程./config make注意demos子目录各自有独立构建方式参考 demos/sslecho/Makefile 的-lssl -lcrypto链接方式与 build.info 的构建声明dtlslistenerecho目录内只有main.c和证书文件实际编译时需要自行将其与构建产物链接例如cd demos/dtlslistenerecho cc -I../../include -g -Wall -o dtlslistenerecho main.c -L../../ -lssl -lcrypto # 动态库环境下需要让 libcrypto/libssl 可见 LD_LIBRARY_PATH../.. ./dtlslistenerecho仓库只读以上仅为本地复现的编译说明。2.2 启动服务器cd demos/dtlslistenerecho ./dtlslistenerecho s2.3 连接多个客户端分别在独立终端可同时连接最多 10 个客户端./dtlslistenerecho c localhost ./dtlslistenerecho c localhost ./dtlslistenerecho c localhost每个客户端可以独立发送消息并收到回显。2.4 指定 DTLS 协议版本客户端第三个参数为可选的协议版本# 强制 DTLS 1.2 ./dtlslistenerecho c localhost dtls12 # 强制 DTLS 1.3 ./dtlslistenerecho c localhost dtls13 # 默认协商可用的最高版本 ./dtlslistenerecho c localhost版本解析逻辑在 main.c 的main()中dtls12映射为DTLS1_2_VERSIONdtls13映射为DTLS1_3_VERSION未知值直接报错退出。无参数时的用法打印由 usage() 完成Usage: dtlslistenerecho s --or-- dtlslistenerecho c hostname [dtls12|dtls13]2.5 特殊命令与服务端关闭客户端输入kill仅断开该客户端服务端继续运行对应线程退出客户端输入killall断开所有客户端并优雅关闭服务端killall的处理流程1) 服务端置位 shutdown 标志2) 所有连接线程收到终止信号3) 每个线程完成当前操作后干净退出4) 服务端关闭 listener 并退出。两个命令均兼容\n与\r\n换行见 handle_connection() 与 process_line()。3. 服务端SSL Listener 的构建源码级解析3.1 双栈 UDP 套接字与 BIO 装配create_dtls_listener()main.c分四步完成监听就绪地址解析BIO_lookup_ex(NULL, port_str, BIO_LOOKUP_SERVER, AF_INET6, SOCK_DGRAM, 0, res)请求 IPv6 通配地址。源码注释明确不向BIO_listen传BIO_SOCK_V6_ONLY因此得到的是双栈套接字可同时服务 IPv6 与 IPv4 客户端。套接字创建与绑定BIO_socket(family, SOCK_DGRAM, 0, 0)建 UDP socket再用BIO_listen(fd, addr, BIO_SOCK_REUSEADDR | BIO_SOCK_NONBLOCK)绑定BIO_SOCK_NONBLOCK使 listener 从一开始就是非阻塞的。装配 datagram BIOBIO_new_dgram(fd, BIO_NOCLOSE)把 socket 包成数据报 BIO。由于 rbio/wbio 是同一个 BIOSSL_set0_rbio()与SSL_set0_wbio()各取一个引用所以先执行BIO_up_ref(listener_bio)加一次引用计数再set0两次后把局部指针置空——这是引用计数所有权转移的典型写法。创建 listener 并监听SSL_new_listener(ssl_ctx, 0)创建 DTLS listenerflags 传 0默认执行 HVR/HRR 客户端校验随后SSL_set_blocking_mode(listener, 0)显式置非阻塞最后SSL_listen(listener)进入监听态。这里有两个值得强调的 API 语义均在 include/openssl/ssl.h.in 声明DTLS listener 默认是阻塞模式且它 accept 出来的连接会继承该模式。本 Demo 全链路用SSL_poll()驱动并给SSL_accept_connection()传SSL_ACCEPT_CONNECTION_NO_BLOCK标志定义见 ssl.h.in所以必须先把 listener 置非阻塞被接受的连接才随之非阻塞。SSL_listen()与SSL_accept_connection()构成“监听—非阻塞接受”的组合accept 不阻塞在握手上握手进度由后续SSL_accept()在连接线程内推进。3.2 监听主循环轮询、接受与线程槽位run_server()main.c的主循环结构创建CRYPTO_THREAD_lock_new()原子锁全局变量atomic_lock供跨线程原子读写 shutdown/finished 标志用SSL_CTX_use_certificate_chain_file(ctx, cert.pem)SSL_CTX_use_PrivateKey_file(ctx, key.pem, SSL_FILETYPE_PEM)装载证书与私钥构造监听轮询项listener_item.desc SSL_as_poll_descriptor(listener); listener_item.events SSL_POLL_EVENT_IC; /* Incoming Connection */SSL_POLL_EVENT_ICIncoming Connection 事件定义见 ssl.h.in表示“有新的可接受连接”。每轮先调用cleanup_finished_threads()回收已完成线程的槽位再以 5 秒超时SSL_poll()监听 listener收到 IC 事件后new_conn SSL_accept_connection(listener, SSL_ACCEPT_CONNECTION_NO_BLOCK); ... DTLS_set_timer_cb(new_conn, dtls_timer_cb); /* 封顶握手重传退避 */ slot find_free_thread_slot(conn_threads); if (slot 0) { /* 容量已满打印 Connection limit reached (10)SSL_free(new_conn) 丢弃 */ }即先在 listener 上提取出一个连接对象再分配空闲槽位槽位满则直接释放该连接对象并打印拒绝信息与 README 所述“达到容量上限的新连接尝试会被拒绝”完全对应。分配成功后run_thread()启动平台线程POSIX 走pthread_createWindows 走CreateThread见 main.c并打印Main: Spawned thread N for new connection。线程资源回收有两条路径保证槽位最多 10 个可反复使用常规回收cleanup_finished_threads()main.c在主循环每轮检查finished原子标志对已完成线程wait_for_thread()join后SSL_free(conn)并清零槽位关停回收shutdown_all_threads()main.c先向所有活动线程写shutdown_requested 1再逐个 join、释放 SSL 对象。3.3 连接线程握手推进与回显循环每个连接线程执行handle_connection()main.c分为握手与回显两阶段。握手阶段是一个经典的非阻塞推进循环while ((ret SSL_accept(conn_args-conn)) ! 1) { /* 检查 shutdown_requested */ err SSL_get_error(conn_args-conn, ret); if (err ! SSL_ERROR_WANT_READ err ! SSL_ERROR_WANT_WRITE) { /* 致命错误退出 */ } /* 等待 socket 就绪而非忙轮询等待时长对齐 DTLS 重传定时器 */ item.desc SSL_as_poll_descriptor(conn_args-conn); item.events (err SSL_ERROR_WANT_READ) ? SSL_POLL_EVENT_R : SSL_POLL_EVENT_W; if (!DTLSv1_get_timeout(conn_args-conn, timeout)) { timeout.tv_sec POLL_TIMEOUT_SEC; } SSL_poll(item, 1, sizeof(item), timeout, 0, result_count); if (result_count 0) continue; /* 定时器到期前无入站数据回到 SSL_accept() 触发重传 */ }这段代码体现了一个 DTLS 编程关键点握手期间的等待时长应当由DTLSv1_get_timeout()给出的重传定时器决定而不是固定常量。SSL_poll()在定时器到期前没有入站数据报时result_count 0循环回到SSL_accept()库会重传上一个 flight对端死亡时重传预算耗尽会以致命错误从SSL_accept()抛出从而终止循环。回显阶段同样以SSL_poll()为心跳事件SSL_POLL_EVENT_R每 5 秒醒来一次并检查shutdown_requested收到则 breakkillall 或关停路径空闲判定result_count 0或没有可读事件时若time(NULL) idle_deadline距上次收到数据超过 90 秒则放弃连接每收到一次有效数据就把idle_deadline顺延 90 秒正常数据SSL_read_ex()读入后打印Thread N: Received: ...再用SSL_write_ex()循环回显遇到WANT_READ/WANT_WRITE继续重试其它错误goto donekill仅本线程退出killall则原子写server_shutdown 1通知主循环。线程退出前统一SSL_shutdown()向对端发送 close_notify然后CRYPTO_atomic_store_int(conn_args-finished, 1, ...)标记完成等待主循环回收槽位。4. 握手重传退避封顶DTLS_set_timer_cb() 实战UDP 上 DTLS 握手靠重传保证可靠性。库默认退避把超时翻倍、最高到 60 秒在允许的重传轮数内一个死对端可以让握手卡近8 分钟远长于本 Demo 的 90 秒空闲策略。dtls_timer_cb()main.c通过DTLS_set_timer_cb()安装了自定义计时回调为每次 flight 计算下一个重传间隔static unsigned int dtls_timer_cb(SSL *s, unsigned int timer_us) { unsigned int next (timer_us 0) ? 1000000u : timer_us * 2; if (next DTLS_MAX_RETRANSMIT_TIMEOUT_US) next DTLS_MAX_RETRANSMIT_TIMEOUT_US; /* 单次封顶 8 秒 */ return next; }语义是首次从 1 秒开始、逐次翻倍、单次不超过 8 秒。源码注释给出了总时长推算1 2 4 8 x 10 ≈ 87 秒与CLIENT_IDLE_TIMEOUT_SEC保持在同一数量级避免“握手卡死比空闲超时更久”的策略矛盾。该回调在服务端每个新接受的连接main.c与客户端main.c两侧都做了安装。该 API 的完整说明可参考 doc/man3/DTLS_set_timer_cb.pod。5. 客户端UDP 连接、主机名校验与版本锁定5.1 UDP“连接”与 datagram BIOcreate_dtls_client()main.c展示了 UDP 客户端的标准装配顺序BIO_lookup(server_name, port_str, BIO_LOOKUP_CLIENT, AF_UNSPEC, SOCK_DGRAM, res)解析服务器地址IPv4/IPv6 均可遍历BIO_ADDRINFO列表对每个地址BIO_socket()BIO_connect()取第一个成功的对 UDP 而言BIO_connect只是记录默认对端connected UDPBIO_socket_nbio(fd, 1)将 socket 置为非阻塞BIO_new_dgram(fd, BIO_NOCLOSE)创建 datagram BIO——由于 socket 已 connectedBIO 能自动经getpeername检测到对端无需再调BIO_dgram_set_peer()SSL_new(ssl_ctx)创建客户端BIO_up_ref() 两次SSL_set0_*bio()完成同一 BIO 的双挂载SSL_set1_dnsname(client, server_name)设定主机名用于证书校验。5.2 证书验证为什么命令行参数必须与证书匹配run_client()main.c做了三件事/* 1) 可选锁定协议版本 */ if (dtls_version ! 0) { SSL_CTX_set_min_proto_version(ssl_ctx, dtls_version); SSL_CTX_set_max_proto_version(ssl_ctx, dtls_version); } /* 2) 强制校验对端证书 */ SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, NULL); /* 3) 信任服务器证书本身自签名场景的简化做法 */ SSL_CTX_load_verify_locations(ssl_ctx, cert.pem, NULL);锁定版本的实现就是min 与 max 同时设为目标值SSL_CTX_set_min_proto_version()/SSL_CTX_set_max_proto_version()这也是 README“Code demonstrates”清单中列出的 API注释明确说明直接信任服务端证书是“非常规用法”生产环境通常应加载受信任根证书目录客户端按SSL_set1_dnsname()设置的命令行主机名严格做 DNS 名称匹配。因此使用自带cert.pemCNlocalhost时必须传localhost即使解析指向同一台机器127.0.0.1这类 IP 字面量也不会通过校验——IP 字面量与 DNS 名称是两种不同的匹配规则。README 的收尾建议实际部署时请用真实主机名生成证书。5.3 握手推进与双路轮询主循环do_client_handshake()main.c与服务端握手循环同构SSL_connect()返回 WANT_READ/WANT_WRITE 时用SSL_as_poll_descriptor()SSL_poll()等待等待时长同样由DTLSv1_get_timeout()决定result_count 0时回到SSL_connect()触发重传。进入应用数据阶段后客户端把stdin 与 SSL socket 放在同一个poll()里main.cpfds[0]监听 stdinpfds[1]通过BIO_get_rpoll_descriptor()取出 SSL 底层文件描述符。有服务端数据时先SSL_read_ex()打印Server: ...遇到SSL_ERROR_ZERO_RETURN判定服务端关闭并退出有键盘输入时按行切分、经process_line()发送。这一“先服务端数据、后用户输入”的顺序保证了回显的及时处理。Windows 分支则退化为select()_kbhit()的短超时轮询并配合setvbuf(stdin, NULL, _IONBF, 0)让控制台输入逐行可读。6. 关键调用链与 API 索引把全 Demo 的调用关系串起来可以得到一张清晰的分层视图main() ─ s → run_server() ├─ create_context(DTLS_server_method) ── cert.pem/key.pem ├─ create_dtls_listener(): BIO_lookup_ex → BIO_socket → BIO_listen │ → BIO_new_dgram → SSL_new_listener → SSL_set0_*bio │ → SSL_set_blocking_mode(0) → SSL_listen └─ 主循环: SSL_poll(SSL_POLL_EVENT_IC) → SSL_accept_connection(SSL_ACCEPT_CONNECTION_NO_BLOCK) → DTLS_set_timer_cb(dtls_timer_cb) → run_thread → handle_connection() ├─ 握手: SSL_accept ⇄ SSL_poll/DTLSv1_get_timeout └─ 回显: SSL_poll(SSL_POLL_EVENT_R) → SSL_read_ex/SSL_write_ex kill / killall / 90s 空闲 → SSL_shutdown main() ─ c → run_client(hostname[, dtls12|dtls13]) ├─ min/max proto version 锁定可选 ├─ SSL_CTX_set_verify(PEER) SSL_CTX_load_verify_locations(cert.pem) ├─ create_dtls_client(): BIO_lookup → BIO_socket/BIO_connect │ → BIO_socket_nbio(1) → BIO_new_dgram → SSL_set1_dnsname └─ do_client_handshake(): SSL_connect ⇄ SSL_poll/DTLSv1_get_timeout → poll(stdin, ssl_fd) 双路循环 → SSL_read_ex / SSL_write_ex涉及的 OpenSSL API 与文档/源码对应关系均在当前仓库中可查API / 常量在本 Demo 中的角色仓库内位置SSL_new_listener()/SSL_listen()/SSL_accept_connection()listener 创建、监听、非阻塞提取连接include/openssl/ssl.h.in、doc/man3/SSL_new_listener.podSSL_POLL_EVENT_IC/SSL_POLL_EVENT_R、SSL_POLL_ITEM监听“新连接”与连接“可读”事件include/openssl/ssl.h.in、doc/man3/SSL_poll.podSSL_poll()/SSL_as_poll_descriptor()所有非阻塞等待的统一入口doc/man3/SSL_poll.podSSL_set_blocking_mode()listener 及继承连接置非阻塞include/openssl/ssl.h.inDTLS_set_timer_cb()/DTLSv1_get_timeout()封顶重传退避决定 poll 等待时长doc/man3/DTLS_set_timer_cb.podSSL_CTX_set_min/max_proto_version()客户端锁定 DTLS 1.2/1.3include/openssl/ssl.h.inSSL_set1_dnsname()客户端按命令行主机名校验证书main.c 调用点7. 小结demos/dtlslistenerecho的价值在于它把 SSL Listener 这一套面向“一个监听对象服务多客户端”的 API 用最小代码完整走了一遍listener 的非阻塞语义与继承关系、SSL_ACCEPT_CONNECTION_NO_BLOCK的提取方式、SSL_poll()DTLSv1_get_timeout()对齐重传定时器的等待策略、DTLS_set_timer_cb()对握手退避的封顶、基于原子标志的线程优雅关停以及客户端SSL_set1_dnsname()严格 DNS 匹配的证书校验陷阱localhost与127.0.0.1的差异。其 10 连接槽位、90 秒空闲超时等约束则是刻意为演示清晰而设——在真实工程中可以把它作为骨架把固定槽位数组替换为连接池/事件驱动模型并按生产证书体系调整信任锚即可演进为多客户端 DTLS 服务的基础实现。【免费下载链接】opensslGeneral purpose TLS and crypto library项目地址: https://gitcode.com/GitHub_Trending/ope/openssl创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表