一、为什么Indy在高并发下容易出问题
Indy是Delphi中老牌的网络组件库,就像一位经验丰富但年纪稍大的邮差。当信件不多时,他能准确投递,但一旦遇到双十一这样的爆仓情况,就可能手忙脚乱。主要原因有三:
- 默认设置是为普通场景设计的,就像邮差平时只背个小邮包
- 连接管理比较"粗放",没有现代快递公司的分拣系统
- 线程调度像老式火车站,多个列车共用一条轨道
我们来看个典型问题示例(技术栈:Delphi 10.4 + Indy 10.6.2):
procedure TForm1.Button1Click(Sender: TObject);
var
i: Integer;
begin
for i := 1 to 1000 do // 模拟1000个并发请求
TThread.CreateAnonymousThread(
procedure
var
HTTP: TIdHTTP;
begin
HTTP := TIdHTTP.Create(nil);
try
HTTP.Get('http://example.com/api'); // 简单GET请求
finally
HTTP.Free;
end;
end
).Start;
end;
这段代码的问题在于:
- 每个请求都新建连接,像每次寄信都雇个新邮差
- 没有错误处理和重试机制
- 线程创建没有限制,可能耗尽系统资源
二、连接池的正确打开方式
连接池就像快递公司的配送车队,让邮差们可以重复利用。这是改进后的方案:
// 连接池管理单元
unit ConnectionPool;
interface
uses
IdHTTP, System.Classes, System.SyncObjs;
type
THTTPConnectionPool = class
private
FPool: TThreadList<TIdHTTP>; // 线程安全的列表
FMaxCount: Integer;
FTimeout: Integer;
public
constructor Create(MaxCount: Integer; Timeout: Integer);
function GetConnection: TIdHTTP;
procedure ReleaseConnection(HTTP: TIdHTTP);
end;
implementation
constructor THTTPConnectionPool.Create(MaxCount, Timeout: Integer);
begin
FPool := TThreadList<TIdHTTP>.Create;
FMaxCount := MaxCount;
FTimeout := Timeout;
end;
function THTTPConnectionPool.GetConnection: TIdHTTP;
var
List: TList<TIdHTTP>;
begin
List := FPool.LockList;
try
if List.Count > 0 then
Exit(List.ExtractAt(0)); // 取出闲置连接
if List.Count < FMaxCount then
begin
Result := TIdHTTP.Create(nil);
Result.ConnectTimeout := FTimeout; // 设置超时
Result.ReadTimeout := FTimeout;
Exit;
end;
// 等待可用连接
while List.Count = 0 do
begin
FPool.UnlockList;
Sleep(100);
List := FPool.LockList;
end;
Result := List.ExtractAt(0);
finally
FPool.UnlockList;
end;
end;
procedure THTTPConnectionPool.ReleaseConnection(HTTP: TIdHTTP);
begin
FPool.Add(HTTP); // 归还连接
end;
使用示例:
var
Pool: THTTPConnectionPool;
initialization
Pool := THTTPConnectionPool.Create(50, 5000); // 最大50连接,5秒超时
finalization
Pool.Free;
end;
// 使用时
procedure TForm1.Button2Click(Sender: TObject);
var
i: Integer;
begin
for i := 1 to 1000 do
TThread.CreateAnonymousThread(
procedure
var
HTTP: TIdHTTP;
begin
HTTP := Pool.GetConnection;
try
try
HTTP.Get('http://example.com/api');
except
on E: Exception do
LogError(E.Message); // 错误处理
end;
finally
Pool.ReleaseConnection(HTTP);
end;
end
).Start;
end;
三、性能调优的七个关键点
连接超时设置:给邮差配个手表
IdHTTP1.ConnectTimeout := 3000; // 3秒连接超时 IdHTTP1.ReadTimeout := 5000; // 5秒读取超时启用Keep-Alive:让邮差多跑几趟
IdHTTP1.Request.Connection := 'keep-alive';压缩传输:让邮包变小
IdHTTP1.Request.AcceptEncoding := 'gzip, deflate'; IdHTTP1.HTTPOptions := IdHTTP1.HTTPOptions + [hoKeepOrigProtocol];DNS缓存:记住客户地址
IdDNSResolver1.Host := '8.8.8.8'; // 使用可靠DNS IdHTTP1.Compressor := IdCompressorZLib1; // 启用压缩线程池控制:别让太多邮差同时出门
// 使用TThreadPool替代直接创建线程 TThreadPool.Default.SetMaxWorkerThreads(CPUCount * 4);连接限制:控制同时派出的邮差数量
IdHTTP1.ProxyParams.BasicAuthentication := False; IdHTTP1.MaxAuthRetries := 2; // 认证重试次数日志监控:记录邮差的工作日志
IdLogFile1.Filename := 'http_log.txt'; IdHTTP1.Intercept := IdLogFile1;
四、实战中的避坑指南
场景一:服务器突然重启
try
HTTP.Get(url);
except
on E: EIdConnClosedGracefully do
Reconnect; // 优雅重连
on E: EIdSocketError do
Sleep(1000); // 等待后重试
end;
场景二:处理慢速网络
procedure TForm1.LongRequest;
var
HTTP: TIdHTTP;
begin
HTTP := TIdHTTP.Create(nil);
try
HTTP.OnWorkBegin := WorkBegin; // 显示进度
HTTP.OnWork := Work;
HTTP.OnWorkEnd := WorkEnd;
HTTP.Get(largeFileUrl);
finally
HTTP.Free;
end;
end;
场景三:批量请求处理
// 使用TIdThreadSafeStringList共享任务队列
var
TaskQueue: TIdThreadSafeStringList;
procedure TWorkerThread.Execute;
var
task: string;
begin
while not Terminated do
begin
task := TaskQueue.Lock.Extract(0);
if task = '' then Break;
ProcessTask(task);
TaskQueue.Unlock;
end;
end;
五、Indy的替代方案比较
虽然Indy很好用,但也要知道其他选择:
纯Socket方案:更底层,但开发复杂
var Client: TIdTCPClient; begin Client := TIdTCPClient.Create(nil); try Client.Host := 'example.com'; Client.Port := 80; Client.Connect; Client.IOHandler.Write('GET / HTTP/1.0'#13#10#13#10); ShowMessage(Client.IOHandler.AllData); finally Client.Free; end; end;REST组件:适合现代API
var RESTClient: TRESTClient; Request: TRESTRequest; begin RESTClient := TRESTClient.Create('https://api.example.com'); Request := TRESTRequest.Create(nil); try Request.Client := RESTClient; Request.Resource := 'users/1'; Request.Execute; ShowMessage(Request.Response.JSONValue.ToString); finally Request.Free; RESTClient.Free; end; end;
六、总结与最佳实践
经过以上探索,我们得出以下经验:
- 连接管理:像管理员工一样管理连接,避免频繁招聘和裁员
- 错误处理:给每个可能出错的地方准备Plan B
- 资源控制:限制最大并发数,防止系统过载
- 监控指标:记录响应时间、错误率等关键数据
- 渐进式优化:先确保稳定,再追求性能
最后记住:没有放之四海皆准的方案,要根据你的具体业务场景调整这些参数。就像给邮差配装备,送同城快递和跨国包裹需要的配置完全不同。
评论