简介:Indy 10.2.3 是 Delphi 开发环境中常用的网络通信组件库,主要面向 Delphi 2007 至 2010 版本的桌面与网络应用开发者,帮助快速实现 TCP/IP、HTTP、FTP、SMTP、POP3 等协议通信,降低底层 Socket 开发门槛。压缩包共包含 2000 个文件,约 6.39MB,以 pas 源代码、txt 说明与变更日志、bmp/gif 图标、dpk 工程文件、rc 资源脚本为主,并包含 NUnit 单元测试、构建批处理、DLL、可执行程序及多种配置文件,既适合直接安装集成,也适合通过源码学习组件内部机制。包内变更日志清晰记录了相对早期版本在性能优化、错误修复、SSL/TLS 新版本支持及异步处理等方面的改进;Test 与 NUnit 目录提供测试用例,Lib、Builder、icons、KB 等目录分别对应库文件、构建工具、界面图标与常见问题知识库,可辅助完成编译部署和问题排查。目前已有 170 人学习浏览,对于需要兼容旧版 Delphi 并快速搭建网络功能的开发者来说,这份资源兼具参考价值与实用价值。
1. indy10.2.3:老 Delphi 项目里最容易被甩锅、又最不敢动的网络层
某个维护了三年的 Delphi 服务端项目,最近一次组件升级后,客户端连接频繁超时、内存稳定上涨,查了两天没有头绪。最后把网络组件回退到 indy10.2.3,重启后一切恢复平静。这不是巧合,而是 Indy 版本行为差异导致的典型问题。indy10.2.3 是 Indy 10 系列里一个偏早期但非常稳定的版本,很多旧工程的后端服务、客户端通信、HTTP 上报都锁在这个版本上。它的定位很简单:不追新特性,只求 TCP 收发行为可预期、依赖干净、升级成本可控。这篇文章写给两类人:一类是被老项目绑在 indy10.2.3 上、想弄清楚它到底能干什么的维护者;另一类是想从高版本降下来、又担心踩坑的开发者。后面所有内容都围绕一件事:怎么把 indy10.2.3 跑稳、跑明白。
2. 先搞清楚 indy10.2.3 是什么:版本定位与依赖锁定
2.1 10.2.3 在 Indy 10 系列里的位置
Indy 10 从早期版本一路迭代到 10.6.x,中间经历过多次内部接口调整。10.2.3 属于 10.2 这条分支的后期修订版,它的特点是:TIdTCPServer、TIdTCPClient、TIdHTTP 这些核心组件的基本模型已经定型,和后续版本差别不大;但它在 SSL 处理、默认编码、异常类型上保留了更老的行为习惯。比如 10.2.3 里 OpenSSL 组件默认找的是 libeay32.dll 和 ssleay32.dll 这两个旧命名文件,而后面的版本已经切换到 libcrypto 和 libssl 命名。这个差异对只看代码不看底层 DLL 的人来说,就是典型的黑匣子问题:代码没变,换了版本就崩。
另一个容易被忽略的点是包结构。10.2.3 把核心运行包和设计时包分开,核心包不依赖 IDE 运行,设计时包才负责组件面板显示。如果你只关心服务端部署,可以只编译核心包,不装设计时包;如果你需要在 IDE 里拖动组件,才需要编译并安装 dclIndy 系列包。这一点在升级时特别重要,因为很多安装教程把两步混在一起,导致运行时缺包或者 IDE 直接起不来。
从选型的角度看,10.2.3 并不是最先进的版本,但它有一个不可替代的优势:行为可预期。很多老项目针对它的超时机制、编码默认值、异常类型做过大量适配,代码里可能到处都是针对 Indy 10.2.3 的 workaround。升级换版本意味着所有 workaround 都要重新验证,这个成本往往比换掉整个网络层还高。
2.2 为什么旧的 10.2.3 反而更稳:编码、超时与 DLL 行为差异
升级到新版本后最常见的三个差异点,恰好对应 Indy 最容易出问题的三个维度。
第一是默认编码。10.2.3 里 TIdIOHandler 读字符串时,默认使用 IndyTextEncoding_Default,也就是跟随系统区域设置的 ANSI 编码。而新版本在多数情况下更倾向于 UTF-8。如果你的服务端和客户端约好的是 GBK 或 ANSI,升级后就可能出现乱码。这种现象在日志类服务里非常常见,表面上看是新版本有 bug,实际上只是默认编码变了。
第二是超时语义。10.2.3 的 ConnectTimeout 和 ReadTimeout 单位是毫秒,并且 ReadTimeout 绑定在 IOHandler 上。后续版本对超时的判定逻辑做过调整,比如对空闲连接的处理更激进。一个在 10.2.3 下能稳定保持一周的长连接,在新版本下可能被超时机制主动断开。对于心跳间隔比较久的连接,这种差异是致命的。
第三是 OpenSSL DLL 的加载行为。10.2.3 只认旧的 DLL 文件名,顺序也有讲究:先找程序目录,再找系统目录。新版本则要求较新的 OpenSSL 版本,并且文件名不同。老项目里如果直接拷贝 DLL 到 exe 目录就能用,升级后同样的操作会失效。
这三个差异汇总成一张对比表:
| 维度 | indy10.2.3 行为 | 后续版本常见行为 | 影响 |
|---|---|---|---|
| 默认字符串编码 | 跟随系统 ANSI 区域设置 | 更倾向 UTF-8 | 中文乱码、日志乱码 |
| 超时单位与触发逻辑 | 毫秒,按 IOHandler 判定 | 多次调整判定逻辑 | 长连接被无故断开 |
| OpenSSL DLL 文件名 | libeay32.dll / ssleay32.dll | libcrypto / libssl | SSL 握手失败 |
| 设计时包依赖 | 核心包与设计时包分离 | 延续分离但包名变化 | 安装路径易混乱 |
所以,如果你接手的是一个基于 10.2.3 的老项目,别急着升级。先评估这三个维度在代码里是否被依赖,再决定要不要动。很多时候不动,就是最稳的方案。
3. 把 10.2.3 装进现有 IDE:卸载、编译与路径设置
3.1 先确认现在装的是哪个版本
动手之前,先确认当前 IDE 里实际加载的 Indy 版本。这个步骤很多人跳过,结果编译了半天用的是旧包,问题依旧。最可靠的办法是写一行代码在运行时输出版本号:
uses IdGlobal; procedure TForm1.Button1Click(Sender: TObject); var V: string; begin V := IdVersion; ShowMessage(V); // 会输出类似 10.2.3 的版本字符串 end;逻辑说明:IdGlobal 单元里的 IdVersion 常量在编译期由 Indy 的头文件定义,直接引用即可拿到当前生效的版本号。注意,如果 IDE 里同时存在多个版本的 Indy 源码头文件,编译顺序决定了这个值取的是哪一份。
参数说明:如果输出结果是 10.1.x 或 10.2.0,说明当前工程实际链接的不是 10.2.3,后续操作要以这个输出为准,而不是看 IDE 组件面板里显示的包名。组件面板显示的是设计时包,运行时代码链接的是核心包,两者可能不一致。
3.2 卸载旧包、编译 10.2.3 源码的完整顺序
确认旧版本后,按顺序做以下操作。顺序不能乱,否则会出现设计时包指向新版本、运行时链接旧版本的错位问题。
第一步,打开 IDE 的 Component > Install Packages 列表,找到所有名称里带 Indy 的包,例如 dclIndyCore.bpl、dclIndyProtocols.bpl,点击 Remove 卸载。这一步只移除设计时注册,不影响已编译工程里的运行时代码。
第二步,关闭 IDE,删除临时目录里残留的.bpl和.dcp文件。常见位置是 IDE 的默认库输出目录,以及系统临时目录。残留文件是很多疑难问题的根源,版本混用往往就是这里产生的。
第三步,重新打开 IDE,在 Tools > Options > Delphi Options > Library > Library path 中,把 10.2.3 的源码目录加到最前面。注意要加的是包含IdTCPClient.pas、IdTCPServer.pas的根目录,而不是某个子目录。路径顺序决定编译器优先使用哪份源码。
第四步,打开源码目录里的包工程文件,例如IndyCore.dpk、IndyProtocols.dpk,先编译 IndyCore,再编译 IndyProtocols,最后编译对应的设计时包。编译过程中如果提示找不到某个单元,优先检查库路径是否把源码根目录放进去了。
第五步,重新打开组件面板,确认 TIdTCPClient、TIdTCPServer 等组件出现。之后回到工程里重新编译,再用上一节的 IdVersion 验证一次。
这个顺序的目的是保证:源码路径唯一、编译顺序正确、设计时包与核心包一致。任何一步错位,都会在高版本和低版本之间来回横跳。
3.3 编译参数、库路径与条件编译
在实际工程里,除了 IDE 库路径,还要检查工程的搜索路径。有些工程在 project 文件里直接写死了旧版本的绝对路径,这种情况下就算 IDE 库里加了新路径,编译器仍然优先使用工程里写死的路径。
// 在工程源文件里加条件编译,避免头文件版本不一致 {$IFDEF INDY10} uses IdGlobal, IdTCPClient, IdTCPServer, IdHTTP; {$ENDIF}逻辑说明:使用条件编译把 Indy 相关单元包裹起来,可以防止在非 Indy 环境下编译时报错。INDY10 这个符号需要在 Project > Options > Delphi Compiler > Conditional defines 里手动添加。
参数说明:库路径优先级从高到低依次是:工程搜索路径、IDE 库路径、系统默认路径。如果你在 IDE 里改了路径但没有生效,优先检查工程文件里是否有额外的 Search path。另外,10.2.3 里 OpenSSL 相关单元需要确定是否启用了USE_OPENSSL条件编译,如果启用了,运行时就必须提供对应的 DLL,否则 SSL 组件在实例化时会报错。
编译参数上,建议关闭运行时主题相关影响,但这不是必须;真正必须的是把Assertions保持开启,这样 Indy 内部不少断言能在开发阶段暴露出问题,发布时再关闭。这个习惯能省掉很多排查时间,属于典型的血泪经验。
4. 用 indy10.2.3 把 TCP 收发跑起来:三个高频组件的参数
4.1 服务端最小可运行示例:TIdTCPServer
TIdTCPServer 是 Indy 10.2.3 里最常用的服务端组件。它的工作模型是:监听指定端口,每个连接进入后在独立线程里执行 OnExecute 事件。在 10.2.3 里,这个模型的线程模型已经非常成熟,不需要额外创建线程。
procedure TForm1.FormCreate(Sender: TObject); begin IdTCPServer1.DefaultPort := 9000; IdTCPServer1.Active := True; end; procedure TForm1.IdTCPServer1Execute(AContext: TIdContext); var s: string; begin // ReadLn 默认按行读取,行结束符是 CRLF s := AContext.Connection.IOHandler.ReadLn; // 原样回写,并在末尾补上行结束符 AContext.Connection.IOHandler.Write('echo: ' + s + #13#10); end;逻辑说明:AContext 是当前连接的上下文,每个连接都有独立的 IOHandler。ReadLn 是阻塞读取,会一直等到收到行结束符或者连接断开。Write 写出的内容不会自动追加行结束符,所以要在字符串里手动加#13#10。这个细节经常导致客户端等不到回包,是典型的“服务端没返回”假象。
参数说明:DefaultPort 是监听端口;Active 置 True 表示启动监听。正式项目里建议把 Active 放到 FormShow 或单独的开始按钮里,不要在 FormCreate 里直接启动,因为窗口还没准备好时,异常弹窗很难看。MaxConnections 默认是 0,表示不限制;如果需要限流,设置成具体数字,超过后新连接会被拒绝。ListenQueue 是内核监听队列长度,高并发场景建议适当加大,比如 100 到 200。
在 10.2.3 里,OnExecute 里如果发生未捕获异常,会导致当前连接被断开,但服务端监听不受影响。如果希望异常时不断开连接,需要在 OnExecute 内部自己包一层 try/except。这个是 10.2.3 和后续版本行为差异比较大的地方,后续版本对异常的处理更温和。
4.2 客户端连接参数:TIdTCPClient 的握手与超时
客户端的核心参数是 Host、Port、ConnectTimeout、ReadTimeout。在 10.2.3 里,这些参数单位都是毫秒,ConnectTimeout 在 Connect 调用时生效,ReadTimeout 绑定在 IOHandler 上。
IdTCPClient1.Host := '127.0.0.1'; IdTCPClient1.Port := 9000; // 建立 TCP 连接的超时时间 IdTCPClient1.ConnectTimeout := 3000; // 读取响应的超时时间 IdTCPClient1.ReadTimeout := 5000; IdTCPClient1.Connect; try // 发送一行数据 IdTCPClient1.IOHandler.Write('ping' + #13#10); // 读取服务端回包 Result := IdTCPClient1.IOHandler.ReadLn; finally IdTCPClient1.Disconnect; end;逻辑说明:ConnectTimeout 控制的是 TCP 三次握手阶段;ReadTimeout 控制的是读操作阶段。ReadTimeout 需要在 Connect 之后设置,或者在连接前设置但确保 IOHandler 已经创建。在 10.2.3 里,IOHandler 默认是 TIdIOHandlerStack,它在第一次 Connect 时自动创建。
参数说明:ReadTimeout 赋值为 0 表示无限等待,可能导致线程一直挂住,不建议在生产环境使用。如果服务端是长连接且不定期推送数据,ReadTimeout 应该设成一个较大的值,比如 30000,而不是无限。另外,Write 调用后要立即 ReadLn,中间不要做耗时操作,否则服务端可能已经返回数据,而客户端还在等 IOHandler 里的缓冲区凑满,造成假死。
10.2.3 里还有一个容易被忽略的参数:BoundPort。如果需要客户端固定使用某个端口连接,可以设置它,但默认值为 0,表示由系统随机分配。这个参数在防火墙严格的环境里很有用,但设置后可能因端口被占用而连接失败,需要配合异常处理。
4.3 TIdHTTP 里的编码、代理与超时
TIdHTTP 在 10.2.3 里是一个很完整的 HTTP 客户端,但它对服务端返回的编码判断有一些历史包袱。默认情况下,它根据 HTTP 响应头里的 Content-Type 的 charset 参数判断编码;如果没有 charset,就按 ISO-8859-1 处理。对于返回中文但没有声明 charset 的接口,就会得到乱码。
var Response: string; begin IdHTTP1.ReadTimeout := 5000; IdHTTP1.ConnectTimeout := 5000; // 手动指定响应编码,绕开默认 ISO-8859-1 的坑 IdHTTP1.Response.ContentType := 'text/html; charset=utf-8'; Response := IdHTTP1.Get('http://example.com/api'); end;逻辑说明:在发出 Get 请求前手动设置 Response.ContentType 的 charset,可以强制 TIdHTTP 按 UTF-8 解码响应体。这只对本次请求生效,下次请求前需要重新设置,否则可能沿用上一次的编码设置。
参数说明:ConnectTimeout 在 10.2.3 的 TIdHTTP 里控制的是 TCP 连接阶段;ReadTimeout 控制的是响应读取阶段。如果请求整个过程都很慢,通常瓶颈在网络读取而不是连接。另外,TIdHTTP 的 ProxyParams 属性组里可以配置代理,在需要走代理的环境里,需要设置 ProxyParams.ProxyServer 和 ProxyParams.ProxyPort。如果不需要代理,确保 ProxyParams.ProxyPort 设置为 0,否则可能出现“一连就挂”的玄学问题。
5. indy10.2.3 避坑指南:五个典型的翻车现场与排查顺序
5.1 服务端收到一串乱码:UTF-8 被当成 ANSI 读
现象:客户端发过来的中文,服务端 ReadLn 之后变成乱码。
原因:客户端用 UTF-8 发送,服务端 10.2.3 默认用系统 ANSI 编码读取,两边字符集不一致。
解决:在连接建立后,强制设置 IOHandler 的编码。
AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;逻辑说明:DefStringEncoding 决定 ReadLn、Write 等字符串操作使用的编码。两端都设置成 IndyTextEncoding_UTF8 后,乱码问题消失。注意 10.2.3 里这个属性在 IOHandler 上是直接可见的,不需要额外引用单元。
5.2 SSL 握手失败:OpenSSL DLL 版本与文件名不匹配
现象:使用 TIdSSLIOHandlerSocketOpenSSL 的客户端连不上 10.2.3 的服务端,报错提示 SSL 库加载失败。
原因:10.2.3 的 SSL 组件固定加载 libeay32.dll 和 ssleay32.dll,而系统里只有 libcrypto/libssl。
解决:找到配套的旧版本 DLL 放到 exe 目录,确保文件名与 10.2.3 预期一致。如果代码里有{$IFDEF USE_OPENSSL},确认编译时已经启用。压测时注意,DLL 放在系统目录也能加载,但优先级低于 exe 目录,混放可能导致加载到错误版本。
5.3 超时设置无效:RTTI 看不见的默认值
现象:给 ReadTimeout 赋值 5000,但服务端不返回时,客户端仍然卡住十几秒才报错。
原因:在 10.2.3 里,ReadTimeout 对 TIdTCPClient 直接生效,但如果连接已经建立,IOHandler 内部有自己维护的超时状态,重新赋值后不会立即重置当前读操作的超时判定。
解决:不要在连接中途动态修改 ReadTimeout,而是在 Connect 前一次性设置。如果必须中途修改,先调用 IOHandler.ReadTimeout 赋值,然后确保没有其他线程在同一个连接上同时做读操作。实践中也看到过多个线程共用一个客户端导致超时失灵的情况,不建议那样用。
5.4 服务端启动就报端口占用:TIME_WAIT 造成的假象
现象:服务端 Close 后立刻重启,报 10048 地址被占用。
原因:TCP 连接关闭后端口进入 TIME_WAIT 状态,默认持续约 2 到 4 分钟。
解决:设置 ReuseSocket 属性为 True。10.2.3 的 TIdTCPServer 上,设置ReuseSocket := rsTrue可以让服务端在端口处于 TIME_WAIT 时也能重新绑定。注意这个属性要在 Active 置 True 之前设置,否则不生效。
5.5 客户端连不上 IPv6 主机:默认 IP 版本是 IPv4
现象:给 Host 传了一个 IPv6 地址,Connect 报错。
原因:10.2.3 里 TIdTCPClient 的 IPVersion 默认是 ID_IPV4,不会自动切到 IPv6 解析。
解决:设置IPVersion := ID_IPV6。如果主机名本身兼容 IPv6,也可以使用 Host 方式让系统解析,但 10.2.3 在纯 IPv6 环境下手动指定更可靠。这个坑在局域网 IPv6 项目里特别明显,属于搜索词经常被问到的问题。
6. 用自测脚本验证 indy10.2.3 的连接行为:压测、抓包与固化参数
验证 10.2.3 的服务端稳不稳,不能只看能不能连通。我的习惯是写两个最小脚本:一个循环发数据,一个循环收数据,把超时时间调到比实际业务低,观察异常出现频率。
procedure TForm1.Timer1Timer(Sender: TObject); begin try IdTCPClient1.Connect; IdTCPClient1.IOHandler.Write('heartbeat' + #13#10); s := IdTCPClient1.IOHandler.ReadLn; // 如果回包内容不是预期,计数器加一 except Inc(FailCount); end; end;逻辑说明:用 Timer 每 5 秒触发一次连接和读写,模拟客户端心跳。FailCount 记录失败次数,连续跑 12 小时,数字稳定在 0 说明基础连接逻辑没大问题。
压测时看三个数值:连接数、内存、句柄数。在 Windows 任务管理器里看内存占用,处理器数影响不大;句柄数要关注,因为连接未释放时句柄数会持续上涨。10.2.3 服务端没有内置连接池,每个连接一个线程,连接数过千时线程切换开销明显,这时候应该检查代码里是否有连接未关闭的分支。如果句柄数持续上涨,多半是某条异常路径没有调用 Disconnect。
抓包观察也要成为习惯。连接建立后,按正常业务发一包数据,看 TCP 层面是否有重传;客户端 Close 后,观察服务端是否收到 FIN。如果 TCP 层没有异常,问题大概率在应用层编码或超时设置上。
最后说一个从项目里带出来的教训:老项目里别因为某个新特性就动 Indy 版本。把 10.2.3 的核心参数、DLL 依赖、编码行为写成一份环境说明,放在工程目录里,比任何口头沟通都有效。每次接手的人先读那份说明,再动代码,能少踩一大半坑。希望帮到你。
本文还有配套的精品资源,点击获取