简介: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 编成设计期包的完整路径
拿到压缩包先别急着双击 dpk,Indy 的安装失败十有八九不是包本身坏了,而是编译顺序和搜索路径打架。这一章按“先清场、再分层编译、最后回环验证”的顺序走一遍,中途每一步都有可观察的结果,错了能立刻定位到是哪一层出的问题。
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\System | IdGlobal、IdComponent、IdBaseComponent | IndySystem290 |
| 核心 | Lib\Core | IdTCPConnection、IdIOHandler、IdThread | IndyCore290 |
| 协议 | Lib\Protocols | IdHTTP、IdTCPClient、IdTCPServer、IdSMTP | IndyProtocols290 |
| SSL | Lib\SuperCore | IdSSLOpenSSL、IdSSLOpenSSLHeaders | 随 Protocols 一起编 |
运行时包和设计期包要分成两批:先IndySystem290→IndyCore290→IndyProtocols290(这三个是运行库),再dclIndySystem290→dclIndyCore290→dclIndyProtocols290(这三个才是往 IDE 里注册组件的)。
在 IDE 里操作就是打开对应的.dpk,右键 Build,成功后右键 Install。麻烦在于 IDE 一次只能开一个工程,来回切换很烦,所以我更常用命令行批处理:
# 前提:先用 IDE 打开一次各 dpk,让 IDE 生成对应的 .dproj # 之后就可以脱离 IDE 反复构建,改源码后重编不用开 IDE set MSBUILD="C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe" %MSBUILD% Packages\IndySystem290.dproj /t:Build /p:Config=Release /p:Platform=Win32 %MSBUILD% Packages\IndyCore290.dproj /t:Build /p:Config=Release /p:Platform=Win32 %MSBUILD% Packages\IndyProtocols290.dproj /t:Build /p:Config=Release /p:Platform=Win32 %MSBUILD% Packages\dclIndyCore290.dproj /t:Build /p:Config=Release /p:Platform=Win32 %MSBUILD% Packages\dclIndyProtocols290.dproj /t:Build /p:Config=Release /p:Platform=Win32/p:Platform=必须显式给,不能靠默认值。32 位和 64 位是两套完全独立的 BPL 和 DCU,只编 Win32 却在 Win64 项目里引用,报错信息会指向一个看起来毫无关联的单元。/p:Config=Release也不是可选项,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是这个包里用得最多的组件,它的行为几乎全靠属性控制,记不住参数就只能靠试。下面这张表是我常用的稳定配置:
| 属性 | 默认值 | 建议值 | 说明 |
|---|---|---|---|
| ConnectTimeout | 0(无限) | 5000 | 建立 TCP 连接的超时,毫秒 |
| ReadTimeout | 0(无限) | 30000 | 等待响应的超时,毫秒 |
| HandleRedirects | False | True | 是否自动跟随 3xx |
| RedirectMaximum | 15 | 5 | 跟随次数上限,防循环重定向 |
| Request.UserAgent | Mozilla/3.0 (compatible; Indy Library) | 按目标站点要求 | 有些站点直接按 UA 拦截 |
| Request.Accept | 空 | text/html,application/json | 影响服务端返回格式协商 |
| ProtocolVersion | pv1_1 | 保持 pv1_1 | 切到 HTTP/1.0 会丢 keep-alive |
HandleRedirects默认关着,这是很多人“拿到的内容是 302 页面”的根因。但也要注意,开了自动跳转后,OnRedirect事件里能看到中间跳转,跨域跳转时Request.Referer不会自动改,某些站点会因此判为异常请求。
流式下载是另一个高频需求。直接把大文件读成 string 会瞬间吃掉两倍内存,正确姿势是给它一个TStream:
var 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 TIdTCPServer:OnExecute 的上下文与连接生命周期
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的终止符默认是#10(LF),但很多自定义协议用#13#10(CRLF)。不显式传终止符时,遇到 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 位)是最快的确认方式。
至于协议版本,我建议直接用集合而不是Method:
Ssl.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 提供了TIdURI:
uses 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; charset=utf-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,最后才敢把项目往新环境上搬。希望帮到你。
本文还有配套的精品资源,点击获取