You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于TIdHTTP实现多线程文件下载:分片合并问题求助

多线程分片下载后的文件合并方案

问题分析

你通过TIdHTTP的Range属性实现了多线程分片下载,已成功生成多个分片文件,但需要将这些分片按顺序合并为完整文件。同时注意到原代码中Range的起始值计算不符合HTTP规范(字节范围从0开始),这会导致文件内容缺失,需先修正该问题,再实现合并逻辑。

步骤1:修正分片下载的Range计算

原代码中rangeStart从1开始,不符合HTTP Range的0起始规则,且未处理文件大小无法被总分片数整除的情况。修改TDLThread.Execute中的Range计算部分:

procedure TDLThread.Execute;
var
  idh: TIdHTTP;
  ssl: TIdSSLIOHandlerSocketOpenSSL;
  f: TFileStream;
  remoteFileName, localFileName : string;
  fileSize, rangeLength, rangeStart, rangeEnd: Int64; // 改用Int64避免大文件溢出
begin
  inherited;
  // 简化文件名提取(替代原循环逻辑)
  remoteFileName := ExtractFileName(remoteFile);
  
  idh := TIdHTTP.Create(nil); // 不要用Form1作为Owner,避免线程同步风险
  try
    idh.OnWork := work;
    ssl := TIdSSLIOHandlerSocketOpenSSL.create(idh);
    ssl.SSLOptions.Method := sslvSSLv23;
    idh.IOHandler := ssl;

    localFileName := remoteFileName + '_' + IntToStr(currentPart);
    f := TFileStream.Create(destination + localFileName, fmCreate);
    try
      idh.Head(remoteFile);
      fileSize := idh.Response.ContentLength;
      
      rangeLength := fileSize div totalParts;
      rangeStart := rangeLength * (currentPart - 1);
      // 最后一个分片直接取到文件末尾,处理无法整除的情况
      if currentPart = totalParts then
        rangeEnd := fileSize - 1
      else
        rangeEnd := rangeStart + rangeLength - 1;
      
      idh.Request.Range := 'bytes=' + IntToStr(rangeStart) + '-' + IntToStr(rangeEnd);
      idh.Get(remoteFile, f);
    finally
      f.Free;
    end;
  finally
    idh.Free;
  end;
end;

步骤2:跟踪所有下载线程,等待全部完成

在主窗体中添加线程列表,管理所有下载线程,确保所有分片下载完成后再执行合并:

type
  TForm1 = class(TForm)
    Button1: TButton;
    IdHTTP1: TIdHTTP;
    IdSSLIOHandlerSocketOpenSSL1: TIdSSLIOHandlerSocketOpenSSL;
    ProgressBar1: TProgressBar;
    procedure Button1Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    FDownloadThreads: TList<TDLThread>;
    procedure WaitForAllThreads;
    procedure MergeFileParts(const ADestPath, AFileName: string; ATotalParts: Integer);
  public
    { Public declarations }
  end;

// 实现部分
procedure TForm1.FormDestroy(Sender: TObject);
begin
  WaitForAllThreads;
  FDownloadThreads.Free;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  i, totalParts: integer;
  dlthread: TDLThread;
begin
  totalParts := 2;
  FDownloadThreads := TList<TDLThread>.Create;
  
  for i := 1 to totalParts do
  begin
    dlthread := TDLThread.Create(True);
    dlthread.remoteFile := 'https://download.jgsoft.com/editpad/SetupEditPadLite.exe';
    dlthread.destination :=  'c:\users\admin\desktop\';
    dlthread.totalParts := totalParts;
    dlthread.currentPart := i;
    FDownloadThreads.Add(dlthread);
    dlthread.Start; // 用Start替代Resume,符合Delphi新版本规范
  end;
  
  // 等待所有线程完成后执行合并
  WaitForAllThreads;
  MergeFileParts('c:\users\admin\desktop\', ExtractFileName('https://download.jgsoft.com/editpad/SetupEditPadLite.exe'), totalParts);
end;

procedure TForm1.WaitForAllThreads;
var
  Thread: TDLThread;
begin
  if not Assigned(FDownloadThreads) then Exit;
  for Thread in FDownloadThreads do
  begin
    Thread.WaitFor;
    Thread.Free;
  end;
  FDownloadThreads.Clear;
end;

步骤3:实现分片合并函数

编写MergeFileParts函数,按分片顺序读取内容并写入完整文件:

procedure TForm1.MergeFileParts(const ADestPath, AFileName: string; ATotalParts: Integer);
var
  DestStream: TFileStream;
  SourceStream: TFileStream;
  i: Integer;
  PartFileName: string;
begin
  // 创建目标完整文件
  DestStream := TFileStream.Create(ADestPath + AFileName, fmCreate);
  try
    for i := 1 to ATotalParts do
    begin
      PartFileName := ADestPath + AFileName + '_' + IntToStr(i);
      if not FileExists(PartFileName) then
      begin
        ShowMessage('分片文件缺失:' + PartFileName);
        Exit;
      end;
      
      SourceStream := TFileStream.Create(PartFileName, fmOpenRead or fmShareDenyWrite);
      try
        // 将分片内容复制到完整文件
        DestStream.CopyFrom(SourceStream, SourceStream.Size);
      finally
        SourceStream.Free;
        // 可选:合并完成后删除分片文件
        DeleteFile(PartFileName);
      end;
    end;
    ShowMessage('文件合并完成!');
  finally
    DestStream.Free;
  end;
end;

额外注意事项

  • 线程安全:原代码中work方法直接操作主窗体控件,会引发线程安全问题,需改用Synchronize更新UI:
    procedure TDLThread.work(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64);
    var
      Http: TIdHTTP;
      ContentLength: Int64;
      Percent: integer;
    begin
      Http := TIdHTTP(ASender);
      ContentLength := Http.Response.ContentLength;
    
      if (Pos('chunked', LowerCase(Http.Response.TransferEncoding)) = 0) and (ContentLength > 0) then
      begin
        Percent := 100 * AWorkCount div ContentLength;
        // 确保UI更新在主线程执行
        Synchronize(procedure
                    begin
                      Form1.ProgressBar1.Position := Percent;
                    end);
      end;
    end;
    
  • 大文件适配:所有文件大小相关变量改用Int64,避免32位整数溢出。

内容的提问来源于stack exchange,提问作者delphirules

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 09:25:34