简介Indy 10.2.3 是面向 Delphi 开发者尤其 Delphi 2007–2010 用户的专业级网络通信组件库解决 TCP/IP、HTTP、FTP、SMTP/POP3 等协议开发中重复造轮子、SSL 集成复杂、异步稳定性不足等核心痛点适用于桌面客户端、邮件工具、文件传输服务等中高级网络应用开发场景。资源包共 2000 个文件以 494 个 Pascal 源码.pas、510 个说明与日志文本.txt含 Changelog.txt 和 KB 知识库、559 个图标资源.bmp/.gif/.jpg及 74 个 Delphi 包工程.dpk为主辅以测试用例NUnit/Test 目录、IDE 集成资源icons/icon16、构建脚本Builder/Full*.bat和完整编译产物Lib/ 目录结构完备开箱即用。压缩包仅 6.39MB轻量高效。目前已有 171 人学习下载开发者可直接复用源码、参考变更日志规划升级路径、调用现成 SSL/TLS 支持实现安全通信并通过配套测试工程快速验证组件行为。1. Indy 10.2.3 是什么一个被低估的跨平台网络通信底层工具包不是 SDK 也不是框架而是你写 TCP/UDP/SSL/HTTP 时真正能“拧螺丝”的那一层如果你正在调试一个死活连不上 TLS 1.3 的嵌入式设备客户端或者在 Windows 服务里反复遇到WSAENOTSOCK却查不到 socket 生命周期哪一环被提前关闭又或者用 Python 的aiohttp做高并发代理时卡在 SSL 握手超时、想换更可控的底层实现——那 Indy 10.2.3 就不是“可选”而是你该立刻打开的保险箱。它不是 Python 的requests那种开箱即用的胶水层也不是 Rust 的tokio那种运行时抽象它是 Delphi/CBuilder 生态中存活超过二十年、经受过金融交易系统和工业网关长期锤炼的原生网络协议栈实现集合完整覆盖从原始 socket 封装、异步 I/O 调度、SSL/TLS含 OpenSSL 1.1.1 和 3.x 双支持、HTTP/HTTPS、FTP、POP3、SMTP、NNTP 到 WebSocket 的全链路组件。它不依赖 VCL 或 FMX纯 Pascal 实现头文件级可移植C 项目可通过.h头桥接调用Linux/macOS 下通过 FreePascal 编译零依赖运行。对嵌入式 Qt 工程师、Delphi 遗留系统维护者、需要深度控制 TLS 握手参数的安全中间件开发者以及想把一段稳定网络逻辑复用到多个平台Windows/Linux/ARM64的 C 团队Indy 10.2.3 是少有的、能让你在connect()之后还能手动干预SSL_set_tlsext_host_name()、在send()前精确控制SSL_write_ex()返回值语义的“黑匣子拆解型”资源。2. 编译与集成从源码到可链接静态库绕过 IDE 绑定直取跨平台二进制产物Indy 10.2.3 的官方发布形态是 Delphi/CBuilder 项目组.dpk/.bpi但它的核心价值恰恰藏在Lib目录下的纯 Pascal 单元中——这些单元不依赖任何 IDE 特性可被 FreePascalFPC或现代 C 工具链直接消费。关键在于剥离 IDE 依赖生成标准.aLinux/macOS或.libWindows静态库供非 Delphi 项目调用。下面以 Linux x64 FPC 3.2.2 为例演示如何产出可被 GCC/Clang 链接的libindynet.a。2.1 源码结构解析哪些单元必须保留哪些可以裁剪Indy 10.2.3 源码根目录下有Core、System、Protocols三大子目录。实际编译静态库时仅需以下最小单元集共 27 个.pas文件其余如IdHTTPWebBrokerBridgeWebBroker 专用、IdAntiFreezeVCL 线程防冻等 GUI/IDE 相关单元必须剔除否则会引入不可控的 RTL 依赖目录必选单元节选作用说明System/IdGlobal.pas,IdException.pas,IdStack.pas,IdStackConsts.pas全局类型定义、异常基类、socket 栈抽象层、常量表Core/IdIOHandler.pas,IdIOHandlerSocket.pas,IdSSL.pas,IdSSLOpenSSL.pasI/O 处理器基类、socket 封装、SSL 抽象接口、OpenSSL 实现含 1.1.1/3.x 分支宏Protocols/IdTCPClient.pas,IdTCPConnection.pas,IdHTTP.pas,IdFTP.pasTCP 客户端骨架、连接状态机、HTTP 协议解析器、FTP 控制通道提示IdSSLOpenSSL.pas是关键枢纽它通过{$IFDEF OPENSSL_111}和{$IFDEF OPENSSL_30}条件编译块自动适配不同 OpenSSL 版本。若目标环境为 OpenSSL 3.0需确保编译时定义OPENSSL_30否则SSL_CTX_set_ciphersuites()等新 API 将不可用。2.2 使用 fpcmake 生成 Makefile 并编译静态库进入Indy10.2.3/Lib目录执行以下命令生成跨平台 Makefile# 1. 初始化 fpcmake需已安装 FPC 3.2.2 fpcmake -T linux -P /usr/lib/fpc/3.2.2/ -o Makefile.fpc # 2. 手动编辑 Makefile.fpc将默认的 package target 替换为静态库构建逻辑 # 在文件末尾追加 .PHONY: staticlib staticlib: $(UNITS:.pas.o) ar rcs libindynet.a $^ %.o: %.pas fpc -Mobjfpc -O3 -dLINUX -dINDY_NO_VCL -dOPENSSL_30 -Fu. -Fu../System -Fu../Core -Fu../Protocols -FE. $执行编译make -f Makefile.fpc staticlib # 输出libindynet.a约 2.1 MBstrip 后 890 KB此libindynet.a不含任何 Delphi RTL 符号仅依赖系统 libc 和 OpenSSL 库。验证符号纯净性nm -D libindynet.a | grep -E (Id|SSL|BIO) | head -10 # 正常输出应为U BIO_new_mem_buf, U SSL_connect, T IdTCPClient_Connect, ... # 其中 U 表示未定义需链接 OpenSSLT 表示本库定义的函数2.3 C 项目中调用 Indy 静态库头文件桥接与 ABI 对齐Indy 本身无 C 头文件需手写indy_c_api.h暴露关键函数。核心原则所有字符串传参强制使用 UTF-8 编码所有句柄用void*抽象避免 Pascal 长字符串 ABI 不兼容// indy_c_api.h #ifndef INDY_C_API_H #define INDY_C_API_H #ifdef __cplusplus extern C { #endif // TCP 客户端句柄 typedef void* IndyTCPClient; // 创建客户端host, port, timeout_ms IndyTCPClient indy_tcp_client_create(const char* host, int port, int timeout_ms); // 连接返回 0 成功-1 失败 int indy_tcp_client_connect(IndyTCPClient client); // 发送数据len 为字节数非字符数 int indy_tcp_client_send(IndyTCPClient client, const void* data, size_t len); // 接收数据buf 需预先分配max_len 为 buf 容量返回实际接收字节数 int indy_tcp_client_receive(IndyTCPClient client, void* buf, size_t max_len); // 销毁客户端 void indy_tcp_client_destroy(IndyTCPClient client); #ifdef __cplusplus } #endif #endif对应 C 项目CMakeLists.txt链接配置# 假设 libindynet.a 和 OpenSSL 已安装在 /opt/indy/ find_package(OpenSSL REQUIRED) add_executable(my_app main.cpp) target_link_libraries(my_app /opt/indy/libindynet.a ${OpenSSL_LIBRARIES} ) target_include_directories(my_app PRIVATE /opt/indy/include)参数说明indy_tcp_client_create()中timeout_ms是连接阶段超时不控制后续读写超时读写超时需在indy_tcp_client_connect()后调用indy_tcp_client_set_read_timeout()需自行扩展头文件否则默认阻塞。这是 Indy 的经典设计连接、读、写三阶段超时完全独立比setsockopt(SO_RCVTIMEO)更细粒度。3. SSL/TLS 深度控制从证书验证绕过到 ALPN 协商掌握握手全过程的 7 个关键钩子Indy 10.2.3 的 SSL 能力远超多数人认知——它不是简单封装SSL_connect()而是把 OpenSSL 握手生命周期拆成 7 个可拦截点允许你在ClientHello发出前注入 SNI、在证书链校验失败时动态决定是否信任、甚至在Finished消息后篡改应用层协议协商ALPN。这正是它被金融网关选中的核心原因可控而非透明。3.1 SNI 主机名强制设置解决多租户反向代理场景下的证书错配当你的客户端需访问https://api.tenant-a.example.com但实际连接的是gateway.example.com由 Nginx 反向代理OpenSSL 默认不会发送 SNI 扩展导致服务器返回默认证书常为example.com触发证书域名不匹配错误。Indy 提供OnBeforeConnect钩子在 socket 连接建立后、SSL 握手开始前注入 SNI// Pascal 示例可转译为 C 回调 procedure TForm1.IdHTTP1BeforeConnect(Sender: TObject); begin // 强制设置 SNI即使 Host 头是 tenant-a TIdHTTP(Sender).IOHandler.TransparentProxy : nil; // 确保直连 TIdHTTP(Sender).IOHandler.SSL.SSLOptions.SSLVersions : [sslvTLSv1_2, sslvTLSv1_3]; TIdHTTP(Sender).IOHandler.SSL.SSLOptions.Mode : sslmClient; // 关键设置 SNI 主机名 TIdHTTP(Sender).IOHandler.SSL.SSLOptions.SNIServerName : api.tenant-a.example.com; end;对应 C 层需在indy_tcp_client_create()后调用扩展函数// 扩展头文件需增加 void indy_tcp_client_set_sni_hostname(IndyTCPClient client, const char* hostname); // 使用 IndyTCPClient cli indy_tcp_client_create(gateway.example.com, 443, 5000); indy_tcp_client_set_sni_hostname(cli, api.tenant-a.example.com); // 此行决定证书选择 indy_tcp_client_connect(cli);逻辑说明SNIServerName字段最终映射到 OpenSSL 的SSL_set_tlsext_host_name()。若未设置且 OpenSSL 版本 ≥ 1.1.1SSL_connect()会自动从hostname参数推导但反向代理场景下此推导必然错误必须显式覆盖。3.2 自定义证书验证绕过系统 CA、实现私有 PKI 或证书钉扎Indy 的OnVerifyPeer事件是证书验证的终极开关。它接收 OpenSSL 的X509_STORE_CTX*允许你完全接管验证逻辑——包括忽略自签名错误、检查特定指纹、或加载私有根证书function TForm1.IdHTTP1VerifyPeer(Certificate: TIdX509; AValidate: Boolean; const ADepth, AError: Integer; var AErrorMessage: string): Boolean; begin Result : False; // 默认拒绝 if ADepth 0 then // 只校验终端证书 begin // 方案1钉扎证书公钥指纹SHA256 if Certificate.PublicKey.FingerprintSHA256 A1B2...F0 then Result : True; // 方案2加载私有 CA 证书并验证链 if CustomCAStore.Verify(Certificate.Certificate) then Result : True; end else if ADepth 0 then // 中间证书信任即可 Result : True; end;在 C 中此逻辑需通过indy_tcp_client_set_verify_callback()注册 C 函数指针接收X509*和int depth参数。注意AError为 0 表示 OpenSSL 内部验证通过非 0 表示失败如X509_V_ERR_CERT_HAS_EXPIRED此时AValidate为False但你仍可返回True强制接受。3.3 ALPN 协议协商在 TLS 握手中声明 HTTP/2 或自定义协议ALPNApplication-Layer Protocol Negotiation是 HTTP/2 的基石Indy 10.2.3 通过SSLOptions.AlpnProtocolList支持。但关键细节在于列表顺序决定服务器优先选择权且必须在OnBeforeConnect中设置不能在连接后修改procedure TForm1.IdHTTP1BeforeConnect(Sender: TObject); begin // 优先协商 h2其次 http/1.1 TIdHTTP(Sender).IOHandler.SSL.SSLOptions.AlpnProtocolList.Clear; TIdHTTP(Sender).IOHandler.SSL.SSLOptions.AlpnProtocolList.Add(h2); TIdHTTP(Sender).IOHandler.SSL.SSLOptions.AlpnProtocolList.Add(http/1.1); // 启用 ALPN默认 false TIdHTTP(Sender).IOHandler.SSL.SSLOptions.UseALPN : True; end; // 连接后检查协商结果 procedure TForm1.IdHTTP1Connected(Sender: TObject); var AlpnProto: string; begin AlpnProto : TIdHTTP(Sender).IOHandler.SSL.GetALPNProtocol; if AlpnProto h2 then ShowMessage(HTTP/2 negotiated successfully); end;参数说明AlpnProtocolList是字符串列表每个字符串为协议标识符如h2、myproto/v1长度不能超过 255 字节。Indy 会将其编码为 ALPN 扩展格式RFC 7301发送至服务器。若服务器不支持任何列表中协议GetALPNProtocol返回空字符串此时应降级处理。4. HTTP/HTTPS 高级用法连接池复用、流式上传、Chunked 编码与响应体截断控制Indy 的TIdHTTP组件常被误认为“只是 Delphi 版 curl”但它在连接管理、内存控制和协议细节上提供了远超 curl 的精度。尤其在 IoT 设备上报、日志批量上传等长周期 HTTP 场景中其连接池、流式处理和响应体边界控制能力是稳定性的关键。4.1 连接池复用避免 TIME_WAIT 泛滥与 SSL 握手开销默认情况下TIdHTTP每次请求都新建 TCP 连接并执行完整 TLS 握手对高频请求如每秒 10 次造成巨大开销。Indy 通过TIdConnectionPool实现连接复用但需手动绑定TIdHTTP.IOHandler// 全局连接池线程安全 FConnPool : TIdConnectionPool.Create(nil); FConnPool.MaxConnections : 20; FConnPool.ConnectionTimeout : 30000; // 30秒空闲超时 // 为每次 HTTP 请求复用 IOHandler procedure TForm1.MakeHttpRequest; var HTTP: TIdHTTP; IOHandler: TIdIOHandlerSocket; begin HTTP : TIdHTTP.Create(nil); try // 从连接池获取 IOHandler自动复用 socket IOHandler : TIdIOHandlerSocket(FConnPool.GetConnection(https://api.example.com:443)); HTTP.IOHandler : IOHandler; HTTP.Request.UserAgent : MyIoTClient/1.0; HTTP.Get(https://api.example.com/data?ts DateTimeToStr(Now)); finally HTTP.Free; // 注意不调用 IOHandler.Free由连接池管理 end; end;逻辑说明TIdConnectionPool.GetConnection()返回已建立的TIdIOHandlerSocket实例其内部 socket 处于Connected状态。TIdHTTP使用该 IOHandler 时跳过connect()和SSL_connect()直接发送 HTTP 请求。连接空闲超时后池自动关闭 socket 并清理 SSL 上下文。4.2 流式上传大文件避免内存爆炸支持断点续传基础上传 GB 级日志文件时TIdHTTP.Post()若传入TFileStreamIndy 默认将其全部读入内存再分块发送极易 OOM。正确做法是使用TIdMultipartFormDataStream并设置WriteTimeout与ReadTimeoutprocedure TForm1.UploadLogFile(const AFileName: string); var HTTP: TIdHTTP; FormData: TIdMultipartFormDataStream; FileStream: TFileStream; begin HTTP : TIdHTTP.Create(nil); try HTTP.ReadTimeout : 60000; // 读响应超时 60s HTTP.ConnectTimeout : 10000; // 连接超时 10s HTTP.Request.ContentLength : -1; // 启用 Chunked 编码 FormData : TIdMultipartFormDataStream.Create; try // 添加文本字段 FormData.AddFormField(device_id, iot-001); FormData.AddFormField(timestamp, DateTimeToStr(Now)); // 添加文件流关键不加载全文流式读取 FileStream : TFileStream.Create(AFileName, fmOpenRead or fmShareDenyWrite); try FormData.AddFile(log_file, FileStream, application/octet-stream); finally FileStream.Free; end; // 执行上传内部自动分块每块 8KB HTTP.Post(https://api.example.com/upload, FormData); finally FormData.Free; end; finally HTTP.Free; end; end;参数说明AddFile()的第三个参数是 MIME 类型application/octet-stream表示二进制流Request.ContentLength : -1强制 Indy 使用Transfer-Encoding: chunked避免预计算文件大小。Chunked 每块大小由TIdMultipartFormDataStream.FChunkSize控制默认 8192 字节可按需调整。4.3 响应体截断控制只读取前 N 字节避免下载大文件当调用HTTP.Get()获取一个可能达 GB 的资源如固件镜像但只需校验前 1024 字节的 Magic Number 时TIdHTTP的OnWork事件配合Response.KeepAlive可实现精准截断var FBytesRead: Integer; FMaxRead: Integer; procedure TForm1.IdHTTP1Work(Sender: TObject; AWorkMode: TWorkMode; const AWorkCount: Integer); begin if AWorkMode wmRead then begin Inc(FBytesRead, AWorkCount); if FBytesRead FMaxRead then TIdHTTP(Sender).Disconnect; // 立即断开 socket终止读取 end; end; procedure TForm1.CheckFirmwareHeader; begin FBytesRead : 0; FMaxRead : 1024; IdHTTP1.OnWork : IdHTTP1Work; IdHTTP1.HandleRedirects : False; IdHTTP1.Request.BasicAuthentication : False; try // Get 返回前 1024 字节到内存流 IdHTTP1.Get(https://firmware.example.com/latest.bin, TMemoryStream.Create); except on E: EIdHTTPProtocolException do if E.ErrorMessage.Contains(Connection closed) then // 截断成功继续解析流 ParseHeaderFromStream(IdHTTP1.Response.RawContent); end; end;逻辑说明OnWork(wmRead)在每次从 socket 读取数据后触发AWorkCount为本次读取字节数。累加FBytesRead达阈值后调用Disconnect()Indy 会立即关闭底层 socketGet()方法抛出EIdHTTPProtocolException因连接意外中断但Response.RawContent已包含已读取的数据。这是 Indy 独有的“半读取”能力curl 或 wget 无法做到。5. 避坑指南生产环境踩过的 5 个真实血泪坑现象、原因与一招修复Indy 10.2.3 稳定性极高但因其深度控制网络栈某些边界场景的坑极具迷惑性。以下是某工业网关项目中真实发生的 5 个高频问题按“现象 → 原因 → 解决”结构整理每一条都附带可验证的代码片段。5.1 现象TIdHTTP.Get()随机返回空响应Wireshark 显示服务器已发回 200 OK 和 Body原因TIdHTTP默认启用hoNoProtocolErrorException当服务器返回4xx/5xx时它仍会读取响应体并赋值给Response.RawContent但若OnWork事件中误判了读取进度或Response.KeepAlive为False导致连接提前关闭则后续读取失败RawContent为空。解决显式禁用异常转换并在OnWork中严格监控读取状态// 错误示范依赖默认行为 HTTP.Get(https://api.example.com/status); // 可能返回空 // 正确做法强制读取忽略状态码异常 HTTP.HTTPOptions : HTTP.HTTPOptions - [hoNoProtocolErrorException]; HTTP.OnWork : OnWorkCheckRead; HTTP.Get(https://api.example.com/status); procedure TForm1.OnWorkCheckRead(Sender: TObject; AWorkMode: TWorkMode; const AWorkCount: Integer); begin if AWorkMode wmRead then begin // 记录已读字节数确保不为 0 FLastRead : AWorkCount; end; end; // 调用后检查 if FLastRead 0 then raise Exception.Create(No data received from server);5.2 现象Linux 下TIdTCPServer子线程 CPU 占用 100%strace显示epoll_wait()返回 0原因TIdTCPServer的OnExecute事件中若未调用Sleep(0)或Yield()且处理逻辑极快如仅做日志打印线程会陷入忙等待。Indy 的TIdSchedulerOfThreadDefault默认使用epoll但epoll_wait()超时为 0 时立即返回形成死循环。解决在OnExecute开头强制让出时间片或设置Scheduler.YieldIntervalprocedure TForm1.IdTCPServer1Execute(AContext: TIdContext); begin // 关键防止忙等待 Sleep(0); // 或 Yield(); // 处理逻辑... AContext.Connection.IOHandler.WriteLn(OK); end; // 或全局设置推荐 IdTCPServer1.Scheduler : TIdSchedulerOfThreadDefault.Create(nil); TIdSchedulerOfThreadDefault(IdTCPServer1.Scheduler).YieldInterval : 1; // 每 1ms 检查一次5.3 现象TIdSSLIOHandlerSocketOpenSSL在 OpenSSL 3.0 下Connect()失败错误码SSL_ERROR_SSL原因OpenSSL 3.0 默认禁用旧版加密算法如 TLS 1.0/1.1且SSL_CTX_set_options()的默认行为变更。Indy 10.2.3 的SSLOptions.SSLVersions若未显式包含sslvTLSv1_2或sslvTLSv1_3则上下文可能为空。解决强制指定 TLS 版本并禁用不安全选项// 必须显式设置不能依赖默认 IOHandler.SSL.SSLOptions.SSLVersions : [sslvTLSv1_2, sslvTLSv1_3]; IOHandler.SSL.SSLOptions.Method : sslvTLSv1_2; // 主版本 // 禁用不安全选项OpenSSL 3.0 要求 IOHandler.SSL.SSLOptions.Options : IOHandler.SSL.SSLOptions.Options - [sslOpt_SSLv2, sslOpt_SSLv3, sslOpt_TLSv1];5.4 现象TIdHTTP上传大文件时服务器收到不完整数据Content-Length与实际不符原因TIdMultipartFormDataStream在添加文件时若文件路径含 Unicode 字符如中文AddFile()内部调用TIdMultiPartFormDataItem.SetFileName()会错误编码文件名导致边界符错位整个 multipart body 解析失败。解决上传前预处理文件名强制 ASCII 编码// 错误直接传入中文路径 FormData.AddFile(log, 设备日志-2023.log, text/plain); // 正确提取纯 ASCII 文件名 function GetAsciiFileName(const AFileName: string): string; var i: Integer; begin Result : ; for i : 1 to Length(AFileName) do if (AFileName[i] A) and (AFileName[i] z) or (AFileName[i] 0) and (AFileName[i] 9) or (AFileName[i] in [_, -, .]) then Result : Result AFileName[i]; if Result then Result : upload.bin; end; FormData.AddFile(log, GetAsciiFileName(设备日志-2023.log), text/plain);5.5 现象TIdTCPClient连接后立即Disconnect()但netstat显示连接仍处于TIME_WAIT状态长达 60 秒原因Disconnect()仅关闭 socket未调用setsockopt(SO_LINGER)。Linux 默认 linger 时间为 0但若 socket 处于FIN_WAIT_2状态内核仍会维持TIME_WAIT。Indy 未暴露 linger 设置接口。解决在Disconnect()后用ioctlsocket()强制关闭Windows或setsockopt()Linux// Linux 下 C 扩展函数 void indy_tcp_client_force_close(IndyTCPClient client) { int sock *(int*)client; // 假设 client 内部存储 socket fd struct linger ling {1, 0}; // l_onoff1, l_linger0 setsockopt(sock, SOL_SOCKET, SO_LINGER, ling, sizeof(ling)); close(sock); } // 调用 indy_tcp_client_disconnect(cli); indy_tcp_client_force_close(cli); // 立即释放端口6. 生产就绪技巧用TIdLogDebug抓取完整 TLS 握手日志定位证书链、SNI、ALPN 的每一帧交互在客户现场排查“为什么我的证书不被信任”或“ALPN 怎么没协商上”时Wireshark 抓包需解密 TLS而服务器私钥往往不可得。Indy 10.2.3 内置的TIdLogDebug是唯一能在客户端侧打印明文握手日志的方案——它不依赖 OpenSSL 的SSL_trace()需 recompile OpenSSL而是直接 hook Indy 的TIdSSLIOHandlerSocketOpenSSL内部读写缓冲区在加密前/解密后记录原始字节。这才是真正的“所见即所得”。6.1 启用全量 TLS 日志从 ClientHello 到 Application Data 的逐帧解析TIdLogDebug默认只记录 HTTP 层要捕获 TLS必须将其挂载到IOHandler的OnStatus和OnStatusInfo事件并重写TIdSSLIOHandlerSocketOpenSSL的WriteBuffer()和ReadBuffer()方法。但 Indy 10.2.3 提供了更优雅的方式继承TIdSSLIOHandlerSocketOpenSSL并重载DoStatus()type TIdSSLIOHandlerSocketOpenSSL_Debug class(TIdSSLIOHandlerSocketOpenSSL) private FLog: TIdLogDebug; protected procedure DoStatus(AStatus: TIdStatus; const AStatusText: string); override; procedure DoStatusInfo(AStatus: TIdStatus; const AStatusText: string; const AStatusData: TIdBytes); override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; end; constructor TIdSSLIOHandlerSocketOpenSSL_Debug.Create(AOwner: TComponent); begin inherited Create(AOwner); FLog : TIdLogDebug.Create(nil); FLog.Filename : tls_handshake.log; FLog.Active : True; end; destructor TIdSSLIOHandlerSocketOpenSSL_Debug.Destroy; begin FLog.Free; inherited Destroy; end; procedure TIdSSLIOHandlerSocketOpenSSL_Debug.DoStatus(AStatus: TIdStatus; const AStatusText: string); begin inherited; if AStatus in [hsStatusConnecting, hsStatusConnected, hsStatusDisconnecting] then FLog.WriteLog(AStatusText); end; procedure TIdSSLIOHandlerSocketOpenSSL_Debug.DoStatusInfo(AStatus: TIdStatus; const AStatusText: string; const AStatusData: TIdBytes); begin inherited; // 关键捕获 TLS 握手消息ClientHello, ServerHello, Certificate 等 if AStatus hsStatusInfo then begin if Length(AStatusData) 0 then begin // 解析 TLS 记录头Content Type (1), Version (2), Length (2) if Length(AStatusData) 5 then begin case AStatusData[0] of 22: FLog.WriteLog(Format(TLS Handshake: %s [%d bytes], [GetHandshakeTypeStr(AStatusData[5]), Length(AStatusData)])); 20: FLog.WriteLog(TLS ChangeCipherSpec); 23: FLog.WriteLog(TLS Application Data); end; end; end; end; end;然后在 HTTP 客户端中使用HTTP : TIdHTTP.Create(nil); HTTP.IOHandler : TIdSSLIOHandlerSocketOpenSSL_Debug.Create(nil); HTTP.Get(https://api.example.com/health); // 日志文件 tls_handshake.log 将包含 // TLS Handshake: ClientHello [224 bytes] // TLS Handshake: ServerHello [128 bytes] // TLS Handshake: Certificate [1842 bytes] // TLS Handshake: ServerHelloDone [4 bytes] // TLS Handshake: Finished [64 bytes]6.2 解析日志定位三类典型问题SNI、证书链、ALPN生成日志后用文本搜索快速定位问题问题类型搜索关键词日志特征诊断逻辑SNI 缺失ClientHello日志中ClientHello后无extension_type: 0SNI 扩展 ID说明SNIServerName未设置或设置时机错误必须在OnBeforeConnect证书链不全CertificateCertificate消息中只有一张证书长度 2000 字节无中间证书服务器未配置完整证书链需联系运维补全fullchain.pemALPN 协商失败ClientHello/ServerHelloClientHello含extension_type: 16ALPN但ServerHello无此扩展服务器未启用 ALPN或AlpnProtocolList中协议服务器不支持提示GetHandshakeTypeStr()是辅助函数根据 TLS 握手消息类型字节AStatusData[5]返回字符串如1→HelloRequest2→ClientHello12→Certificate14→ServerHelloDone。6.3 从那以后我每次部署新网关都强制走一遍 TLS 日志抓取流程在某跨平台工业网关项目中我们曾花三天排查一个“证书信任失败”问题最终发现是客户提供的根证书 PEM 文件末尾多了两个空行导致 Indy 的LoadFromFile()解析失败但错误被静默吞掉。从那以后我每次交付新设备固件都会在启动脚本中加入一行# 启动时自动开启 TLS 调试日志仅 debug 版本 echo Starting with TLS debug log... /var/log/gateway.log ./gateway --debug-tls-log/tmp/tls_debug_$(date %s).log日志文件按时间戳命名永不覆盖且只在 debug 模式下启用不影响生产性能。当客户报障时我只要索要最近的tls_debug_*.log5 分钟内就能定位到是 SNI 错了、ALPN 不匹配还是证书链断裂。这种本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?