Indy 10 IdHTTP实现的一些问题

Some Problems of Indy 10 IdHTTP Implementation

关于 IdHTTP 的 Indy 10,许多事情已经 运行 完美,但有一些事情在这里不太好。这就是为什么我再次需要你的帮助。

下载按钮 运行 完美。我正在使用以下代码:

void __fastcall TForm1::DownloadClick(TObject *Sender)
{
  MyFile = SaveDialog->FileName;
  TFileStream* Fist = new TFileStream(MyFile, fmCreate | fmShareDenyNone);
  Download->Enabled = false;
  Urlz = Edit1->Text;
  Url->Caption = Urlz;
   try
    {
     IdHTTP->Get(Edit1->Text, Fist);
     IdHTTP->Connected();
     IdHTTP->Response->ResponseCode = 200;
     IdHTTP->ReadTimeout = 70000;
     IdHTTP->ConnectTimeout = 70000;
     IdHTTP->ReuseSocket;
     Fist->Position = 0;
    }
   __finally
  {
    delete Fist;
    Form1->Updated();
  }
}

但是,"Cancel Resume"按钮仍然无法恢复中断的下载。意思是,尽管我使用了 IdHTTP->Request->Ranges 属性,但每次我调用 Get() 时它总是发回整个文件。

我使用以下代码:

void __fastcall TForm1::CancelResumeClick(TObject *Sender)
{
  MyFile = SaveDialog->FileName;;
  TFileStream* TFist = new TFileStream(MyFile, fmCreate | fmShareDenyNone);

 if (IdHTTP->Connected() == true)
   {
    IdHTTP->Disconnect();
    CancelResume->Caption = "RESUME";
    IdHTTP->Response->AcceptRanges = "Bytes";
   }
 else
   {
  try {
   CancelResume->Caption = "CANCEL";
   // IdHTTP->Request->Ranges == "0-100";
   // IdHTTP->Request->Range = Format("bytes=%d-",ARRAYOFCONST((TFist->Position)));
   IdHTTP->Request->Ranges->Add()->StartPos = TFist->Position;
   IdHTTP->Get(Edit1->Text, TFist);
   IdHTTP->Request->Referer = Edit1->Text;
   IdHTTP->ConnectTimeout = 70000;
   IdHTTP->ReadTimeout = 70000;
    }
  __finally {
    delete TFist;
   }
}

同时,通过使用FormatBytes函数,found here已经能够只显示下载文件的大小。但仍然无法确定下载速度或传输速度。

我正在使用以下代码:

void __fastcall TForm1::IdHTTPWork(TObject *ASender, TWorkMode AWorkMode, __int64 AWorkCount)
  {
   __int64 Romeo = 0;
   Romeo = IdHTTP->Response->ContentStream->Position;
   // Romeo = AWorkCount;
   Download->Caption = FormatBytes(Romeo) + " (" + IntToStr(Romeo) + " Bytes)";
   ForSpeed->Caption = FormatBytes(Romeo);
   ProgressBar->Position = AWorkCount;
   ProgressBar->Update();
   Form1->Updated();
 }

请指教并举个例子。我们将不胜感激!

在您的 DownloadClick() 方法中:

  1. 调用 Connected() 是没有用的,因为您没有对结果做任何事情。也不能保证连接将保持连接状态,因为服务器可以发送 Connection: close 响应 header。我在您的代码中没有看到任何请求 HTTP keep-alives 的内容。让 TIdHTTP 为您管理连接。

  2. 您正在将 Response->ResponseCode 强制为 200。不要那样做。尊重服务器实际发送的响应代码。没有抛出异常就说明响应成功了,不管是200还是206。

  3. 您正在读取 ReuseSocket 属性 值并忽略它。

  4. 关闭文件前无需将Fist->Position属性重置为0

话虽如此,您的 CancelResumeClick() 方法有很多问题。

  1. 您在打开文件时使用了 fmCreate 标志。如果该文件已经存在,您将从头开始覆盖它,因此 TFist->Position 将始终为 0。使用 fmOpenReadWrite 代替,这样现有文件将打开 as-is。然后你必须寻找到文件的末尾以提供正确的 PositionRanges header.

  2. 您依靠套接字的 Connected() 状态来做出决定。不要那样做。连接可能在上一个响应之后就消失了,或者可能在发出新请求之前已经超时并关闭了。该文件仍然可以通过任何一种方式恢复。 HTTP 是无状态的。套接字在请求之间保持打开状态还是关闭状态都没有关系。每个请求都是 self-contained。使用先前响应中提供的信息来管理下一个请求。不是套接字状态。

  3. 您正在修改 Response->AcceptRanges 属性 的值,而不是使用之前响应提供的值。服务器告诉你文件是否支持恢复,所以你必须记住那个值,或者在尝试恢复下载之前查询它。

  4. 当您实际调用 Get() 时,服务器可能会或可能不会尊重所请求的 Range,这取决于所请求的文件是否支持字节范围。如果服务器以 206 的响应代码进行响应,则接受请求的范围,并且服务器仅发送请求的字节,因此您需要将它们附加到现有文件中。但是,如果服务器响应的响应代码为 200,则服务器正在从头开始发送整个文件,因此您需要用新字节替换现有文件。你没有考虑到这一点。

在您的 IdHTTPWork() 方法中,为了计算 download/transfer 速度,您必须跟踪每次事件触发之间实际传输的字节数。当事件被触发时,保存当前的 AWorkCount 和滴答计数,然后在下次触发事件时,您可以比较新的 AWorkCount 和当前的滴答来知道已经过去了多少时间以及如何许多字节被传输。根据这些值,您可以计算出速度,甚至可以估计剩余时间。

至于你的进度条,你不能单独使用AWorkCount来计算一个新的位置。仅当您在 OnWorkBegin 事件中将进度条的 Max 设置为 AWorkCountMax 时才有效,并且在下载开始之前并不总是知道该值。您需要考虑正在下载的文件的大小,是重新下载还是正在恢复,恢复期间请求了多少字节等。因此,显示进度条涉及更多工作HTTP 下载。

现在,回答你的两个问题:

How to retrieve and save the download file to a disk by using its original name?

由服务器在Content-Dispositionfilename参数中提供 header,and/or在name的参数中[=38] =] header。如果服务器未提供任何值,您可以使用您请求的 URL 中的文件名。 TIdHTTP 有一个 URL 属性 提供最后请求的解析版本 URL.

但是,由于您是在发送下载请求之前在本地创建文件,因此您必须使用临时文件名创建一个本地文件,然后在下载完成后重命名该本地文件。否则,在使用该文件名创建本地文件之前,使用 TIdHTTP.Head() 确定真实文件名(您也可以使用它确定是否支持恢复),然后使用 TIdHTTP.Get() 下载到该本地文件。否则,使用 TMemoryStream 而不是 TFileStream 将文件下载到内存,然后在完成后使用所需的文件名保存。

when I click http://get.videolan.org/vlc/2.2.1/win32/vlc-2.2.1-win32.exe then the server will process requests to its actual url. http://mirror.vodien.com/videolan/vlc/2.2.1/win32/vlc-2.2.1-win32.exe. The problem is that IdHTTP will not automatically grab through it.

这是因为 VideoLan 没有使用 HTTP 重定向将客户端发送到真正的 URL(TIdHTTP 支持 HTTP 重定向)。 VideoLan 正在使用 HTML 重定向(TIdHTTP 不支持 HTML 重定向)。当网络浏览器下载第一个 URL 时,会在真正的下载开始之前显示一个 5 秒的倒计时计时器。因此,您必须手动检测服务器是否向您发送HTML页面而不是真正的文件(看TIdHTTP.Response.ContentType属性),解析HTML确定真正的URL,然后下载.这也意味着您不能将第一个 URL 直接下载到您的目标本地文件中,否则您会损坏它,尤其是在恢复期间。您必须首先将服务器的响应缓存到临时文件或内存中,以便您可以在决定如何对其进行操作之前对其进行分析。这也意味着你必须记住真正的 URL 才能恢复,你无法使用原始倒计时 URL.

恢复下载

试试下面的方法。它没有考虑上面提到的所有内容(特别是 speed/progress 跟踪、HTML 重定向等),但应该让你更接近一点:

void __fastcall TForm1::DownloadClick(TObject *Sender)
{
    Urlz = Edit1->Text;
    Url->Caption = Urlz;

    IdHTTP->Head(Urlz);
    String FileName = IdHTTP->Response->RawHeaders->Params["Content-Disposition"]["filename"];
    if (FileName.IsEmpty())
    {
        FileName = IdHTTP->Response->RawHeaders->Params["Content-Type"]["name"];
        if (FileName.IsEmpty())
            FileName = IdHTTP->URL->Document;
    }

    SaveDialog->FileName = FileName;
    if (!SaveDialog->Execute()) return;

    MyFile = SaveDialog->FileName;
    TFileStream* Fist = new TFileStream(MyFile, fmCreate | fmShareDenyWrite);
    try
    {
        try
        {
            Download->Enabled = false;
            Resume->Enabled = false;

            IdHTTP->Request->Clear();
            //...

            IdHTTP->ReadTimeout = 70000;
            IdHTTP->ConnectTimeout = 70000;

            IdHTTP->Get(Urlz, Fist);
        }
        __finally
        {
            delete Fist;
            Download->Enabled = true;
            Updated();
        }
    }
    catch (const EIdHTTPProtocolException &)
    {
        DeleteFile(MyFile);
        throw;
    }
}

void __fastcall TForm1::ResumeClick(TObject *Sender)
{
    TFileStream* Fist = new TFileStream(MyFile, fmOpenReadWrite | fmShareDenyWrite);
    try
    {
        Download->Enabled = false;
        Resume->Enabled = false;

        IdHTTP->Request->Clear();
        //...

        Fist->Seek(0, soEnd);
        IdHTTP->Request->Ranges->Add()->StartPos = Fist->Position;
        IdHTTP->Request->Referer = Edit1->Text;

        IdHTTP->ConnectTimeout = 70000;
        IdHTTP->ReadTimeout = 70000;

        IdHTTP->Get(Urlz, Fist);
    }
    __finally
    {
        delete Fist;
        Download->Enabled = true;
        Updated();
    }
}

void __fastcall TForm1::IdHTTPHeadersAvailable(TObject*Sender, TIdHeaderList *AHeaders, bool &VContinue)
{
    Resume->Enabled = ( ((IdHTTP->Response->ResponseCode == 200) || (IdHTTP->Response->ResponseCode == 206)) && TextIsSame(AHeaders->Values["Accept-Ranges"], "bytes") );

    if ((IdHTTP->Response->ContentStream) && (IdHTTP->Request->Ranges->Count > 0) && (IdHTTP->Response->ResponseCode == 200))
        IdHTTP->Response->ContentStream->Size = 0;
}

@罗密欧:

此外,您可以尝试使用以下函数来确定真正的下载文件名。

我已经根据 RRUZ'function 将其翻译成 C++。到目前为止一切顺利,我也在我的简单 IdHTTP 下载程序中使用它。

但是,这个翻译结果当然还需要Remy Lebeau,RRUZ,或者这里的任何其他大师的价值改进输入。

String __fastcall GetRemoteFileName(const String URI)
{
    String result;
    try
    {
        TIdHTTP* HTTP = new TIdHTTP(NULL);
        try
        {
            HTTP->Head(URI);
            result = HTTP->Response->RawHeaders->Params["Content-Disposition"]["filename"];
            if (result.IsEmpty())
            {
                result = HTTP->Response->RawHeaders->Params["Content-Type"]["name"];
                if (result.IsEmpty())
                    result = HTTP->URL->Document;
            }
        }
        __finally
        {
            delete HTTP;
        }
    }
    catch(const Exception &ex)
    {
        ShowMessage(const_cast<Exception&>(ex).ToString());
    }

    return result;
}