【问题标题】:Download file progressively using TIdHttp使用 TIdHttp 逐步下载文件
【发布时间】:2012-11-18 10:16:28
【问题描述】:

我想使用 TIdHttp (Indy10) 实现一个简单的 http 下载器。我从互联网上找到了两种代码示例。不幸的是,他们都没有100%让我满意。这是代码,我需要一些建议。


变体 1

var
  Buffer: TFileStream;
  HttpClient: TIdHttp;
begin
  Buffer := TFileStream.Create('somefile.exe', fmCreate or fmShareDenyWrite);
  try
    HttpClient := TIdHttp.Create(nil);
    try
      HttpClient.Get('http://somewhere.com/somefile.exe', Buffer); // wait until it is done
    finally
      HttpClient.Free;
    end;
  finally
    Buffer.Free;
  end;
end;

代码紧凑且非常容易理解。问题是它在下载开始时分配磁盘空间。另一个问题是我们不能直接在 GUI 中显示下载进度,除非代码在后台线程中执行(或者我们可以绑定 HttpClient.OnWork 事件)。


变体 2:

const
  RECV_BUFFER_SIZE = 32768;
var
  HttpClient: TIdHttp;
  FileSize: Int64;
  Buffer: TMemoryStream;
begin
  HttpClient := TIdHttp.Create(nil);
  try
    HttpClient.Head('http://somewhere.com/somefile.exe');
    FileSize := HttpClient.Response.ContentLength;

    Buffer := TMemoryStream.Create;
    try
      while Buffer.Size < FileSize do
      begin
        HttpClient.Request.ContentRangeStart := Buffer.Size;
        if Buffer.Size + RECV_BUFFER_SIZE < FileSize then
          HttpClient.Request.ContentRangeEnd := Buffer.Size + RECV_BUFFER_SIZE - 1
        else
          HttpClient.Request.ContentRangeEnd := FileSize;

        HttpClient.Get(HttpClient.URL.URI, Buffer); // wait until it is done
        Buffer.SaveToFile('somefile.exe');
      end;
    finally
      Buffer.Free;
    end;
  finally
    HttpClient.Free;
  end;
end;

首先我们从服务器查询文件大小,然后我们分段下载文件内容。检索到的文件内容将在完全接收后保存到磁盘。潜在的问题是我们必须向服务器发送多个 GET 请求。我不确定某些服务器(例如 megaupload)是否会限制特定时间段内的请求数。


我的期望

  1. 下载器应该只向服务器发送一个 GET 请求。
  2. 下载开始时不得分配磁盘空间。

感谢任何提示。

【问题讨论】:

标签: delphi http download indy idhttp


【解决方案1】:

变体 #1 是最简单的,也是 Indy 的用途。

关于磁盘分配问题,您可以从TFileStream 派生一个新类并覆盖其SetSize() 方法以不做任何事情。 TIdHTTP 仍会在适当的时候尝试预分配文件,但实际上不会分配任何磁盘空间。写入TFileStream 将根据需要增大文件。

关于状态报告,TIdHTTP 具有用于此目的的 OnWork... 事件。 OnWorkBeginAWorkCountMax 参数将是实际文件大小(如果已知)(响应未分块),如果未知则为 0。 OnWork 事件的AWorkCount 参数将是迄今为止已传输的累计字节数。如果文件大小已知,您可以通过简单地将AWorkCount 除以AWorkCountMax 并乘以100 来显示总百分比,否则只显示AWorkCount 值本身。如果要显示传输的速度,可以通过AWorkCount 值的差异和多个OnWork 事件之间的时间间隔来计算。

试试这个:

type
  TNoPresizeFileStream = class(TFileStream)
  procedure
    procedure SetSize(const NewSize: Int64); override;
  end;

procedure TNoPresizeFileStream.SetSize(const NewSize: Int64);
begin
end;

.

type
  TSomeClass = class(TSomething)
  ...
    TotalBytes: In64;
    LastWorkCount: Int64;
    LastTicks: LongWord;
    procedure Download;
    procedure HttpWorkBegin(ASender: TObject; AWorkMode: TWorkMode; AWorkCountMax: Int64);
    procedure HttpWork(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64);
    procedure HttpWorkEnd(ASender: TObject; AWorkMode: TWorkMode);
  ...
  end;

procedure TSomeClass.Download;
var
  Buffer: TNoPresizeFileStream;
  HttpClient: TIdHttp;
begin
  Buffer := TNoPresizeFileStream.Create('somefile.exe', fmCreate or fmShareDenyWrite);
  try
    HttpClient := TIdHttp.Create(nil);
    try
      HttpClient.OnWorkBegin := HttpWorkBegin;
      HttpClient.OnWork := HttpWork;
      HttpClient.OnWorkEnd := HttpWorkEnd;

      HttpClient.Get('http://somewhere.com/somefile.exe', Buffer); // wait until it is done
    finally
      HttpClient.Free;
    end;
  finally
    Buffer.Free;
  end;
end;

procedure TSomeClass.HttpWorkBegin(ASender: TObject; AWorkMode: TWorkMode; AWorkCountMax: Int64);
begin
  if AWorkMode <> wmRead then Exit;

  // initialize the status UI as needed...
  //
  // If TIdHTTP is running in the main thread, update your UI
  // components directly as needed and then call the Form's
  // Update() method to perform a repaint, or Application.ProcessMessages()
  // to process other UI operations, like button presses (for
  // cancelling the download, for instance).
  //
  // If TIdHTTP is running in a worker thread, use the TIdNotify
  // or TIdSync class to update the UI components as needed, and
  // let the OS dispatch repaints and other messages normally...

  TotalBytes := AWorkCountMax;
  LastWorkCount := 0;
  LastTicks := Ticks;
end;

procedure TSomeClass.HttpWork(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64);
var
  PercentDone: Integer;
  ElapsedMS: LongWord;
  BytesTransferred: Int64;
  BytesPerSec: Int64;
begin
  if AWorkMode <> wmRead then Exit;

  ElapsedMS := GetTickDiff(LastTicks, Ticks);
  if ElapsedMS = 0 then ElapsedMS := 1; // avoid EDivByZero error

  if TotalBytes > 0 then
    PercentDone := (Double(AWorkCount) / TotalBytes) * 100.0;
  else
    PercentDone := 0.0;

  BytesTransferred := AWorkCount - LastWorkCount;

  // using just BytesTransferred and ElapsedMS, you can calculate
  // all kinds of speed stats - b/kb/mb/gm per sec/min/hr/day ...
  BytesPerSec := (Double(BytesTransferred) * 1000) / ElapsedMS;

  // update the status UI as needed...

  LastWorkCount := AWorkCount;
  LastTicks := Ticks;
end;

procedure TSomeClass.HttpWorkEnd(ASender: TObject; AWorkMode: TWorkMode);
begin
  if AWorkMode <> wmRead then Exit;

  // finalize the status UI as needed...
end;

【讨论】:

  • 只是一些不相关的东西,但我在你的代码中看到:你使用 Ticks 来计算时间跨度。在这个例子中没什么大不了的。但我建议使用 TDateTime 来表示 StartTime 和 StopTime 并使用 TTimeSpan.Subtract(StopTime, StartTime) 来计算时间跨度。因为 MSDN 说如果系统连续运行 49.7 天,刻度将被重置。如果您的应用程序在服务器上运行,则持续时间可能计算错误。
  • 我故意选择不使用TDateTime,因为我不希望代码受到可能的时钟变化(夏令时、用户操作等)的影响。此外,代码正在计时事件之间发生的时间间隔,这永远不会接近LongWord 的 49.7 天限制。 GetTickDiff() 解释了每当 GetTickCount() 回绕到零时发生的回绕,所以这不是问题。
  • @RemyLebeau 就像我昨天说的那样,我再次尝试了 Indy,这次是版本 10,我觉得 v10 确实比 9 好得多,并且运行良好。我已经恢复,通过 HTTP POST 进行日志记录,我使用上面代码中的想法来做一些统计,所以是的 - Indy 很棒,谢谢!
【解决方案2】:

下面是一个例子,展示了如何使用组件 OnWork 来显示一个进度条:

Download a File from internet programatically with an Progress event using Delphi and Indy

您不必担心磁盘分配。分配的磁盘空间实际上并未写入,因此不会损坏您的磁盘。很高兴它被分配,这样另一个进程就不可能要求磁盘空间并让你用完空间!

【讨论】:

    【解决方案3】:

    不要忘记为 Variant 2 添加这个

     : Else HttpClient.Request.ContentRangeEnd := FileSize;
    

    替换

       if Buffer.Size + RECV_BUFFER_SIZE < FileSize then
      HttpClient.Request.ContentRangeEnd := Buffer.Size + RECV_BUFFER_SIZE - 1;
    

       if Buffer.Size + RECV_BUFFER_SIZE < FileSize then
      HttpClient.Request.ContentRangeEnd := Buffer.Size + RECV_BUFFER_SIZE - 1;
       Else HttpClient.Request.ContentRangeEnd := FileSize;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-01-10
      • 1970-01-01
      • 1970-01-01
      • 2013-03-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多