一、为什么Indy在高并发下容易出问题

Indy是Delphi中老牌的网络组件库,就像一位经验丰富但年纪稍大的邮差。当信件不多时,他能准确投递,但一旦遇到双十一这样的爆仓情况,就可能手忙脚乱。主要原因有三:

  1. 默认设置是为普通场景设计的,就像邮差平时只背个小邮包
  2. 连接管理比较"粗放",没有现代快递公司的分拣系统
  3. 线程调度像老式火车站,多个列车共用一条轨道

我们来看个典型问题示例(技术栈: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;

三、性能调优的七个关键点

  1. 连接超时设置:给邮差配个手表

    IdHTTP1.ConnectTimeout := 3000; // 3秒连接超时
    IdHTTP1.ReadTimeout := 5000;    // 5秒读取超时
    
  2. 启用Keep-Alive:让邮差多跑几趟

    IdHTTP1.Request.Connection := 'keep-alive';
    
  3. 压缩传输:让邮包变小

    IdHTTP1.Request.AcceptEncoding := 'gzip, deflate';
    IdHTTP1.HTTPOptions := IdHTTP1.HTTPOptions + [hoKeepOrigProtocol];
    
  4. DNS缓存:记住客户地址

    IdDNSResolver1.Host := '8.8.8.8'; // 使用可靠DNS
    IdHTTP1.Compressor := IdCompressorZLib1; // 启用压缩
    
  5. 线程池控制:别让太多邮差同时出门

    // 使用TThreadPool替代直接创建线程
    TThreadPool.Default.SetMaxWorkerThreads(CPUCount * 4);
    
  6. 连接限制:控制同时派出的邮差数量

    IdHTTP1.ProxyParams.BasicAuthentication := False;
    IdHTTP1.MaxAuthRetries := 2; // 认证重试次数
    
  7. 日志监控:记录邮差的工作日志

    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很好用,但也要知道其他选择:

  1. 纯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;
    
  2. 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;
    

六、总结与最佳实践

经过以上探索,我们得出以下经验:

  1. 连接管理:像管理员工一样管理连接,避免频繁招聘和裁员
  2. 错误处理:给每个可能出错的地方准备Plan B
  3. 资源控制:限制最大并发数,防止系统过载
  4. 监控指标:记录响应时间、错误率等关键数据
  5. 渐进式优化:先确保稳定,再追求性能

最后记住:没有放之四海皆准的方案,要根据你的具体业务场景调整这些参数。就像给邮差配装备,送同城快递和跨国包裹需要的配置完全不同。