带有文件流的多线程 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;
我遵循了很多使用 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;