Indy 10.6.3.3:Delphi 12 网络组件安装与 HTTPS 避坑 简介Indy 10.6.3.3 是面向 Delphi 与 CBuilder 开发者的跨平台网络组件库源码包适合需要快速实现 TCP/IP、HTTP、FTP 等通信功能的中高级开发者。它把多种互联网协议封装成可直接拖放的组件包含连接管理与网页请求等常用部件并支持异步执行和事件回调可用于邮件客户端、Web 服务、文件传输与聊天程序等场景。压缩包共 2000 个文件约 9.67MB以 pas 源码、dpk 包文件、dproj 工程、bmp 与 ico 资源以及 txt、rc、res 等说明和构建文件为主目录兼顾编译安装、示例参考与资源生成。包内保留核心协议单元、设计期包与多版本工程文件便于在 RAD Studio 中按需编译和二次开发完整源码也方便按项目需求裁剪、修改和扩展协议实现并对照组件结构排查连接、收发与超时等问题。已有 362 人学习适合作为 Delphi 网络应用开发的基础组件资料对理解组件化网络编程与协议封装亦有参考价值。1. 解压先别急着点 InstallIndy 10.6.3.3 换掉的是 Delphi 12 的整套网络底座Delphi 控件里最容易被低估的一类是那种平时看不见、一旦出问题就全盘停摆的底层库Indy 就是典型。IndyInternet Direct本来是 Delphi 12 安装包里自带的网络组件集TIdHTTP、TIdFTP、TIdSMTP、TIdTCPClient、TIdTCPServer 这一批控件全出自它而随 IDE 预装的那份版本往往偏旧碰上 TLS 1.3、新版 SSL 运行库或者某些服务端不常见的握手方式就容易出问题。这份 Indy-10.6.3.3 给的是完整源码加设计期包覆盖上去等于给整个网络层换发动机——协议实现和 SSL 后端都更新了代价是 IDE 本身也依赖这套包装错顺序连 Delphi 都打不开。它适合手里有存量 Delphi 项目、被 HTTPS 或长连接折腾过、又不想引入第三方网络框架的人。2. 编译与安装六个包的设计期与运行期顺序不能乱Indy 不是一个 dpk 包打包全部而是按「基础 → 核心 → 协议」切成三层每层再分运行期包和设计期包。很多人解压后直接在 IDE 里双击 dclIndyProtocols.dpk 点 Install然后报一堆「Cannot load package」原因几乎都是跳过了下层的运行期包。这一章先把包组结构拆清楚再给一套可以照着敲的命令行编译流程最后才是 IDE 里挂包和清 DCU。2.1 包组结构与依赖方向先把解开后的目录看一眼Core、System、Protocols 三个源码目录配上根目录下若干 dpk/dproj。它们的依赖是单向的编译顺序不能反。包文件类型依赖主要内容IndySystem.dpk运行期无IdGlobal、IdException、IdStack、IdResourceStrings 等基础单元IndyCore.dpk运行期IndySystemIdTCPClient、IdTCPServer、IdIOHandler、IdContext、IdThreadIndyProtocols.dpk运行期IndyCoreIdHTTP、IdFTP、IdSMTP、IdPOP3、IdSSLOpenSSL 等协议单元dclIndySystem.dpk设计期IndySystem.bpl注册基础控件到工具面板dclIndyCore.dpk设计期IndyCore.bpl注册 TIdTCPClient、TIdTCPServer 等dclIndyProtocols.dpk设计期IndyProtocols.bpl注册 TIdHTTP、TIdFTP、TIdSSLIOHandlerSocketOpenSSL 等三条判断依据值得记住。第一运行期包产出的是 .bpl程序运行时要么静态链接要么带着这些 bpl 走所以它必须先于设计期包编译出来。第二设计期包只负责把组件注册进 IDE它自己不包含实现代码只是引用运行期包所以 IDE 位数决定设计期包只能编 Win32。第三Indy 的单元之间存在大量隐式引用比如 IdHTTP 引用了 IdIOHandler、IdSSLOpenSSL 又引用 IdSSLOpenSSLHeaders如果只编了 IndyProtocols 而 IndyCore 的 DCU 是旧版链接阶段就会报「Unit IdIOHandler was compiled with a different version of IdGlobal」。2.2 用命令行把三个运行期包先编出来比在 IDE 里点 Build 更可控的做法是走 msbuild尤其当你需要同时出 Win32 和 Win64、或者要写自动化脚本的时候。Delphi 安装时会往系统环境里写一个 BDS 变量指向 Studio 的安装根目录rsvars.bat 就在它的 bin 下作用是初始化编译所需的环境变量。:: 1) 初始化编译环境BDS 由安装程序写入没写就手动 set 一下 call %BDS%\bin\rsvars.bat :: 2) 严格按依赖顺序编译路径切到解压后的 Indy 根目录 cd /d D:\src\Indy-10.6.3.3 :: 3) 先基础层再核心层最后协议层 msbuild IndySystem.dproj /t:Build /p:ConfigRelease /p:PlatformWin32 msbuild IndyCore.dproj /t:Build /p:ConfigRelease /p:PlatformWin32 msbuild IndyProtocols.dproj /t:Build /p:ConfigRelease /p:PlatformWin32这三条命令的逻辑是串行的IndyCore 编译时需要读 IndySystem 产出的 .dcu 和 .bpl所以上一条不成功就不要往下走。参数上ConfigRelease是为了让设计期挂进 IDE 的那份包体积小、断言少调试信息留给项目自己的 Debug 配置PlatformWin32是因为 Delphi 12 的 IDE 进程本身是 32 位设计期包必须是 Win32运行期包才可以按目标平台再编一遍 Win64。如果你只关心 64 位程序也要先把 Win32 的三个运行期包和设计期包编出来否则 IDE 里根本拖不出 TIdHTTP。编译完检查输出目录应该能看到 IndySystem.bpl、IndyCore.bpl、IndyProtocols.bpl 以及对应的 .dcp。少了任何一个下一步挂设计期包一定失败。2.3 在 IDE 里挂设计期包并清掉旧 DCU命令行编完只是有了文件IDE 还不知道。挂载这一步有几个顺序上的讲究第一步关掉所有打开的项目先备份%BDS%\bin目录下所有 Indy 开头的 bpl。Delphi 12 的 IDE 自身就链接了 Indy替换失败会导致 IDE 启动报错有备份才不至于重装。第二步Component → Install Packages在列表里把已有的 Indy 相关设计期包逐个 Remove。这一步不做新包和旧包会同时注册同名组件工具面板上出现两个 TIdHTTP编译时报「Duplicate identifier」。第三步Add依次选 dclIndySystem.bpl、dclIndyCore.bpl、dclIndyProtocols.bpl。顺序不能反因为 dclIndyProtocols 加载时会去解析对 IndyProtocols.bpl 的引用而 IndyProtocols.bpl 又依赖 IndyCore.bpl。第四步Tools → Options → Language → Delphi → Library把 System、Core、Protocols 三个源码目录加到 Library path 的最前面。放在最前面是关键否则编译器会优先命中 IDE 自带路径下的旧 DCU编译通过但行为还是旧版。第五步清 DCU。搜一遍项目目录和公共 DCU 输出目录把 Id 开头的 .dcu 全删掉。Indy 的单元名很集中Id*.dcu通配基本不会误伤。清完重新 Build All再拖一个 TIdHTTP 到空窗体上能编译、能出现属性编辑器安装就算过了。3. TIdTCPClient 与 TIdTCPServer一条能收发的闭环怎么搭Indy 里最容易被新手跳过的其实是 TCP 这一层大家都直奔 TIdHTTP因为 HTTP 简单。但只要涉及长连接、私有协议、需要服务端主动推送最后还是要回到 TIdTCPClient/TIdTCPServer。这两个组件看似只有几个属性真正会出问题的地方集中在三处超时、报文边界、以及 OnExecute 到底跑在哪个线程里。3.1 长连接场景为什么优先选 TCP 组件而不是 HTTP选型上有个朴素判断如果你的交互是「客户端发一次、服务端答一次然后结束」用 TIdHTTP 最省事它帮你处理了连接复用、重定向、分块传输。但如果服务端需要在没有请求的时候主动下发消息或者一次交互里有多轮往返鉴权、订阅、心跳HTTP 就要靠轮询或长轮询去模拟成本和不确定性都上去了。TIdTCPClient 给的是裸的字节流好处是完全可控什么时候连、连多久、发什么、等多久、断了怎么重连全在你手里。代价是报文边界要自己定。TCP 本身是字节流没有消息的概念一次 WriteLn 发出去的内容对端可能分两次收到也可能和下一次的内容粘在一起。这就是为什么 Indy 提供了 ReadLn 这种带终止符的读法——它在帮你把字节流切回消息。还有个实际因素TIdTCPServer 自带连接上下文管理、最大连接数限制、线程池调度这些在 TIdHTTP 里是隐藏的。你需要看「当前有多少个客户端连着」的时候IdTCPServer1.Contexts.LockList一句就能拿到比在 HTTP 层自己数会话简单得多。3.2 客户端超时、编码与连接生命周期客户端的坑几乎全在超时上。TIdTCPClient 有两个独立超时ConnectTimeout 管建连ReadTimeout 管读阻塞。默认值给得比较宽松在弱网或者对端假死的时候一个 Read 能挂很久。uses IdTCPClient, IdGlobal, IdException; function TcpPing(const AHost: string; APort: Integer): string; var Client: TIdTCPClient; begin Client : TIdTCPClient.Create(nil); try Client.Host : AHost; Client.Port : APort; Client.ConnectTimeout : 3000; // 建连上限含 DNS 解析时间 Client.ReadTimeout : 5000; // 单次 Read 阻塞上限超时抛 EIdReadTimeout try Client.Connect; try // 显式指定 UTF-8避免中文在两端编码不一致时变成乱码 Client.IOHandler.WriteLn(PING, IndyTextEncoding_UTF8); Result : Client.IOHandler.ReadLn(LF, Client.ReadTimeout, 4096, IndyTextEncoding_UTF8); finally Client.Disconnect; end; except on E: EIdConnectTimeout do Result : CONNECT_TIMEOUT on E: EIdReadTimeout do Result : READ_TIMEOUT on E: EIdSocketError do Result : SOCKET_ERROR: IntToStr(E.LastError); end; finally Client.Free; end; end;这段代码有几个点值得说明。ConnectTimeout单位是毫秒它覆盖的不只是三次握手DNS 解析也在这个预算里所以填 3000 意味着解析慢的域名会直接判超时。ReadLn的四个参数依次是终止符、超时、单行最大字节数、文本编码把AMaxLineLength填成 4096 是一种防御防止对端发来一条几十兆、没有换行的「脏数据」把内存打满。异常类型上连接超时抛 EIdConnectTimeout读超时抛 EIdReadTimeout两者都继承自 EIdException但分开捕获能让你在日志里一眼看出是网络没通还是对端不回。连接生命周期上Connect和Disconnect要成对出现而且 Disconnect 放在内层 finallyFree 放在外层 finally。原因是 Connect 抛异常时连接根本没建立此时调 Disconnect 虽然无害但没意义而一旦连上无论读写是否异常都必须断开否则套接字句柄会泄漏。3.3 服务端OnExecute 在哪个线程里跑这是 Indy 新手翻车率最高的一点。TIdTCPServer 的 OnExecute 不是主线程回调而是在每条连接自己的上下文线程里执行。也就是说你在这个事件里碰任何 VCL 控件改 Label.Caption、往 Memo 里追加日志、弹 MessageBox轻则界面卡住重则访问违例或者随机崩溃——因为 VCL 的窗口句柄有线程亲和性。uses IdContext, IdTCPConnection, IdGlobal, IdException; procedure TForm1.FormCreate(Sender: TObject); begin IdTCPServer1.Bindings.Clear; with IdTCPServer1.Bindings.Add do begin IP : 0.0.0.0; // 监听所有网卡只给本机用就填 127.0.0.1 Port : 9000; end; IdTCPServer1.MaxConnections : 200; // 超过直接拒绝防止连接打满 IdTCPServer1.ListenQueue : 50; // 已完成握手但未被取走的队列长度 IdTCPServer1.TerminateWaitTime : 5000; // 停服时等各线程收尾的上限 IdTCPServer1.Active : True; end; procedure TForm1.IdTCPServer1Execute(AContext: TIdContext); var Req: string; begin try // 读一行5 秒没等到换行就按超时处理单行上限 4KB Req : AContext.Connection.IOHandler.ReadLn(LF, 5000, 4096, IndyTextEncoding_UTF8); except on E: EIdReadTimeout do Exit; // 客户端连上不发数据直接放弃这条连接 end; if SameText(Trim(Req), PING) then AContext.Connection.IOHandler.WriteLn(PONG, IndyTextEncoding_UTF8) else AContext.Connection.IOHandler.WriteLn(ERR UNKNOWN, IndyTextEncoding_UTF8); // 需要更新界面时必须切回主线程不能直接写控件属性 TThread.Queue(nil, procedure begin Memo1.Lines.Add(recv: Req); end); end;逻辑上有几处需要展开。Bindings是监听地址列表默认可能绑在 127.0.0.1 上做成服务端给别的机器连就会发现连不通所以先 Clear 再显式 Add 是稳妥写法。MaxConnections是硬上限超过之后新连接会被直接关掉不给 OnConnect 机会这在被扫描或者被压测的时候很有用。ListenQueue对应底层 listen 的 backlog短连接高并发场景下如果偏小会出现客户端已经握手成功但服务端迟迟不取的情况。异常处理那一层是必须的。OnExecute 里抛出的异常如果不捕获Indy 会把它吞掉并断开连接表面看是「客户端莫名其妙掉了」日志里什么都没有。显式捕获 EIdReadTimeout 并 Exit至少能让「对端连上不发数据」这种情况可控。TThread.Queue 那一段是线程安全的写法Queue 是异步投递不阻塞工作线程对应的 TThread.Synchronize 是同步等待在主线程忙的时候会把工作线程一起拖住连接数一多就雪崩。所以除了极小规模的工具都建议用 Queue。3.4 报文边界ReadLn 的终止符与最大长度ReadLn 的两个参数——终止符和最大长度——决定了你的协议能不能稳定工作。终止符默认是 LF#10但很多设备端发的是 CRLF#13#10。这时候如果两边不约定清楚服务端会读到「PING」后面跟一个 #13Trim之后才能比对上一旦业务数据里本身包含 #13切分就乱了。常见做法是协议头里带长度字段或者用定长头 变长体。如果坚持用行协议务必在两边统一成 LF服务端读到的内容末尾不要做隐式 Trim 之外的任何处理。最大长度一定要设置Indy 允许传 -1 表示不限这在实际部署里等于给了一个内存炸弹——对端只要持续发不含终止符的字节缓冲区就一直涨。大包场景不适合 ReadLn。行读取是逐字节扫描找终止符几 MB 的报文用它会明显变慢正确做法是用ReadBytes或者ReadStream先读 4 字节长度头再按长度读体。4. TIdHTTP 与 SSL把 HTTPS 请求真正跑通的参数清单TIdHTTP 是 Indy 里使用频率最高的组件也是「看起来一行就能跑、实际各种报错」的典型。它本身的封装很薄几乎所有行为都由 IOHandler 和 Request 上的属性决定。这一章按「归属权 → 参数 → 落地代码 → SSL 运行库」的顺序走前三步决定你的代码会不会崩第四步决定 HTTPS 能不能通。4.1 IOHandler 的归属权一个 double free 的经典现场TIdHTTP 自己会创建一个默认的 IOHandler纯 socket不带 SSL。当你要访问 HTTPS就必须手动挂一个 TIdSSLIOHandlerSocketOpenSSL 上去。问题出在释放上TIdHTTP 的析构函数里会 Free 掉当前的 IOHandler所以如果你在 finally 里既Http.Free又SSL.Free第二次释放就是访问违例而且崩的位置往往在程序退出时堆栈看着跟 HTTP 毫无关系。正确做法是要么把 SSL 的 Owner 设成 HttpTIdSSLIOHandlerSocketOpenSSL.Create(Http)要么创建时 Owner 给 nil、但只 Free Http。两种都行核心原则是同一个对象只能有一个释放者。我一般用前者因为对象树清晰出问题看 Owner 就知道谁管谁。顺带一提给Http.IOHandler赋值时Indy 内部会把旧的默认 handler 释放掉所以你不需要也不应该自己去释放它。4.2 关键参数表下面这张表是我在几个项目里反复调出来的常用值具体数字按你的服务端 SLA 调整但项目别留空。参数常用取值说明ConnectTimeout3000 ~ 5000建连阶段上限含 DNS单位毫秒ReadTimeout15000 ~ 30000单次读阻塞上限超时抛 EIdReadTimeoutHandleRedirectsTrue自动跟随 301/302注意 POST 被重定向后可能退化成 GETRedirectMaximum5重定向次数上限防止循环跳转Request.ContentTypeapplication/json; charsetutf-8POST 时必须显式设置否则服务端可能按表单解析Request.Acceptapplication/json影响服务端的内容协商Request.UserAgent自定义字符串留空会被部分网关直接拒绝SSLOptions.SSLVersions[sslvTLSv1_2, sslvTLSv1_3]不要留空留空走库默认值可能协商到不再被接受的旧协议SSLOptions.ModesslmClient客户端固定这个值SSLOptions.VerifyMode[sslvrfPeer]空集合等于不校验服务端证书生产环境不要这么干SSLOptions.VerifyDepth3证书链校验深度SSLOptions.RootCertFileCA 证书路径留空则用系统证书存储IOHandler.Deflate.CompressionLevel0 或 6开启 gzip 压缩服务端支持时能显著减小正文SSLVersions这一项值得单独说。很多人为了「兼容性」把 SSLv3、TLSv1.0 全塞进去结果服务端反而拒绝握手因为现代网关会直接拒掉低版本协议。反过来只写 sslvTLSv1_3 也不行一些内网服务还停留在 1.2。给 [sslvTLSv1_2, sslvTLSv1_3] 是最稳的起点。4.3 GET 与 POST 的落地写法先看一个带 JSON 体的 POST涵盖超时、头部、编码三件事。uses IdHTTP, IdSSLOpenSSL, IdSSL, IdGlobal, SysUtils, Classes; function HttpPostJson(const AUrl, AJson: string; out AStatus: Integer): string; var Http: TIdHTTP; SSL: TIdSSLIOHandlerSocketOpenSSL; Src: TStringStream; begin Http : TIdHTTP.Create(nil); try // Owner 交给 Http由 TIdHTTP 在析构时统一释放避免 double free SSL : TIdSSLIOHandlerSocketOpenSSL.Create(Http); SSL.SSLOptions.Mode : sslmClient; SSL.SSLOptions.SSLVersions : [sslvTLSv1_2, sslvTLSv1_3]; SSL.SSLOptions.VerifyMode : [sslvrfPeer]; Http.IOHandler : SSL; Http.ConnectTimeout : 5000; Http.ReadTimeout : 20000; Http.HandleRedirects : True; Http.RedirectMaximum : 5; Http.Request.ContentType : application/json; charsetutf-8; Http.Request.Accept : application/json; Http.Request.UserAgent : DelphiClient/1.0; Http.Request.CustomHeaders.Values[X-Trace-Id] : demo-0001; // TStringStream 显式指定 UTF-8中文才不会按 ANSI 发出去 Src : TStringStream.Create(AJson, TEncoding.UTF8); try Result : Http.Post(AUrl, Src); AStatus : Http.ResponseCode; finally Src.Free; end; finally Http.Free; // IOHandler 随 Http 一起释放不要再 Free SSL end; end;几个参数的作用ReadTimeout设 20 秒是因为不少接口在冷启动时首字节要等十几秒设太短会频繁误判超时CustomHeaders用来塞链路追踪 ID排查跨服务问题时能对上日志TStringStream的编码参数不能省默认会用系统 ANSI 页JSON 里只要有中文服务端收到的就是一堆问号。GET 加查询参数时不要手动拼字符串用TIdURI.ParamsEncode做转义否则参数里带、、中文就会把请求拆坏。下载文件用Http.Get(AUrl, AFileStream)的重载配合OnWork事件可以拿到进度在OnWork里判断超时或者用户取消抛一个自定义异常就能中断传输这是 Indy 提供的少数几个中断点之一。4.4 SSL 运行库放哪、怎么确认加载成功HTTPS 报「Could not load SSL library」几乎是必踩的一关。原因通常只有三种位数不对、文件名不对、搜索路径不对。位数上32 位程序必须配 32 位运行库64 位配 64 位。文件名上Indy 会按一串候选名去试3.x 系列是 libssl-3.dll / libcrypto-3.dll64 位版本带 -x64 后缀1.1 系列是 libssl-1_1.dll 这一类。搜索路径上最省事的做法是把两个 DLL 放到 exe 同目录这样不依赖系统目录部署也好打包。确认是否加载成功别靠猜uses IdSSLOpenSSLHeaders; procedure TForm1.CheckSSL; begin if not LoadOpenSSLLibrary then raise Exception.Create(SSL 运行库加载失败: WhichFailedToLoad); Memo1.Lines.Add(SSL 版本: OpenSSLVersion); end;WhichFailedToLoad会直接告诉你它试过的候选名里哪些失败了对着这个名字去补 DLL 就行比反复重启程序快得多。另外还有一个容易忽略的点如果程序目录里同时存在 1.1 和 3.x 两套 DLLIndy 会按内部顺序挑一个可能出现「本机好好的、客户机器上握手失败」统一成一套、删掉多余的能省掉很多玄学问题。5. 避坑与排查五个让我返工的现场前面几章讲的是「怎么装、怎么写」这一章讲「为什么照写了还是不对」。下面五条都是我在实际项目里返工过的按「现象 → 原因 → 解决」写遇到症状对得上的可以直接跳。5.1 编译报「compiled with a different version of IdGlobal」现象新装的包新建空项目拖一个 TIdHTTP编译立刻报单元版本不一致提示的却是 IdGlobal 这种基础单元。原因旧 DCU 残留。Delphi 的编译缓存是按单元名找的项目搜索路径、公共 DCU 输出目录、IDE 自带路径里任意一处有旧版 IdGlobal.dcu编译器就可能先命中它然后和新建的 IndyCore.dcp 对不上。解决先把 Library path 里 Indy 的三个源码目录顶到最前面再全盘搜Id*.dcu删掉最后 Project → Build All不是 CompileCompile 不会重新链接。如果还不行检查是不是同时装了 32 位和 64 位包而当前编译目标选错了平台。5.2 装完设计期包IDE 启动报「Cannot load package dclIndyCore」现象包在 IDE 里点 Install 成功了关掉重开 Delphi弹窗说某个 Indy 包加载失败工具面板上组件全没了。原因设计期包的加载顺序或者依赖没满足。Indy 的设计期包依赖对应的运行期 bpl如果运行期 bpl 没在系统能找到的路径里通常是 IDE 的 bin 目录加载时解析不到依赖就失败。另一种情况是 32 位和 64 位混装把 Win64 的 bpl 塞进了 32 位 IDE。解决先备份%BDS%\bin下所有 Indy 开头的 bpl再把编译出来的三个运行期 bpl 复制进去设计期包只放 Win32 版本。然后用%BDS%\bin\bds.exe -rXXX起一个干净注册表配置验证确认是包的问题还是注册表残留。这一步别嫌麻烦直接在主配置上反复试很容易把 IDE 搞到起不来。5.3 服务端 OnExecute 里更新界面跑一会儿就崩现象单客户端测试完全正常压到几十个连接后随机访问违例崩的位置在 VCL 内部堆栈看不出和网络有关。原因OnExecute 在连接上下文线程里执行直接写 Label.Caption 或者 Memo.Lines 属于跨线程操作 VCL。单连接时主线程恰好空闲看起来没事并发一上来就撞车。解决所有界面更新走 TThread.Queue参数在进入 Queue 之前拷贝好闭包里捕获的是变量引用循环里尤其容易出错。如果只是记日志干脆别碰界面写到一个线程安全的队列里主线程用 Timer 批量刷。5.4 ReadLn 一直阻塞到超时对端明明发了数据现象服务端 ReadLn 每次都在 5 秒后抛 EIdReadTimeout抓包看数据确实到了。原因终止符不匹配。对端发的是 CRLF你按 LF 读读到的内容里带着 #13长度也偏一位或者对端压根没发换行只是发了一段定长报文。解决先抓一次原始字节确认结束符再决定用 ReadLn(LF) 还是 ReadLn(#13#10)。定长报文改用ReadBytes(N)。还有一种隐蔽情况是缓冲区里残留了上一次的数据导致下一次读到的「第一行」其实是上次的尾巴这种要在协议层加序号或者长度头来兜底。5.5 HTTPS 在本机能跑到客户机器上报 SSL 加载失败现象开发机一切正常部署到现场机器上所有 HTTPS 请求全部失败错误是 Could not load SSL library。原因DLL 没跟着发布或者发布的是位数不对的版本。开发机上系统目录里恰好有运行库所以「看起来能跑」。解决把两个 DLL 和 exe 放同一目录一起打包用WhichFailedToLoad打印实际尝试的文件名做交叉验证。部署脚本里加一条校验文件缺失就直接在启动时报错别等到第一次发请求才暴露。6. 进阶心跳、重连与线程池把 Indy 用成能上线的样子装好、跑通只是及格线。真正上线要解决的是「断了怎么办」和「连太多怎么办」。我的习惯是把 TCP 客户端封在一个常驻线程里用短超时做心跳探测断线走指数退避重连而不是在按钮点击里裸调 Connect。type TTcpWorker class(TThread) private FHost: string; FPort: Integer; protected procedure Execute; override; public constructor Create(const AHost: string; APort: Integer); end; procedure TTcpWorker.Execute; var Client: TIdTCPClient; Backoff: Integer; begin Backoff : 1000; // 首次重连间隔 1 秒 while not Terminated do begin Client : TIdTCPClient.Create(nil); try Client.Host : FHost; Client.Port : FPort; Client.ConnectTimeout : 3000; Client.ReadTimeout : 8000; try Client.Connect; Backoff : 1000; // 连上就重置退避 while (not Terminated) and Client.Connected do begin Client.IOHandler.WriteLn(PING, IndyTextEncoding_UTF8); if Client.IOHandler.ReadLn(LF, 8000, 256, IndyTextEncoding_UTF8) PONG then Break; // 应答不对主动断开重连 Sleep(15000); // 心跳间隔 end; except on E: EIdException do ; // 断线统一落到重连逻辑 end; finally Client.Free; end; if Terminated then Break; Sleep(Backoff); Backoff : Min(Backoff * 2, 30000); // 指数退避封顶 30 秒 end; end;这段骨架的关键在三个数字心跳间隔 15 秒、读超时 8 秒、退避封顶 30 秒。读超时要小于心跳间隔否则上一次读还没返回下一次写就排队了退避要封顶不然服务端故障恢复之后客户端还在一分钟一次的节奏上慢慢爬。Terminated在每个可能长阻塞的位置都检查一次是因为 Sleep 和 ReadLn 都不会响应线程终止信号不加检查就会出现「关了程序进程还在」的情况。服务端侧如果连接数上到几百默认的「一连接一线程」会开始吃内存这时候换 TIdSchedulerOfThreadPool把IdTCPServer1.Scheduler指向一个线程池实例配好MaxThreads和MinThreads空闲线程会被复用上下文切换成本明显下降。配置项单机工具建议值对外服务建议值MaxConnections50500 ~ 2000ListenQueue20100TerminateWaitTime300010000Scheduler默认每连接一线程TIdSchedulerOfThreadPoolMaxThreads不适用CPU 核数 × 4有一件事我一直保持任何走 Indy 的对外接口上线前必须做一次「拔网线」测试——物理断网、服务端假死、DNS 解析失败三种场景各跑一遍看客户端会不会卡死、会不会无限重连打满 CPU、日志里能不能看出是哪一层超时。这套检查我吃过亏早期一个版本只测了正常路径现场网络抖动时客户端卡在 ReadLn 里整整一个下午界面完全无响应。从那以后我每次封网络组件都强制走一遍断网、假死、拒绝连接三连测才算完。希望帮到你。本文还有配套的精品资源点击获取