带有文件流的多线程 TIdHTTP 文件下载导致文件损坏

Multithreaded TIdHTTP file download with filestream results in corrupted file

我遵循了很多使用 TIdHTTP 组件创建多线程文件下载的示例,但我遇到了以下问题:

但首先,我的代码的简化版本。

这部分计算需要下载的文件大小:

TIdHTTP* tcpClient = new TIdHTTP(NULL);
tcpClient->ProtocolVersion = pv1_1;
tcpClient->Head(URL);
__int64 LSize = tcpClient->Response->ContentLength;
System::Classes::TFileStream *STFile = new System::Classes::TFileStream(FFileName, fmCreate);
try
{
    STFile->Size = LSize;
}
__finally
{
    delete STFile;
};
delete tcpClient;

Next 是我的 MainForm 多次调用的线程的 Execute 方法的一部分。 FStartPos 是该线程在文件中的起始位置(i.o.w 第一个线程从位置 0 开始),FEndPos 是需要检索的块的结尾:

TFileStream *LStream = new TFileStream(FFileName, fmOpenWrite | fmShareDenyNone);
LHttpClient = new TIdHTTP(NULL);
LHttpClient->ProtocolVersion = pv1_1;
LHttpClient->Request->BasicAuthentication = true;
LHttpClient->Request->Username = FUsername;
LHttpClient->Request->Password = FPassword;
try
{
    LHttpClient->OnWork = ReceiveDataEvent;
    try
    {
    try
    {
      LStream->Seek(FStartPos, TSeekOrigin::soBeginning);
      LHttpClient->Request->Range = "bytes="+UnicodeString(FStartPos)+"-"+UnicodeString(FEndPos);
      LHttpClient->Get(URL, LStream);
      IsFin = true;
    } catch(Exception &e)
    {
        // log the error
    }
    }
    __finally
    {
        LHttpClient->Disconnect();
        delete LHttpClient;
    }
}
__finally
{
    delete LStream;
}

当我尝试下载例如一个 87MB 的文件,我创建了 5 个下载线程,所以每个线程应该下载 17MB 奇数。我看到的是文件被创建(并报告大小为 87MB)。通常线程 3 首先完成,文件大小跳到 52MB,然后线程 1 完成,文件大小跳到 17MB,最后线程 4 完成,文件现在为 69MB(而不是启动时的 87MB)。

我有一种感觉,要么我没有正确使用 TFileStream,要么以一种不适合使用它的方式使用它。

我的问题是,我的代码有错吗?或者是否有更好/更合适的方式从多个线程写入单个文件,但每个线程都在自己的块中?

(我是 运行 C++ Builder 10.1,我认为内置 Indy 10)

提前感谢您的任何建议。

-G-

问题的根源是您的工作线程在创建 TFileStream:

时使用了 fmOpenWrite 标志

Open the file for writing only. Writing to the file completely replaces the current contents.

这意味着每个线程都在清除文件中已有的内容并将其大小重置为 0!

对于您的尝试,您需要使用 fmOpenReadWrite 来代替:

Open the file to modify the current contents rather than replace them.

话虽这么说,但还有一些其他事项需要考虑:

  • Head()退出后,请确认Response->AcceptRange属性设置为"bytes"。如果不是,请勿产生多个线程来下载文件,因为服务器将忽略您分配给 Request->Range 属性 的任何内容,并且每个请求都会完整下载整个文件。

  • 当您为下载范围生成多个线程时,在 Get() 收到响应 headers 后,您应该检查 Response->ContentRange... 属性以确保您正在得到您的期望,并处理服务器可能向您发送的范围小于您请求的范围的情况。例如,您可以检查 OnHeadersAvailable 事件,如果发生意外情况,您可以在任何内容写入 TFileStream 之前取消下载(通过引发异常)。这对于范围处理尤其重要,以防您收到 200 响应(接收整个文件)而不是 206 响应(仅接收文件的范围)。

  • Request->Range 属性 已弃用,您应该使用 Request->Ranges 属性 代替:

    //LHttpClient->Request->Range = "bytes="+UnicodeString(FStartPos)+"-"+UnicodeString(FEndPos);      
    TIdEntityRange *range = LHttpClient->Request->Ranges->Add();
    range->StartPos = FStartPos;
    range->EndPos = FEndPos;