
简介Indy 10.2.3是一套面向Delphi 7~2007开发者的网络通信组件库专为解决桌面与服务端应用中的TCP/IP、HTTP、FTP、SMTP/POP3等协议交互而设计。整个压缩包共含839个文件以354个Pascal源文件为核心搭配63个Delphi包工程dpk及资源脚本rc/res另有180个位图等界面资源整体仅4.98MB轻量且结构清晰。目前已有342人学习下载。借助这套组件开发者可直接拖拽TIdTCPServer、TIdHTTP等控件快速搭建自定义协议服务端、Web客户端、FTP上传下载及邮件收发功能内置SSL/TLS加密与多线程事件模型可显著降低网络编程的复杂度。同时包内附带的工程文件bdsproj和编译脚本便于在不同Delphi版本中一键编译安装对需要维护老旧IDE项目或从零实现网络功能的开发者来说是一份实用的基础工具包。 打从Delphi 7那会儿开始搞网络通讯的程序员对Indy这个名字就不会陌生。Indy 10.2.3就是Delphi 10.3 Rio自带的那个网络组件库版本也是很多老项目从XE系列往上迁的时候最先遇见的版本。这套东西用一句话概括纯Object Pascal写成的Internet网络组件集底层帮你封装好socket上层把TCP、UDP、HTTP、FTP、SMTP这些协议直接做成控件拖拽配置一下就能跑通讯。我前阵子把一个维护了五六年的工控上位机通讯服务整套迁移到10.3上通讯模块全程依赖Indy过程中踩了不少坑也把10.2.3的脾气摸了个七七八八。这篇东西就写给那些正在用、或者准备用Indy 10.2.3写网络服务的Delphi开发者尤其是要处理socket长连接、HTTP接口对接和SSL/TLS加密通讯的朋友。1. Indy 10.2.3到底是什么为什么老Delphi程序员绕不开它1.1 一套组件打天下的网络库Delphi里做网络通讯你其实有好几条路可以走直接调用WinSock API用自带TClientSocket/TServerSocket或者上第三方的网络库。但我个人最习惯的还是Indy因为它的覆盖面实在太全了。你日常能想到的网络场景它几乎都有现成的组件TCP通讯有TIdTCPClient和TIdTCPServerUDP有TIdUDPClient和TIdUDPServerHTTP客户端有TIdHTTP服务端有TIdHTTPServer再加上TIdFTP、TIdSMTP、TIdPOP3这些应用层协议组件。写工控设备的Modbus TCP上位机、对接第三方平台的RESTful接口、做邮件告警推送全部能在同一套框架里完成不需要来回切换底层API。Indy的设计思路跟现在很多人熟悉的异步框架不一样。它默认走的是阻塞式socket模型也就是所谓的工作线程模式。你的网络请求会在单独的线程里执行收到数据才返回没收到就一直等。这套模型写起来特别直观发什么、收什么、什么时候超时逻辑一目了然不用像事件驱动模型那样到处挂回调。对于习惯了顺序写业务逻辑的Delphi老开发来说这个思维模式非常顺手。1.2 10.2.3这个版本的身份关于Indy的版本号很多人容易记混。你现在打开Delphi 10.3 Rio组件面板里的Indy版本就是10.2.3。这个版本内部有个很明显的特征它对应的卸载包名称还带“10”字头很多官方Demo的单元文件名也保留了旧命名习惯。从功能上看10.2.3相比更早的XE系列版本在网络协议解析上有不少修正尤其是HTTP的头部解析、Cookie处理、重定向跳转这些细节比老版本老实了很多不会动不动就抛协议解析异常。另外它内部把IPv6的支持做得很完整TIdDNSResolver、TIdIPWatch这些组件的兼容性也改善了。但这里有个关键点要提前说清楚Indy 10.2.3是绑定OpenSSL 1.0.2时代的组件库。它的SSL/TLS底层默认只认libeay32.dll和ssleay32.dll这组老命名文件。你如果拿OpenSSL 1.1.x那套新命名的libssl-1_1-x64.dll去配大概率会翻车。这一点后面我会单独展开讲因为这是我见过最多人栽跟头的坑。2. 环境准备与版本确认先把手里的牌摸清楚2.1 确认你用的Indy到底是哪个版本动手写代码之前先把当前IDE里实际编译的Indy版本确认清楚。这一步看着基础实际上能帮你省掉很多自以为“代码没写对”的排查时间。具体操作很简单在Delphi 10.3里新建一个工程放一个TIdTCPClient到窗体上选中组件后在对象查看器里找Unit属性双击跳转到IdTCPClient单元源码看文件头的版本信息。或者在代码里用IdVersion这个全局常量直接ShowMessage一下也行。如果发现IDE自带的Indy版本不是你想要的比如你装了第三方库把组件面板污染了优先在Component Install Packages里把多余的运行期包移除恢复默认状态。老实说用官方自带版本是最省心的因为10.2.3跟10.3的RTL配合本来就很稳没必要为了追新去强上10.6.x的独立版那会引入OpenSSL版本冲突和包依赖问题。2.2 OpenSSL DLL十有八九的SSL报错都出在这Indy的TIdSSLIOHandlerSocketOpenSSL组件本质上只是一个外壳真正的加密逻辑全在OpenSSL的DLL里。Indy 10.2.3时代的代码只认两个文件libeay32.dll和ssleay32.dll。你在做任何HTTPS请求或者SSL加密的TCP通讯之前必须先把这两个文件放到程序的运行目录或者放到系统PATH能搜索到的地方。注意千万别把OpenSSL 1.1.x的libssl-1_1-x64.dll、libcrypto-1_1-x64.dll改个名字冒充老文件塞进去。Indy 10.2.3在启动SSL时会做内部版本探测随便改名字会触发“Could not load SSL library”或者“Invalid SSL library”之类的报错。我自己的做法是固定用一个版本OpenSSL 1.0.2u。这个版本是1.0.2系列最后的维护版本稳定性经过了多年验证配合Indy 10.2.3跑HTTPS和TLS 1.2都没问题。下载解压后把libeay32.dll和ssleay32.dll复制到exe同目录。如果是32位程序就放32位DLL64位程序放64位DLL。混用会直接崩溃这个没有例外。3. 核心组件实操从TCP到HTTP的落地代码3.1 TIdTCPClient与TIdTCPServer自定义协议通讯的最佳拍档工控行业最常见的场景就是设备端通过TCP长连接上报数据上位机做服务端接收。Indy里的TIdTCPServer用起来非常直接。你只要关心OnExecute事件这个事件会在每个客户端连接对应的独立线程中触发收到数据就解析解析完就应答。procedure TForm1.IdTCPServer1Execute(AContext: TIdContext); var buff: TIdBytes; cmd: string; len: Integer; begin // 从连接中读取数据最多等5000毫秒 if not AContext.Connection.IOHandler.Readable(5000) then begin AContext.Connection.IOHandler.WriteLn(timeout); Exit; end; // 按行读取文本命令 cmd : AContext.Connection.IOHandler.ReadLn; if cmd PING then AContext.Connection.IOHandler.WriteLn(PONG) else if cmd TIME then AContext.Connection.IOHandler.WriteLn(FormatDateTime(yyyy-mm-dd hh:nn:ss, Now)) else AContext.Connection.IOHandler.WriteLn(UNKNOWN); end;这段代码有几个值得注意的点。Readable(5000)是用来做超时预检测的避免直接ReadLn把线程卡死。在长连接场景里如果客户端异常断开服务器的OnExecute里读操作会抛异常你需要在事件里套try except在异常分支里主动调AContext.Connection.Disconnect把这条坏连接清掉。如果不这么做连接对象会挂在连接列表里不释放时间一长服务器连接数就满了新客户端连不进来。这是我维护过程中印象最深的一个坑表面现象是“服务器运行三天后连不上了”实际就是连接泄漏。客户端这边相对更简单。TIdTCPClient负责建连和收发with IdTCPClient1 do begin Host : 192.168.1.100; Port : 9100; ConnectTimeout : 3000; ReadTimeout : 5000; Connect; try IOHandler.WriteLn(PING); lblStatus.Caption : IOHandler.ReadLn; finally Disconnect; end; end;设置ConnectTimeout非常关键不设的话遇到不可达IP程序会卡在Connect那里很久。Indy 10.2.3的ConnectTimeout单位是毫秒底层走的是非阻塞处理加超时判断实测值比较准确。3.2 TIdHTTP接口对接的几个实用细节现在写上位机几乎绕不开对接HTTP接口不管是WebAPI还是内部服务。TIdHTTP用起来和TNetHTTPClient类似但10.2.3版本的TIdHTTP对HTTP/1.1协议的处理更成熟一些。一个标准的带SSL的GET请求是这样的var lHttp: TIdHTTP; lSSL: TIdSSLIOHandlerSocketOpenSSL; content: string; begin lHttp : TIdHTTP.Create(nil); lSSL : TIdSSLIOHandlerSocketOpenSSL.Create(nil); try lSSL.SSLOptions.Method : sslvTLSv1_2; // 强制TLS 1.2 lSSL.SSLOptions.Mode : sslmClient; lHttp.IOHandler : lSSL; lHttp.ConnectTimeout : 5000; lHttp.ReadTimeout : 10000; lHttp.HandleRedirects : True; // 自动跟随302跳转 lHttp.Request.UserAgent : Indy/10.2.3; content : lHttp.Get(https://api.example.com/status); Memo1.Lines.Add(content); finally lHttp.Free; lSSL.Free; end; end;很多人问为什么IOHandler要先创建再赋值直接在设计期把SSL组件拖上去不行吗行但运行时动态创建的好处是方便切换同一个TIdHTTP实例我可以先赋值nil走普通HTTP再赋值SSL走HTTPS不用为每种协议单独维护一个控件。此外注意TIdHTTP在10.2.3版本里如果你不用IOHandler去访问HTTPS会直接报“IOHandler value is not valid”这是正常的说明你必须先指定SSL组件。POST请求要提交JSON的时候记得设置Request.ContentType和Request.Charset否则服务器那边解析请求体会当成普通表单取不到JSON字段lHttp.Request.ContentType : application/json; lHttp.Request.Charset : utf-8; lHttp.Request.Accept : application/json; content : lHttp.Post(https://api.example.com/submit, TStringStream.Create({temp:23.5,id:dev01}));这里有个经验TStringStream用完后记得Free或者用TStringStream.Create后传入的参数交给TIdHTTP时它不会自动释放你需要hold住变量再释放。另外Indy的字符编码处理有时会让人摸不着头脑我会在后面的问题章节专门说。4. 并发场景里的正确姿势多客户端与多线程4.1 TIdTCPServer天生就是多线程的TIdTCPServer的每一个客户端连接默认是在独立的线程上下文里运行的。这意味着你在OnExecute里写的代码天然就处于多线程环境。因此只要在事件里访问了窗体上的VCL控件比如Memo、Label就必须用Synchronize或者TThread.Queue包一层否则界面上会随机崩溃而且不是你一测就能复现的那种崩溃是用户现场跑几天才出现的隐性崩溃。标准的写法是这样procedure TForm1.IdTCPServer1Execute(AContext: TIdContext); var data: string; begin data : AContext.Connection.IOHandler.ReadLn; TThread.Queue(nil, procedure begin Memo1.Lines.Add(收到: data); end); end;用TThread.Queue而不是Synchronize是因为Queue不会阻塞当前工作线程在网络线程里读数据时性能更好。还有个很容易忽略的细节TIdTCPServer.OnConnect和OnDisconnect这两个事件也是在独立线程里触发的同样不能直接操作VCL控件别以为只有OnExecute才需要小心。4.2 客户端多线程千万不要共享组件实例如果你要在主线程之外发起HTTP请求或者TCP通讯有个铁律每个线程必须创建独立的TIdHTTP或者TIdTCPClient实例用完了释放。Indy的客户端组件不是线程安全的同一个组件实例在多个线程里同时调用Connect和Get会出现访问冲突或者数据错乱。这个坑我早年踩过做一个多路数据采集器每路一个线程图省事共用了一个全局TIdTCPClient结果运行不到半小时直接AV。多线程客户端的正确示例是每次使用方法时内部即时创建procedure SendRequestInThread(AUrl: string); var lHttp: TIdHTTP; lSSL: TIdSSLIOHandlerSocketOpenSSL; begin lHttp : TIdHTTP.Create(nil); lSSL : TIdSSLIOHandlerSocketOpenSSL.Create(nil); try lSSL.SSLOptions.Method : sslvTLSv1_2; lSSL.SSLOptions.Mode : sslmClient; lHttp.IOHandler : lSSL; lHttp.ConnectTimeout : 5000; lHttp.ReadTimeout : 10000; lHttp.Get(AUrl); finally lHttp.Free; lSSL.Free; end; end;别觉得每次创建释放创建释放浪费性能实际上对现代机器来说这开销完全可以忽略但换来的隔离性和稳定性是实打实的。5. 常见问题与排查技巧实录5.1 “Could not load SSL library”的终极解法这个报错我见过太多次了几乎成了Indy SSL问题的代名词。它分好几种情况。第一种是DLL压根没放解决方法就是去下载OpenSSL 1.0.2u把libeay32.dll和ssleay32.dll放到运行目录。第二种是DLL放了但是位数不对程序是32位你放了64位DLL同样报这个错。第三种比较隐晦你用了OpenSSL 1.1.x的DLL改名成老名字Indy内部调用初始化函数时发现结构体对不上报出来的错误还是这个。所以排查它就一条路确认版本、确认位数、确认文件名。全都对了基本就不会再报。5.2 连接超时与“假死”问题用TIdTCPClient访问不可达IP时如果没设ConnectTimeout或者设得很大程序界面可能直接“假死”。这是因为你如果是在主线程里调用Connect阻塞socket会让整个界面线程卡住。解决办法有两个一是设置合理的ConnectTimeout和ReadTimeout二是把网络操作放到独立线程里去跑配合线程里的WaitFor这样就算超时也不会拖死界面。我个人的习惯是双管齐下既设了超时也用线程包了一层双击按钮后至少界面永远能响应。5.3 中文乱码字符串编码迷宫Indy处理字符串编码有时确实让人头大。默认情况下TIdHTTP.Get返回的字符串按照ASCII解析遇到UTF-8的中文内容会显示成乱码。通用的处理方法是拿到Bytes后再手动按正确的编码转换var resp: TIdBytes; content: string; begin resp : lHttp.Get(AUrl); // 用重载版本拿字节数据 content : TEncoding.UTF8.GetString(resp); // 按UTF-8转字符串 end;如果服务器返回的是GBK就把TEncoding.UTF8换成TEncoding.GetEncoding(936)。实际对接国内设备时很多老设备的Web接口返回的都是GBK这个细节处理不好对接调试能卡你半天。5.4 心跳、粘包与半包处理自己定义TCP应用层协议时读写双方的报文边界必须明确。Indy的ReadLn默认按换行符分帧适合简单ASCII协议。但如果是二进制帧我建议在报文头里带上长度字段先读固定长度头部解析出体长度再读指定长度的Body。我常用的封装思路是这样帧头4字节存整个帧长度读取时用ReadBytes(HeadSize)读长度再用ReadBytes(BodySize)读内容两个ReadBytes之间的数据在Indy内部是严格按字节流顺序来的不用自己拼包。这个方法在10.2.3里跑得很稳避免了粘包和半包的问题。6. 写到最后的一些实战心得Indy 10.2.3不是最新版本但它依然是很多Delphi项目里网络通讯的基石。我在用它迁移老项目的过程中最深的体会是Indy的设计哲学很朴素就是让你在阻塞模型里用最直白的方式把网络逻辑写出来你需要做的只是给它配好环境、规范地管理连接生命周期、多线程下别偷懒。如果你手头也正维护着老代码我建议你先把当前Indy版本确认清楚把OpenSSL的DLL版本锁死并固定存放然后给每个网络请求都设置明确的超时时间。就这三件事能帮你拦下绝大部分的网络模块线上故障。遇到SSL初始化报错、连接泄漏、中文乱码别慌这类问题都是老问题网上检索到的答案大概率是基于某个特定DLL版本的经验先把环境对齐了再谈代码优化。另外一个小技巧开发阶段把TIdLogDebug或者TIdLogFile组件挂到TIdTCPClient上把收发数据流完整记录下来很多时候比断点调试高效得多。既然是老伙计就多给点耐心它不会让你失望。本文还有配套的精品资源点击获取