首页 / 资讯中心 / 文章详情

Delphi 12 集成 Indy 10.6.3.3:SSL 与 TCP 排坑

Delphi 12 集成 Indy 10.6.3.3:SSL 与 TCP 排坑 ★ FEATURED ARTICLE
简介Indy-Indy-10.6.3.3.zip 是面向 Delphi 12 及 RAD Studio 开发者的网络组件库源码包汇集 Internet Direct 10.6.3.3 版本的完整实现适合需要构建邮件客户端、Web 服务、FTP 工具或聊天程序的中高级开发者参考与集成。压缩包为 rar 格式共约 2000 个文件体积 9.67MB以 pas 源码、dpk 包工程和 dproj 项目文件为主辅以 bmp、ico 等图标资源rc、res 资源脚本以及 txt 说明和 inc 头文件较完整地保留了 Indy 各组件的编译与安装结构。已有 362 人学习下载。包内可看到 TCP、UDP、HTTP、SMTP、POP3、FTP 等协议的组件实现TIdTCPClient、TIdHTTP 等常用控件可直接拖放使用异步事件与 OnExecute 机制便于处理进度显示和异常对希望研究协议封装、复用源码或按需裁剪扩展的读者这份带工程文件与资源脚本的版本具备较高参考价值。1. Delphi 12 控件库里的 Indy 10.6.3.3老项目为什么还在追这条分支维护过 Delphi 老系统的人对 Indy 的感情都很复杂delphi 控件里它几乎是唯一还在演进的开源网络层TIdHTTP、TIdTCPClient、TIdTCPServer、TIdSMTP 一套组件把 HTTP、TCP、SMTP、FTP 全包了但 IDE 自带的那份版本往往落后一截SSL 握手、IPv6、编码处理上的毛病得靠换源码解决。Indy 10.6.3.3 这个包就是一份完整可编译的源码加设计期包集合装进 Delphi 12 Athens 后可以把 IDE 自带的旧分支整体替换掉。适合正在维护存量项目、需要 TIdHTTP 抓数据或 TIdTCPServer 跑私有协议的人不适合只想拖个控件发个 GET 就收工的场景因为它的坑都在路径、位数和线程模型上。2. 装进 IDE把 Indy 10.6.3.3 编成设计期包的完整路径拿到压缩包先别急着双击 dpkIndy 的安装失败十有八九不是包本身坏了而是编译顺序和搜索路径打架。这一章按“先清场、再分层编译、最后回环验证”的顺序走一遍中途每一步都有可观察的结果错了能立刻定位到是哪一层出的问题。2.1 为什么 Indy 必须以源码形式编进项目Indy 是纯 Object Pascal 实现的协议栈除了 OpenSSL 那两个动态库其余代码全部在编译期以 DCU 形式进 exe。这个设计带来两个直接后果一是断点能一路打进 IdHTTP.pas、IdIOHandler.pas 的内部排查握手细节时不用猜黑匣子二是编译器对“哪份 IdGlobal 被用上了”极其敏感。IDE 自带的 Indy 在安装目录的Bin和Lib下各有一份前者是 BPL后者是 DCU 和源码。很多人的翻车现场长这样装了新包项目里一编译就报Unit IdHTTP was compiled with a different version of IdGlobal或者更隐蔽的Incompatible types: TIdBytes and TIdBytes——同一个类型名来自两个不同版本的单元。原因不是新包没装好而是项目的搜索路径顺序里IDE 老路径排在了 Indy 新路径前面。我一般会做的第一件事是确认“到底有几份 Indy”# 在 Delphi 12 安装目录下找所有 IdGlobal.pas看有几份源码 find /c/Program Files/Embarcadero/Studio/29.0 -name IdGlobal.pas 2/dev/null find /d/Indy10.6.3.3 -name IdGlobal.pas 2/dev/null两条命令各返回一份是正常状态返回三份以上说明机器上还躺着别的 Indy 副本后面必然互相污染。确认完再动手比装完再回滚省事得多。还有一个容易忽略的点Indy 的包名在不同 IDE 版本下带不同后缀Delphi 12 上是290。所以你会看到IndyCore290.bpl、dclIndyCore290.bpl这样的文件。这个后缀不是包作者随手起的它绑定了编译器的包版本校验混用别的版本后缀的 BPL 会直接导致 IDE 加载失败。2.2 分层编译与包安装的准确顺序Indy 的源码目录按依赖关系分了层常见的是Lib\System、Lib\Core、Lib\Protocols较新的分支还会多一个SuperCore放 SSL 相关的底层单元。这个分层不是装饰它对应包的依赖顺序编译必须从底往上走。层级目录关键单元对应包基础Lib\SystemIdGlobal、IdComponent、IdBaseComponentIndySystem290核心Lib\CoreIdTCPConnection、IdIOHandler、IdThreadIndyCore290协议Lib\ProtocolsIdHTTP、IdTCPClient、IdTCPServer、IdSMTPIndyProtocols290SSLLib\SuperCoreIdSSLOpenSSL、IdSSLOpenSSLHeaders随 Protocols 一起编运行时包和设计期包要分成两批先IndySystem290→IndyCore290→IndyProtocols290这三个是运行库再dclIndySystem290→dclIndyCore290→dclIndyProtocols290这三个才是往 IDE 里注册组件的。在 IDE 里操作就是打开对应的.dpk右键 Build成功后右键 Install。麻烦在于 IDE 一次只能开一个工程来回切换很烦所以我更常用命令行批处理# 前提先用 IDE 打开一次各 dpk让 IDE 生成对应的 .dproj # 之后就可以脱离 IDE 反复构建改源码后重编不用开 IDE set MSBUILDC:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe %MSBUILD% Packages\IndySystem290.dproj /t:Build /p:ConfigRelease /p:PlatformWin32 %MSBUILD% Packages\IndyCore290.dproj /t:Build /p:ConfigRelease /p:PlatformWin32 %MSBUILD% Packages\IndyProtocols290.dproj /t:Build /p:ConfigRelease /p:PlatformWin32 %MSBUILD% Packages\dclIndyCore290.dproj /t:Build /p:ConfigRelease /p:PlatformWin32 %MSBUILD% Packages\dclIndyProtocols290.dproj /t:Build /p:ConfigRelease /p:PlatformWin32/p:Platform必须显式给不能靠默认值。32 位和 64 位是两套完全独立的 BPL 和 DCU只编 Win32 却在 Win64 项目里引用报错信息会指向一个看起来毫无关联的单元。/p:ConfigRelease也不是可选项Debug 配置下生成的包体积大一截装进 IDE 后加载变慢而且和别人的环境对不上。编译前先备份 IDE 自带的 BPL这是唯一的后悔药# 把 IDE 原有的 Indy 包挪走而不是删掉 mkdir -p /c/Backup/Indy_ide_290 mv /c/Program Files/Embarcadero/Studio/29.0/bin/Indy*.bpl /c/Backup/Indy_ide_290/ mv /c/Program Files/Embarcadero/Studio/29.0/bin/dclIndy*.bpl /c/Backup/Indy_ide_290/新编出来的 BPL 要放到 IDE 能找到的位置要么直接拷进Bin要么把输出目录加进系统 PATH。放 PATH 的好处是不污染安装目录坏处是换机器部署时容易忘我倾向于拷进Bin并保留备份。最后是搜索路径这是全篇最容易出错的一步。不要改 IDE 的全局 Library Path那会污染所有工程。正确做法是在需要使用 Indy 的项目里于Project Options Delphi Compiler Search path中按顺序加上三个目录并把 Indy 路径放在最前面D:\Indy10.6.3.3\Lib\System;D:\Indy10.6.3.3\Lib\Core;D:\Indy10.6.3.3\Lib\Protocols;$(BDSLIB)\$(Platform)\release注意路径分隔是分号且 Win32 和 Win64 的配置要分别在 Target Platforms 里各设一遍IDE 不会自动同步。设完编译一次在 IDE 里 Ctrl点击TIdHTTP能跳进D:\Indy10.6.3.3\Lib\Protocols\IdHTTP.pas就说明路径生效了跳进安装目录那份就是还没对。2.3 用一对 TCP 组件跑通回环验证装完包别急着上业务代码先用最小闭环证明“编进去的确实是新版本而且能收发”。服务端和客户端都放在同一个测试程序里本机回环不涉及任何外部依赖。服务端部分uses IdContext, IdTCPServer, IdGlobal, System.SysUtils; type TForm1 class(TForm) procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); private FTcp: TIdTCPServer; procedure HandleExecute(AContext: TIdContext); end; procedure TForm1.FormCreate(Sender: TObject); begin FTcp : TIdTCPServer.Create(Self); FTcp.DefaultPort : 17000; FTcp.MaxConnections : 64; // 并发上限超过后新连接被拒 FTcp.ListenQueue : 32; // 已完成三次握手但未被 accept 的队列长度 FTcp.OnExecute : HandleExecute; FTcp.Active : True; end; procedure TForm1.HandleExecute(AContext: TIdContext); var Line: string; begin // 这里运行在工作线程绝对不能碰任何 VCL 控件 Line : AContext.Connection.IOHandler.ReadLn(#13#10); if SameText(Line, PING) then AContext.Connection.IOHandler.WriteLn(PONG); end;MaxConnections和ListenQueue是两个容易被忽略的参数。前者是 Indy 在用户态自己计数的连接上限超了会主动断开并触发OnConnect拒绝后者对应 listen 的 backlog高并发短连接场景下给小了会出现偶发的连接重置。默认值在生产环境里通常偏小。客户端一行代码就能验证var Client: TIdTCPClient; Reply: string; begin Client : TIdTCPClient.Create(nil); try Client.Host : 127.0.0.1; Client.Port : 17000; Client.ConnectTimeout : 3000; // 毫秒握手阶段超时 Client.ReadTimeout : 5000; // 毫秒读阻塞超时 Client.Connect; Client.IOHandler.WriteLn(PING); Reply : Client.IOHandler.ReadLn(#13#10); ShowMessage(Reply); // 期望 PONG finally if Client.Connected then Client.Disconnect; Client.Free; end; end;ConnectTimeout和ReadTimeout的单位都是毫秒且ReadTimeout默认值是 0也就是无限等待。这两个值不设网络一抖程序就永久挂死在那里是最常见的新手翻车。3. 选型边界TIdHTTP、TIdTCPClient、TIdTCPServer 各自该用在哪3.1 阻塞模型的真实代价与 TIdAntiFreeze 的陷阱Indy 全部组件都是阻塞式 socket没有内建事件循环。TIdHTTP.Get调用会一直占着当前线程直到响应回来或超时。这在后台线程里完全没问题放在 VCL 主线程里就是界面假死。于是很多人第一反应是拖一个TIdAntiFreeze上去界面确实不卡了然后就在这个“不卡了”的假象里埋下了重入的雷。TIdAntiFreeze的原理非常朴素它开一个定时器在主线程空闲时调用Application.ProcessMessages让 Windows 消息队列里的重绘消息被处理掉。代价是你在等待 HTTP 返回的那几秒里用户点的每一个按钮、每一条菜单命令都会被真实执行而这些执行会重入你正在等待的那个函数上下文。典型现象是“偶发访问违例重放一次就好了”或者“下载到一半弹了个对话框回来流对象已被释放”。这不是 Indy 的 bug是重入。正确的做法只有一条把阻塞调用搬进TThread或 Delphi 12 里的TTask结果通过TThread.Queue回主线程更新界面。type TFetchTask class(TThread) protected procedure Execute; override; public Url: string; procedure Notify; // 回主线程执行 end; procedure TFetchTask.Execute; var Http: TIdHTTP; Body: string; begin Http : TIdHTTP.Create(nil); try Http.ConnectTimeout : 5000; Http.ReadTimeout : 20000; Body : Http.Get(Url); // 结果塞进字段再由 Notify 处理 finally Http.Free; end; TThread.Queue(nil, Notify); end;注意TThread.Queue不阻塞Synchronize会阻塞工作线程等主线程空闲。在循环里连续请求几十个 URL 时用Queue才不会把工作线程拖成串行。至于进度条TIdHTTP提供了OnWork和OnWorkBegin事件其中的AWorkCount是累计字节数。这两个事件也在工作线程里触发更新界面同样要走Queue。很多人直接在里面写ProgressBar.Position : ...在单核机器或高负载下就是随机崩溃属于血泪经验。3.2 TIdHTTP参数表与流式下载TIdHTTP是这个包里用得最多的组件它的行为几乎全靠属性控制记不住参数就只能靠试。下面这张表是我常用的稳定配置属性默认值建议值说明ConnectTimeout0无限5000建立 TCP 连接的超时毫秒ReadTimeout0无限30000等待响应的超时毫秒HandleRedirectsFalseTrue是否自动跟随 3xxRedirectMaximum155跟随次数上限防循环重定向Request.UserAgentMozilla/3.0 (compatible; Indy Library)按目标站点要求有些站点直接按 UA 拦截Request.Accept空text/html,application/json影响服务端返回格式协商ProtocolVersionpv1_1保持 pv1_1切到 HTTP/1.0 会丢 keep-aliveHandleRedirects默认关着这是很多人“拿到的内容是 302 页面”的根因。但也要注意开了自动跳转后OnRedirect事件里能看到中间跳转跨域跳转时Request.Referer不会自动改某些站点会因此判为异常请求。流式下载是另一个高频需求。直接把大文件读成 string 会瞬间吃掉两倍内存正确姿势是给它一个TStreamvar Http: TIdHTTP; Ssl: TIdSSLIOHandlerSocketOpenSSL; Fs: TFileStream; begin Http : TIdHTTP.Create(nil); Ssl : TIdSSLIOHandlerSocketOpenSSL.Create(nil); Fs : nil; try Ssl.SSLOptions.SSLVersions : [sslvTLSv1_2, sslvTLSv1_3]; // 现代 Indy 用这个集合 Http.IOHandler : Ssl; Http.ConnectTimeout : 5000; Http.ReadTimeout : 60000; Http.HandleRedirects : True; Http.RedirectMaximum : 5; Fs : TFileStream.Create(download.bin, fmCreate); Http.Get(https://example.invalid/large.bin, Fs); // 文件长度可用 Http.Response.ContentLength 交叉核对 finally Fs.Free; Http.Free; Ssl.Free; end; end;SSLOptions.Method在旧代码里很常见新版本上它已经和SSLVersions并存但语义重叠两者混设会导致启用集合被覆盖。我现在的习惯是只设SSLVersions不动Method。TLSv1_3要不要加取决于服务端和 OpenSSL 版本加进去后如果客户端加载的是 1.0.2 的库这个枚举会被静默忽略不会报错排查起来相当费劲。还有一个边界Get传入TStream时Indy 不会自动重置流位置。如果你复用一个TMemoryStream连续请求多个地址第二次拿到的数据会接在第一次后面。每次请求前Stream.Size : 0; Stream.Position : 0;一行都不能省。3.3 TIdTCPServerOnExecute 的上下文与连接生命周期TIdTCPServer给每个连接分配一个工作线程OnExecute在这个线程里被调用而且是循环调用的——这是最容易误解的一点。它不是一个连接回调一次而是 Indy 的线程在连接存活期间反复调用你的OnExecute直到连接断开或抛异常。这意味着两件事第一OnExecute里不要写“只执行一次”的初始化逻辑第二如果方法体里做了长时间阻塞读线程就卡在那儿其他连接不受影响但这个连接的OnDisconnect会延迟触发。按行读取是最常见的写法也是坑最多的地方procedure TForm1.HandleExecute(AContext: TIdContext); var Cmd: string; IO: TIdIOHandler; begin IO : AContext.Connection.IOHandler; IO.ReadTimeout : 30000; // 单次读超时超时抛 EIdReadTimeout try Cmd : IO.ReadLn(#13#10); // 必须和客户端的分隔符完全一致 if Cmd QUIT then AContext.Connection.Disconnect else IO.WriteLn(ACK: Cmd); except on E: EIdReadTimeout do AContext.Connection.Disconnect; // 超时主动踢掉避免线程长期挂着 end; end;ReadLn的终止符默认是#10LF但很多自定义协议用#13#10CRLF。不显式传终止符时遇到 CRLF 结尾的数据会在字符串末尾留一个#13下游判断Cmd PING永远为 False现象是“服务端收到了但没反应”。这种问题看日志看不出来因为日志打印出来是有一行空白的肉眼容易忽略。连接生命周期方面OnConnect触发时 TCP 三次握手已完成OnDisconnect在连接关闭后触发但如果是异常断开OnDisconnect里的AContext.Connection.Connected可能已经是 False。要统计在线数别用这个属性当计数依据自己在OnConnect里加、OnDisconnect里减并给OnDisconnect加上异常保护否则一个异常就能让计数永久偏移。4. SSL/TLS 与字符编码两个翻车率最高的地方4.1 OpenSSL 动态库的加载条件与 SSLVersions 设置Indy 的 HTTPS 能力不在 Delphi 语言层它靠IdSSLOpenSSL这个单元在运行时动态加载 OpenSSL 的两个动态库。加载成功与否完全取决于文件系统状态跟代码写得好不好没关系所以报错信息往往是“Could not load SSL library”这种一句话。加载的判定逻辑集中在IdSSLOpenSSLHeaders里它会按一系列候选名去尝试包括 OpenSSL 1.0.2 时代的libeay32.dll/ssleay32.dll和 1.1 之后的libcrypto-1_1.dll/libssl-1_1.dll以及 3.x 的libcrypto-3.dll/libssl-3.dll。具体接受哪些名字取决于你这份源码里编译开关的设定。不要猜让它自己说uses IdSSLOpenSSL, IdSSLOpenSSLHeaders; procedure TForm1.CheckSsl; begin if not LoadOpenSSLLibrary then raise Exception.CreateFmt(OpenSSL 加载失败缺失: %s, [WhichFailedToLoad]); end;WhichFailedToLoad返回的是那一串它尝试过但没找到的文件名比任何猜测都准。我遇到过最离谱的一次是动态库放在 exe 同目录名字也对但系统 PATH 里某个软件的旧版libeay32.dll优先级更高Indy 加载到了那个然后握手阶段报版本不匹配。把 exe 目录加入 PATH 最前面或者干脆用绝对路径指定问题才消失。位数是另一个必须显式确认的点。32 位 exe 只能加载 32 位动态库64 位相反混用不会有任何提示只会加载失败。任务管理器里看 exe 后面有没有(32 位)是最快的确认方式。至于协议版本我建议直接用集合而不是MethodSsl.SSLOptions.SSLVersions : [sslvTLSv1_2]; // 需要兼容更老的设备时再加 sslvTLSv1_1但现代服务端多半已禁用只留 TLS 1.2 是当下最稳的选择加 1.3 要看服务端和动态库是否都支持。反过来如果代码里还写着sslvSSLv2或sslvSSLv3握手会直接失败因为绝大多数服务端早就关掉了这两个版本。4.2 三处编码URL、Content-Type 与 DefStringEncoding中文乱码在 Indy 里有三个完全不同的来源混在一起排查会浪费大量时间。第一处是 URL 上的参数。TIdHTTP.Get(http://x/search?q中文)这种写法在不少服务端上会返回 400因为 URL 里不允许出现非 ASCII 字节必须先做百分号编码。Indy 提供了TIdURIuses IdURI, IdGlobal; var Encoded: string; begin // 显式指定 UTF-8避免默认编码随环境变化 Encoded : TIdURI.ParamsEncode(中文 关键词, IndyTextEncoding_UTF8); // 结果形如 %E4%B8%AD%E6%96%87%20%E5%85%B3%E9%94%AE%E8%AF%8D end;ParamsEncode会对空格编码成%20而某些站点期望这种差异只能按对方文档来没有通用解。第二处是响应体的字符集。TIdHTTP.Get返回 string 时Indy 会先看响应头Content-Type里的charset参数有就按它解没有就退回IOHandler.DefStringEncoding而这个默认值不是 UTF-8。国内不少接口不声明 charset实际内容是 UTF-8于是中文全变问号。稳妥的写法是自己拿字节流再解var Ms: TMemoryStream; Bytes: TBytes; Text: string; CS: string; begin Ms : TMemoryStream.Create; try Http.Get(Url, Ms); Ms.Position : 0; SetLength(Bytes, Ms.Size); if Ms.Size 0 then Ms.ReadBuffer(Bytes[0], Ms.Size); CS : Http.Response.Charset; // 来自 Content-Type 的 charset if CS then Text : TEncoding.UTF8.GetString(Bytes) else Text : TEncoding.GetEncoding(CS).GetString(Bytes); finally Ms.Free; end; end;TEncoding.GetEncoding传入不认识的编码名会抛异常生产代码里要包一层 try回退到 UTF-8。第三处是请求体。POST 中文时Request.ContentType必须带上 charset否则服务端按自己的默认编码解收到的就是乱码Http.Request.ContentType : application/json; charsetutf-8; Body : TEncoding.UTF8.GetBytes(JsonText); // 用字节流 POST别直接传 string Http.Post(Url, TMemoryStreamOf(Body));直接Post(Url, 中文)时Indy 会用IOHandler.DefStringEncoding转字节同样不是 UTF-8。这类问题在本地测试时一切正常一上线就崩因为测试环境的服务端恰好宽容。5. 排查五条从现场带回来的踩坑记录5.1 装完包 IDE 启动报“找不到 IndyCore290.bpl”现象设计期包 Install 成功重开 IDE 时弹窗提示某个 BPL 无法加载组件面板里 Indy 页签消失。原因只编译并安装了设计期包dclIndyCore290但它依赖的运行时包IndyCore290.bpl没有放到 IDE 的搜索路径里。设计期包本身很薄真正的实现全在运行时包里IDE 启动时要靠系统 PATH 或Bin目录找到它。解决把三个运行时 BPL 和三个设计期 BPL 全部拷到 IDE 的Bin目录或者把输出目录加入系统 PATH 后重启 IDE。注意是系统 PATH 不是用户 PATH某些环境下 IDE 以服务身份启动读不到用户级变量。装之前把原有的 Indy BPL 移到备份目录也是这一条的前置条件新旧两套混在Bin里同样会加载失败。5.2 HTTPS 请求报“Could not load SSL library”但文件明明就在旁边现象动态库和 exe 放在同一目录文件名看起来也没错但一调TIdHTTP.Get(https://...)就抛异常。原因Indy 是按候选名列表逐个尝试加载的列表里既有 1.0.2 的旧名字也有 3.x 的新名字你的文件名可能不在它认识的列表里或者系统 PATH 里存在另一份同名库被优先加载了再或者位数不匹配。解决先调WhichFailedToLoad打印实际尝试的文件名照着改名或补齐再用任务管理器确认 exe 位数去下对应位数的库最后检查 PATH 顺序把 exe 所在目录放到最前面。三步走完基本都能解决剩下的情况是动态库本身缺依赖用dumpbin /dependents看一眼就知道。5.3 下载大文件时内存暴涨或者第二次请求数据接在第一次后面现象循环下载几十个文件后进程占用几个 G或者同一个TMemoryStream复用第二次的结果前面多了一段旧数据。原因把响应直接读成 string 会在内存里保留完整副本大文件时翻倍复用流时没有重置TIdHTTP会从当前位置继续写。解决一律传TStream而不是让 Indy 返回 string并且每次请求前执行Stream.Size : 0; Stream.Position : 0;。如果是下载到磁盘直接用TFileStream配合fmCreate让数据不经过内存中转。5.4 服务端 OnExecute 里更新界面运行一段时间后随机崩溃现象测试时一切正常压测或长时间运行后出现访问违例堆栈指向某个 VCL 控件。原因OnExecute运行在 Indy 分配的工作线程里VCL 的控件只能在主线程操作。Delphi 的 VCL 不是线程安全的跨线程写属性在低负载下大概率不崩高负载下必崩。解决所有界面更新走TThread.Queue需要同步结果时用TThread.Synchronize。数据本身可以先写进线程安全的队列或TInterlocked保护的字段再由主线程定时取。另外OnExecute里不要抛未捕获异常Indy 会把它当成连接级错误处理日志里只剩一行“连接已断开”。5.5 ReadLn 一直卡到超时对端说数据已经发了现象客户端ReadLn阻塞很久最后抛超时抓包能看到数据确实到了。原因终止符不匹配。ReadLn不传参时按 LF 找行尾对端发的是 CRLF 或根本没有任何换行符定长协议、二进制协议。二进制数据用ReadLn更是必然出问题因为数据里可能随机出现#10字节被当成行尾截断。解决文本协议显式传终止符ReadLn(#13#10)并和对端确认到底用哪种二进制或定长协议改用ReadBytes(N)或ReadStream(Stream, N, False)按长度读。另外ReadTimeout一定要设不设就是无限等程序看起来像死机。6. 进阶把 Indy 收进一层可替换的传输接口把TIdHTTP、TIdTCPClient直接散落在业务代码里短期省事长期是灾难——换库要改几十处写单元测试还得起真服务器。我的做法是先定义一个极薄的传输接口Indy 只作为其中一个实现测试时换成内存假实现。type ITransport interface [{3C1F7A20-8B4E-4D19-9E77-2A5B6C0D1E44}] function Get(const AUrl: string; out AStatus: Integer): TBytes; procedure SetTimeout(AConnect, ARead: Integer); end; TIndyTransport class(TInterfacedObject, ITransport) private FHttp: TIdHTTP; FSsl: TIdSSLIOHandlerSocketOpenSSL; public constructor Create; destructor Destroy; override; function Get(const AUrl: string; out AStatus: Integer): TBytes; procedure SetTimeout(AConnect, ARead: Integer); end; function TIndyTransport.Get(const AUrl: string; out AStatus: Integer): TBytes; var Ms: TMemoryStream; begin Ms : TMemoryStream.Create; try FHttp.Get(AUrl, Ms); // 异常由调用方决定是否吞掉 AStatus : FHttp.ResponseCode; Ms.Position : 0; SetLength(Result, Ms.Size); if Ms.Size 0 then Ms.ReadBuffer(Result[0], Ms.Size); finally Ms.Free; end; end;接口里只保留字节流不返回 string编码问题被推到调用方统一处理这样 URL 编码和 charset 判断只写一次。out AStatus而不是抛异常是因为 4xx/5xx 在业务上往往需要区分处理而 Indy 对 4xx 默认抛EIdHTTPProtocolException异常里带ErrorCode用起来反而绕。测试用的假实现直接返回预置字节数组整个测试过程不需要网络跑起来是毫秒级场景真实 Indy 实现假实现正常 200起本地 HTTP 服务返回固定 JSON 字节404请求不存在的路径返回AStatus : 404超时设 1ms 超时 延迟服务直接返回空数组中文编码服务端不声明 charset返回 UTF-8 字节验解码逻辑从这层抽象往外看Indy 10.6.3.3 提供的只是“能跑通的 socket 实现”URL 编码、字符集判定、重试策略这些真正决定稳定性的东西都在业务侧。这也是我现在愿意花时间把它编进 IDE、而不是随便用自带版本的原因——源码在手IdIOHandler.pas里任何一个超时判断都能下断点。从那以后我每次换机器或者升级 IDE都会先跑一遍 2.3 节那个回环 PING/PONG 用例再跑一遍WhichFailedToLoad检查 SSL最后才敢把项目往新环境上搬。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站