简介:Indy 10.6.3.3 是面向 Delphi 与 C++Builder 开发者的跨平台网络组件库源码包,适合需要快速实现 TCP/IP、HTTP、FTP 等通信功能的中高级开发者。它把多种互联网协议封装成可直接拖放的组件,包含连接管理与网页请求等常用部件,并支持异步执行和事件回调,可用于邮件客户端、Web 服务、文件传输与聊天程序等场景。压缩包共 2000 个文件,约 9.67MB,以 pas 源码、dpk 包文件、dproj 工程、bmp 与 ico 资源,以及 txt、rc、res 等说明和构建文件为主,目录兼顾编译安装、示例参考与资源生成。包内保留核心协议单元、设计期包与多版本工程文件,便于在 RAD Studio 中按需编译和二次开发;完整源码也方便按项目需求裁剪、修改和扩展协议实现,并对照组件结构排查连接、收发与超时等问题。已有 362 人学习,适合作为 Delphi 网络应用开发的基础组件资料,对理解组件化网络编程与协议封装亦有参考价值。
1. 解压先别急着点 Install:Indy 10.6.3.3 换掉的是 Delphi 12 的整套网络底座
Delphi 控件里最容易被低估的一类,是那种平时看不见、一旦出问题就全盘停摆的底层库,Indy 就是典型。Indy(Internet 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 | 运行期 | IndySystem | IdTCPClient、IdTCPServer、IdIOHandler、IdContext、IdThread |
| IndyProtocols.dpk | 运行期 | IndyCore | IdHTTP、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:Config=Release /p:Platform=Win32 msbuild IndyCore.dproj /t:Build /p:Config=Release /p:Platform=Win32 msbuild IndyProtocols.dproj /t:Build /p:Config=Release /p:Platform=Win32这三条命令的逻辑是串行的:IndyCore 编译时需要读 IndySystem 产出的 .dcu 和 .bpl,所以上一条不成功就不要往下走。参数上,Config=Release是为了让设计期挂进 IDE 的那份包体积小、断言少,调试信息留给项目自己的 Debug 配置;Platform=Win32是因为 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 放在内层 finally,Free 放在外层 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」后面跟一个 #13,Trim之后才能比对上;一旦业务数据里本身包含 #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 设成 Http(TIdSSLIOHandlerSocketOpenSSL.Create(Http)),要么创建时 Owner 给 nil、但只 Free Http。两种都行,核心原则是同一个对象只能有一个释放者。我一般用前者,因为对象树清晰,出问题看 Owner 就知道谁管谁。
顺带一提,给Http.IOHandler赋值时,Indy 内部会把旧的默认 handler 释放掉,所以你不需要(也不应该)自己去释放它。
4.2 关键参数表
下面这张表是我在几个项目里反复调出来的常用值,具体数字按你的服务端 SLA 调整,但项目别留空。
| 参数 | 常用取值 | 说明 |
|---|---|---|
| ConnectTimeout | 3000 ~ 5000 | 建连阶段上限,含 DNS,单位毫秒 |
| ReadTimeout | 15000 ~ 30000 | 单次读阻塞上限,超时抛 EIdReadTimeout |
| HandleRedirects | True | 自动跟随 301/302;注意 POST 被重定向后可能退化成 GET |
| RedirectMaximum | 5 | 重定向次数上限,防止循环跳转 |
| Request.ContentType | application/json; charset=utf-8 | POST 时必须显式设置,否则服务端可能按表单解析 |
| Request.Accept | application/json | 影响服务端的内容协商 |
| Request.UserAgent | 自定义字符串 | 留空会被部分网关直接拒绝 |
| SSLOptions.SSLVersions | [sslvTLSv1_2, sslvTLSv1_3] | 不要留空,留空走库默认值,可能协商到不再被接受的旧协议 |
| SSLOptions.Mode | sslmClient | 客户端固定这个值 |
| SSLOptions.VerifyMode | [sslvrfPeer] | 空集合等于不校验服务端证书,生产环境不要这么干 |
| SSLOptions.VerifyDepth | 3 | 证书链校验深度 |
| SSLOptions.RootCertFile | CA 证书路径 | 留空则用系统证书存储 |
| IOHandler.Deflate.CompressionLevel | 0 或 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; charset=utf-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.dll(64 位版本带 -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 两套 DLL,Indy 会按内部顺序挑一个,可能出现「本机好好的、客户机器上握手失败」,统一成一套、删掉多余的,能省掉很多玄学问题。
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(不是 Compile,Compile 不会重新链接)。如果还不行,检查是不是同时装了 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,空闲线程会被复用,上下文切换成本明显下降。
| 配置项 | 单机工具建议值 | 对外服务建议值 |
|---|---|---|
| MaxConnections | 50 | 500 ~ 2000 |
| ListenQueue | 20 | 100 |
| TerminateWaitTime | 3000 | 10000 |
| Scheduler | 默认(每连接一线程) | TIdSchedulerOfThreadPool |
| MaxThreads | 不适用 | CPU 核数 × 4 |
有一件事我一直保持:任何走 Indy 的对外接口,上线前必须做一次「拔网线」测试——物理断网、服务端假死、DNS 解析失败,三种场景各跑一遍,看客户端会不会卡死、会不会无限重连打满 CPU、日志里能不能看出是哪一层超时。这套检查我吃过亏,早期一个版本只测了正常路径,现场网络抖动时客户端卡在 ReadLn 里整整一个下午,界面完全无响应。从那以后我每次封网络组件,都强制走一遍断网、假死、拒绝连接三连测,才算完。希望帮到你。
本文还有配套的精品资源,点击获取