
简介Delphi 12 配套的 Indy 10.6.3.3 网络组件库源码包为 Delphi 及 RAD Studio 开发者提供了一套跨协议的互联网通信方案直接回应 Delphi 环境下网络组件缺失的痛点。它覆盖 TCP/IP、UDP、HTTP、SMTP、POP3、FTP 等常见协议通过 TIdTCPClient、TIdHTTP 等组件开发者可将它们拖放至表单并配置属性快速构建邮件客户端、Web 服务器、FTP 客户端、聊天程序等应用同时支持异步网络操作便于在数据传输过程中处理进度与异常反馈适合中高级 Delphi 工程师进行网络模块开发与协议二次扩展。包内共 2000 个文件大小约 9.67MB主体为 509 个 txt 说明文档和 327 个 pas 源代码另有 dpk、dfm、bdsproj、dproj、rc、res、bmp 等分别用于组件包、窗体、工程配置、资源与图标目录结构完整便于直接引入 Delphi 12 工程或按需定制编译目前已有 358 人学习下载。相比零散教程这里提供的是完整可编译的开源组件本体既可直接安装使用也能结合项目需求修改协议实现细节或扩展新协议包内还保留了多种 Delphi 版本的构建脚本对于需要长期维护网络模块的开发者是一份实用且值得收藏的基础资源。1. Delphi 12 控件安装里最容易被忽略的一步Indy 10.6.3.3 源码包怎么落地当你在 Delphi 12 的组件面板里看到 Indy 一栏基本齐全时很容易跳过它直接开始写业务代码。但等到你真要拖一个 TIdHTTPServer 或者 TIdFTPClient 出来做文件传输编译期就会开始报找不到单元的错或者运行时 SSL 库加载失败。Indy 10.6.3.3 这个源码包解决的就是这类问题把协议实现更新到较新状态补全官方预装版本裁剪掉的部分同时保留完整可断点调试的运行时源码。适合的读者很明确用 Delphi 做桌面工具、内网服务端、数据采集和邮件处理的一线开发者尤其是被 IDE 自带的控件版本卡过一下的人。2. 安装前的准备先确定版本基线再规划目录与平台2.1 先看 Delphi 12 自带 Indy 的版本号在动源码包之前最重要的一件事是先确认 IDE 里已经存在的 Indy 是什么版本。否则你费劲装完搜索路径一乱IDE 优先加载了旧的 dcu等于白干。查看版本有一个非常直接的办法新建一个 VCL 应用在 FormShow 里弹出版本字符串uses IdGlobal; procedure TForm1.FormShow(Sender: TObject); begin Caption : Current Indy: IdVersion; end;IdVersion 是 IdGlobal 单元里定义的编译期常量形如Indy 10.6.2.0。通过窗体的标题栏就能看出当前库的大版本。如果你看到的是 10.5.x 或者 10.6.0说明还停留在一个比较旧的基线装 10.6.3.3 的价值很大。我这里给一个判断标准当你的工程要用到TIdHTTP的OnAuthentication事件或者要对接新版 TLS 1.3 服务端时旧版本容易在握手阶段直接失败升级到 10.6.3.x 之后明显改善。还有一个更细的验证方式在代码里打印 GStack 的属性。GStack 是 IdStack 单元暴露的全局对象封装了底层 socket 实现uses IdStack; procedure TForm1.ButtonStackClick(Sender: TObject); begin ShowMessage(GStack.Version); ShowMessage(IntToStr(GStack.WinsockMajorVersion) . IntToStr(GStack.WinsockMinorVersion)); end;参数说明WinsockMajorVersion 和 WinsockMinorVersion 反映的是操作系统底层的 WinSock 版本不是 Indy 自身版本。在 Windows 10 及更新的系统上通常显示 2.2。这个方法更多用于确认运行时栈没有被第三方库污染比如某些程序会用同名的旧 DLL 覆盖 ws2_32.dll这个检查能兜底。2.2 源码包结构System、Core、Protocols 三层与 Packages 目录Indy 10 的源码包从 10.6 开始目录组织相对固定。解压开你会看到三块核心目录Indy-10.6.3.3/ ├─ Lib/ │ ├─ System/ // IdGlobal、IdStack、IdThreadComponent 等基础运行时 │ ├─ Core/ // TIdTCPClient、TIdTCPServer、IOHandler 等核心 socket 组件 │ └─ Protocols/ // TIdHTTP、TIdFTP、TIdSMTP、TIdPOP3 等协议实现 ├─ Packages/ │ └─ Delphi12/ // 对应 RAD Studio 版本的包工程文件dpk └─ Output/ └─ DCU/ // 部分预编译 dcu可省去全量编译时间System、Core、Protocols 三层是 Indy 一直以来的组织方式。System 层是最底层的基础设施包括字节序处理、字符编码转换、socket 抽象Core 层在 System 之上定义连接、IOHandler、Interceptor 等核心运行时模型Protocols 层才真正实现 HTTP、FTP、SMTP 这些具体协议逻辑。这个分层对编译顺序有直接影响必须先把 System 编译出来再编译 Core最后才能编译 Protocols。如果是手工在 IDE 里逐个打开 dpk这个顺序不能乱否则会报“找不到 IdCore.dcu”一类错误。Packages 目录下通常按编译器版本分子文件夹。装到 Delphi 12 时找到对应字样的子目录里面会有三个运行时包和对应的设计时包。同步确认 Output 目录里有没有对应平台的 dcu 文件如果没有就自己编译。这里提醒一句Output 下的预编译 dcu 不总是跟上当前 RAD Studio 版本所以最稳妥的方法还是看 Packages 里的 dpk 是否配套。2.3 替换前要做的取舍共存还是整体替换Indy 10.6.3.3 可以和 IDE 自带的版本共存做法是把它编译成独立的 dcu 路径放在工程搜索路径前面也可以直接替换掉 IDE 自带的 bpl/dcu让整个 IDE 的组件面板都指向新版本。我的建议分两种情况。如果你只是在自己的工程里用 Indy共存模式最安全IDE 本身的第三方插件可能还在用旧版一旦强行替换老插件启动时找不着对应 bplIDE 可能直接起不来这种翻车案例不少。如果你要开发的组件将来要在多台机器上的 IDE 里安装那才需要走替换路线而且替换前必须完整备份原 bpl 和 dcu 目录。替换后有一个常见的坑Delphi 12 的组件面板里会同时出现两套同名控件例如两个 TIdHTTPServer。双份控件的原因是新旧两套设计时包都被 IDE 加载了。解决办法是在 Component → Install Packages 里找到旧版设计时包取消勾选即可不需要卸载。这一步我在多个环境里踩过特征很明确工具箱里控件名后面都带一个“(N)”看着就烦。2.4 平台选择Win32 与 Win64 各要编译一遍Delphi 12 默认控件安装只针对当前平台。如果你想开发的目标平台是 Win64很多数据采集工具和网关程序现在都跑在 64 位环境下那么安装包时需要把平台的编译也做掉。如果不做这一步会出现“在 Win32 工程里一切正常切到 Win64 就提示找不到 dcu”的尴尬情况。这不是源码包的问题是编译产物没覆盖目标平台。做法是在 ProjectGroup 窗口里右键打开 Set Active Platform把每个 dpk 都切到 64-bit Windows 后再 Build 一次。设计时包可以只安装在 IDE 宿主平台上但运行时包必须把目标平台都编出来。这个细节很容易被忽略等你的安装程序在 64 位服务器上一跑起来提示缺少 BPL 时才反应过来又得回头补编译。3. 安装与编译从 dpk 到组件出现在工具栏3.1 用 IDE 打开 dpk 逐级编译并安装设计时包打开 Packages 目录下对应 Delphi 12 的子目录找到几个关键文件IndySystem.dpk、IndyCore.dpk、IndyProtocols.dpk以及 dcl 前缀的设计时包。文件名前缀 dcl 是 design-time 的缩写它本身不包含协议实现只是把控件类注册到 IDE 组件面板。编译顺序不能错。先双击打开 IndySystem.dpk在 ProjectGroup 窗口里右键这个节点选 Build等待输出窗口显示 Build succeeded。同样的操作依次对 IndyCore.dpk、IndyProtocols.dpk 执行。全部 Build 完成后再打开 dclIndy 开头的 dpk右键选 Install看到消息框提示 Package installed 说明注册成功。安装完成后Delphi 12 组件面板会多出几个标签页Indy Clients、Indy Servers、Indy Intercepts、Indy IOHandlers、Indy Misc。拖一个 TIdHTTPServer 到窗体上并运行编译不报错的话这一步就算过关。注意拖控件时如果 IDE 报“Cannot load package”错误大概率是设计时包与你当前 IDE 版本里的 VCL 版本不匹配重新 Build 一次设计时包即可。3.2 只编译不安装手动把 dcu 路径加进 Library Path如果你的工程是以命令行方式编译的或者不想动 IDE 的全局包那就不需要安装设计时包只需把编译出来的 dcu 路径按顺序加进库搜索路径。在 Delphi 12 里打开 Tools → Options → Delphi Options → Library在 Library Path 里把以下三个路径依次添加到最前面解压目录\Lib\System 解压目录\Lib\Core 解压目录\Lib\Protocols这里顺序有讲究。Delphi 的库路径搜索按先后顺序匹配先找到的 dcu 会被优先使用。把 System 放在最前面是为了优先匹配新的核心单元协议层才能稳定指向同一套代码。如果顺序反了编译时很可能出现旧版核心单元和新版协议单元混用的情况表现出很玄学的崩溃现场。提示路径末尾不要带多余的分号。Library Path 以分号分隔多个路径如果你在某个路径后多加了一个分号IDE 会解析成空路径随后报“找不到单元”或“内部链接错误”。这类问题排查起来很费劲因为看起来路径都对实际解析是错的。3.3 确认 IDE 加载的是新版本安装完成不等于新版生效。打开任意工程右键 Project Manager → Options → Delphi Compiler → Search Path查看是否残留旧的 Indy 相关路径。很多默认工程会带上 Delphi 安装目录下的公共库路径那里面通常有自带的旧版 Indy dcu。常见做法是写一段简单代码验证运行时实际版本uses IdHTTP, IdGlobal; procedure TForm1.ButtonCheckClick(Sender: TObject); var Http: TIdHTTP; begin Http : TIdHTTP.Create(nil); try ShowMessage(Version: IdVersion); Http.Free; except on E: Exception do ShowMessage(E.ClassName : E.Message); end; end;如果弹出的版本号依旧是旧版本说明 Library Path 顺序没生效回去检查 3.2 的路径是否被 BDS 自带路径压住了。Delphi 的库路径优先级是从前往后所以必须把你的路径放在 BDS 默认路径之前。这里我补充一个细节很多工程文件的 Search Path 里直接写了绝对路径比如C:\Indy10\Lib\Core。换机器或者换工程时这些绝对路径会失效建议改用环境变量或相对路径。比如把解压目录放到工程目录下的ThirdParty\IndySearch Path 里写.\ThirdParty\Indy\Lib\Core这样整个工程目录拷到别的机器也能直接编译。3.4 命令行自动化构建MSBuild 直接编 dproj如果你的开发流程已经走了 CI需要把 Indy 的编译放进自动化脚本那么可以不打开 IDE直接用 MSBuild 编译 dpk 对应的 dproj 工程文件。常见的包工程里每个 dpk 旁边都有一个同名 .dproj 文件MSBuild 能识别它。set PATHC:\Program Files\Embarcadero\Studio\bin;%PATH% msbuild IndySystem.dproj /p:PlatformWin64 /t:Build /v:minimal msbuild IndyCore.dproj /p:PlatformWin64 /t:Build /v:minimal msbuild IndyProtocols.dproj /p:PlatformWin64 /t:Build /v:minimal参数说明/p:Platform指定目标平台Win32 或 Win64 按需切换/t:Build是 MSBuild 的构建目标/v:minimal控制日志输出级别避免刷屏。构建完成后 dcu 会输出到 dproj 里约定的 Output 目录你把它配到工程的库路径即可。用命令行编译后设计时包的安装你仍然要在 IDE 里做一次但设计时包本身不影响命令行编译的结果。如果你的 CI 产物是最终可执行程序而不需要在目标机器上打开 IDE 拖控件那设计时包完全不用装。4. 落地实战TIdHTTP 下载与 TIdTCPServer 自定义协议4.1 用 TIdHTTP 下载文件超时、重定向和 SSL 绑定TIdHTTP 是 Indy 里最常用的协议控件之一适合做文件下载、接口请求和网页抓取。下面这段代码演示了带超时和重定向控制的文件下载uses IdHTTP, IdSSLOpenSSL; procedure TForm1.DownloadFile(const URL, SavePath: string); var HTTP: TIdHTTP; SSL: TIdSSLIOHandlerSocketOpenSSL; FS: TFileStream; begin HTTP : TIdHTTP.Create(nil); SSL : TIdSSLIOHandlerSocketOpenSSL.Create(HTTP); try HTTP.IOHandler : SSL; HTTP.ConnectTimeout : 10000; // 连接建立超时 10 秒 HTTP.ReadTimeout : 30000; // 单次读超时 30 秒 HTTP.HandleRedirects : True; // 自动跟随 301/302 HTTP.Request.UserAgent : AppName/1.0; FS : TFileStream.Create(SavePath, fmCreate or fmShareDenyWrite); try HTTP.Get(URL, FS); finally FS.Free; end; finally HTTP.Free; // IOHandler 由 HTTP 实例持有并释放 end; end;这里有几个关键参数需要理解。ConnectTimeout 和 ReadTimeout 都以毫秒为单位前者是建立 TCP 连接的最大等待时间后者是建立连接后每次读取数据的最大等待时间。HandleRedirects 设为 True 会跟随服务器返回的 301/302 跳转如果设为 False遇到重定向会抛出 EIdHTTPProtocolException你可以在异常里读 Response.Location 自己处理。SSL 对象的 Create 参数必须是 HTTP 实例这样才能保证 IOHandler 的释放顺序正确。HTTP.Free 会释放它持有的 SSL 对象不需要你手动再 Free 一次。把 SSL 单独释放会造成双重释放程序可能直接崩溃。下载大文件时用带流的 Get 重载是关键字符串版 Get 会把整个响应体读进内存几十 MB 的文件会撑爆内存。用 TFileStream 接收则是一条条写盘内存占用低很多。4.2 用 TIdTCPServer 起一个自定义 TCP 服务TIdTCPServer 适合做自定义协议的服务端比如内网设备接入网关、数据中转服务、消息推送等。下面的例子演示了一个最简单的文本协议服务客户端发 PING服务端回 PONGuses IdContext; procedure TForm1.FormCreate(Sender: TObject); begin IdTCPServer1.DefaultPort : 9000; IdTCPServer1.MaxConnections : 256; IdTCPServer1.OnExecute : IdTCPServer1Execute; IdTCPServer1.Active : True; end; procedure TForm1.IdTCPServer1Execute(AContext: TIdContext); var Cmd: string; begin Cmd : AContext.Connection.IOHandler.ReadLn; if Cmd PING then AContext.Connection.IOHandler.WriteLn(PONG) else AContext.Connection.IOHandler.WriteLn(ERR UNKNOWN); end;OnExecute 事件在 Indy 内部线程池中执行不同的客户端连接对应不同线程所以回调里不需要额外开线程只需要注意不能直接访问主窗体上的 VCL 控件。需要更新 UI 时用 TThread.Synchronize 或 TIdNotify这一点在 4.3 里展开。MaxConnections 限制并发连接数。如果设置成 0 或者不设默认是没有上限的对于生产环境不安全。连接数打满之后新的连接请求会被拒绝AContext 的 OnDisconnect 事件里能看到连接退出。还有一个需要注意的点DefaultPort 设成 0 意味着随机分配端口这在调试时不利于防火墙放行。建议明确指定端口号。如果端口被其他进程占用设置 Active : True 时会抛 Socket Error错误信息里一般直接带端口号。4.3 数据回传 UITIdNotify 的正确用法OnExecute 在后台线程执行直接操作 Label 或 Memo 必然出问题轻则界面闪烁重则随机崩溃。Indy 里除了 TThread.Synchronize还提供了一个更顺手的方式 TIdNotify。uses IdNotify; type TMessageNotify class(TIdNotify) private FData: string; public constructor Create(const AData: string); protected procedure DoNotify; override; end; constructor TMessageNotify.Create(const AData: string); begin inherited Create; FData : AData; end; procedure TMessageNotify.DoNotify; begin MainForm.LogMemo.Lines.Add(FData); end;在 OnExecute 里这样调用TMessageNotify.Create(收到命令: Cmd).Notify;TIdNotify 的 Notify 方法是异步通知会向主线程消息队列投递一个任务后立即返回不会阻塞当前工作线程。如果需要同步等待主线程处理完再继续可以调用 Synchronize 方法但更建议用 Notify因为同步等待在 TCP 回调里有风险如果主线程同时正在等你的 OnExecute 返回两者会互相等待业务线程全部卡死。使用 TIdNotify 时注意它内部持有对象指针在 DoNotify 里访问主线程的控件没问题不要再在 DoNotify 里调用 Synchronize那会嵌套等待。这个类用顺手之后代替大量 TThread.Synchronize 的写法代码干净很多。5. 避坑手册Indy 10.6.3.3 安装与使用中的五个高频问题5.1 Could not load SSL library现象程序用 TIdHTTP 访问 HTTPS 地址运行时抛出异常提示找不到 SSL 库或加载 SSL 库失败。原因Indy 本身不包含 OpenSSL 加密库它需要外部 DLL。Windows 下 OpenSSL 3.x 需要 libssl-3-x64.dll 和 libcrypto-3-x64.dll32 位程序需要 libssl-3.dll 和 libcrypto-3.dllOpenSSL 1.1.x 则需要 libssl-1_1-x64.dll 和 libcrypto-1_1-x64.dll。混用或者缺失都会导致加载失败。解决把对应 DLL 放到 exe 同目录下不要放到系统目录里。如果机器上装过多个软件可能自带不同版本的 OpenSSL环境变量 PATH 里的 DLL 会互相干扰放 exe 同目录最稳。还要注意 TIdSSLIOHandlerSocketOpenSSL 的 SSLOptions.SSLVersions 属性默认配置通常只启用 TLS 1.0 到 1.2如果连接 TLS 1.3 的服务器需要确认 Indy 版本支持 TLS 1.3 并启用相应版本。从 10.6.3.3 开始Indy 对 OpenSSL 3.x 的支持比旧版好很多这也是很多开发者从旧版升级的主要原因。如果你还在用 10.5.x配合 OpenSSL 3.x 会频繁出现握手失败。5.2 编译报 File not found: IdCore.dcu现象打开源码包里的 dpk 或者自己的工程时编译器提示找不到 IdCore.dcu。原因IdCore 单元位于 Core 目录下而编译器的搜索路径没有包含它。通常是只配了 Protocols 路径或者路径顺序不对。解决打开 Tools → Options → Delphi Options → Library在 Library Path 里添加 Core 和 System 路径顺序必须 System 在前、Core 在后。如果是在命令行编译检查你的环境变量里 LIB 或 Library 变量是否指向了解压目录。这里有一个多年来的习惯问题很多人直接把整个 Lib 目录加进搜索路径这样 System、Core、Protocols 都能找到。但这样做的副作用是如果 Lib 目录下有多个版本的子目录或者你同时引用多个第三方库重名单元会很难排查。我一般还是建议精确到三个子目录逐个添加。5.3 ReadLn 一直阻塞不返回现象TIdTCPClient 连接到服务器后调用 IOHandler.ReadLn 读取响应程序像卡死一样一直没有返回数据。原因ReadLn 默认以 LF 即换行符作为行结束符如果对端发送的数据没有换行符或者用的是 CRLFReadLn 会一直等下去。此时如果没有设置 ReadTimeout它就无限期阻塞。解决给连接设置 ReadTimeout比如 3000 毫秒超时后 ReadLn 会抛 EIdReadTimeout 异常你需要捕获它做业务处理。还可以指定行结束符IdTCPClient1.ReadTimeout : 3000; Line : IdTCPClient1.IOHandler.ReadLn(IndyTextEncoding_UTF8);ReadLn 的重载参数很多第一个参数通常是编码第二个是行结束符。如果对端使用 CRLF可以这样显式配置Line : IdTCPClient1.IOHandler.ReadLn(#10); // 或 #13#10字符串参数是按字节值作为分隔符解释的#10表示 LF#13#10表示 CRLF。这个参数不传时默认就是 LF很多服务器实际用 CRLF这时候就会出现一边发数据、一边收不到的现象。5.4 Windows 服务里端口起不来现象把一个业务组件做成 Windows 服务服务启动日志显示正常但客户端始终连不上本地测试也时断时续。原因从 Windows Vista 开始服务运行在 Session 0 隔离环境网络访问逻辑和普通桌面程序不完全一样。防火墙入站规则如果只放行了开发机的某个 exe换成服务的 exe 后规则不匹配端口就相当于关闭。解决先做两件事。第一在服务代码里把 TIdTCPServer 绑定到具体 IP而不是 0.0.0.0。0.0.0.0 表示监听所有网卡但排查问题时很难判断是哪条链路出了问题绑定内网 IP 更直接。第二在防火墙高级设置的入站规则里放行服务 exe 的端口。排查时用 netstat 看端口监听状态netstat -ano | findstr :9000看到监听进程 PID 后再在任务管理器里核对这个 PID 是不是你的服务进程。如果是别的进程占用了端口需要改默认端口或者停掉冲突进程。这个问题不是 Indy 特有的但 TIdTCPServer 的 Active 属性异常时不会给出明确的端口冲突提示容易误判成代码问题。5.5 卸载旧版时 IDE 提示包被占用现象在 Component → Install Packages 里取消勾选旧的 Indy 设计时包IDE 弹出提示说这个包被其他包依赖不能卸载。原因某些第三方组件包在设计时引用了旧版 Indy 的设计时包直接移除它会破坏那些组件的注册信息。解决不要强行删除。先把依赖了该包的第三方包也取消勾选再把旧版 Indy 包移除然后安装新版 Indy 包最后重新勾选第三方包。顺序错乱会导致 IDE 工具栏控件丢失甚至启动时报包错误。如果你不确定哪些包依赖了旧 Indy可以看 Packages 列表里每个包的“Contains”信息那里会列出依赖项。这个方法比逐个试快很多。6. 进阶调试技巧用 TIdLogFile 把网络会话变成文本调试自定义 TCP 协议时最痛苦的不是代码逻辑错而是数据收发到底发生了什么看不到。断点在 OnExecute 里只能看到当前命令收发内容的完整顺序往往要靠加日志输出。Indy 自带的 TIdLogFile 组件就是为了解决这个问题设计的。把它挂到连接上所有读写的数据都会落盘一行不丢。以客户端为例uses IdLogFile, IdTCPClient; procedure TForm1.ConnectToServer; var Logger: TIdLogFile; begin Logger : TIdLogFile.Create(nil); try Logger.Filename : ExtractFilePath(ParamStr(0)) tcp_capture.log; Logoer.LogTimeStamp : True; // 每行带时间戳 Logger.LogRecv : True; // 记录接收数据 Logger.LogSent : True; // 记录发送数据 IdTCPClient1.Intercept : Logger; IdTCPClient1.Connect(127.0.0.1, 9000); finally // 拦截器由 IdTCPClient1 持有连接关闭后自动释放 end; end;TIdLogFile 是 TIdLogBase 的派生类挂到 Intercept 属性后连接通道上的收发都会经过它。LogTimeStamp 会在每条日志前写上时间LogRecv 和 LogSent 分别控制接收和发送方向的内容是否记录。对于数据帧混杂的协议这个日志文件是最直观的“黑匣子”。对于 TIdTCPServer挂日志的方式略有不同。因为在 OnExecute 里你才有 AContext这时才访问连接procedure TForm1.IdTCPServer1OnConnect(AContext: TIdContext); var Logger: TIdLogFile; begin Logger : TIdLogFile.Create(nil); Logger.Filename : ExtractFilePath(ParamStr(0)) conn_ AContext.Binding.PeerIP .log; AContext.Connection.Intercept : Logger; end;每个客户端连接都生成一个独立日志文件文件名带客户端 IP方便逐个连接排查。这一步在调试并发类问题时特别有效比如两个客户端同时发送同一命令服务端处理结果不一致你对比两份日志就能定位到请求顺序的问题。有一次我调试一个设备采集模块对端发来的时间戳格式和文档对不上程序里反复看解析代码都没发现问题。后来就是挂上 TIdLogFile 看原始字节发现对端把年、月、日的分隔符从横杠换成了斜杠而我在代码里写死了按横杠截取。从那以后我每次做协议对接的第一件事就是把日志拦截器挂在客户端上数据从哪一步开始失真日志一看便知。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取