TidHttp文件下载抛出内存不足异常

4
考虑以下使用Indy组件从互联网下载文件的代码:
```Delphi procedure TForm1.Button1Click(Sender: TObject); var IdHTTP: TIdHTTP; begin IdHTTP := TIdHTTP.Create(nil); try IdHTTP.Get('http://www.example.com/file.txt', 'localfile.txt'); finally IdHTTP.Free; end; end; ```
该代码创建了一个TIdHTTP对象,然后使用Get方法从指定的URL下载文件并将其保存在本地文件中。
procedure TForm26.Button1Click(Sender: TObject);
var
  vFileStream : TStream;
begin
  DeleteFile('C:\test.exe');
  vFileStream := TFileStream.Create('C:\test.exe', fmCreate);
  IdHTTP1.Get('SomeUrl', vFileStream);
  vFileStream.Free;
end;

我遇到了内存溢出异常。问题在于我使用的是 TFileStream,但写入的字节不会直接写入磁盘,而是留存在内存中,直到 get 结束为止。
我正在尝试下载一个非常大的文件。
有人知道如何在下载大文件时避免内存溢出异常吗?
Delphi 2010 和最新的 Indy 10(来自 Indy's SVN)。 更新: 这不是一个 FileStream 的问题。这是一个 Indy 的问题。Indy 在写入流之前以某种方式将文件缓存在内存中。

如果有影响的话,需要知道Delphi版本和Indy版本是多少? - David Heffernan
Delphi 2010和最新的Indy 10来自Indy的SVN。 - Rafael Colucci
代码告诉你什么?IdHTTP1.Get在写入文件之前是否将整个文件下载到内存中? - David Heffernan
@DavidHeffernan 我不知道,我还没有看过Indy的代码。 - Rafael Colucci
如果数据已压缩(Indy尚不支持HTTP的流解压缩),或者如果数据是HTML且TIdHTTP.HTTPOptions属性不包含hoNoParseMetaHTTPEquiv标志,则TIdHTTP会将整个文件下载到内存中。 - Remy Lebeau
@RemyLebeau,hoNoParseMetaHTTPEquiv标志是做什么用的? - Leonardo Herrera
3个回答

5
TIdHTTP会将整个文件下载到内存中,如果数据被压缩或者数据为HTML且TIdHTTP.HTTPOptions属性不包含hoNoParseMetaHTTPEquiv标志。然而,Indy目前还不支持HTTP的流式解压缩(FTP除外),因此TIdHTTP会在将其解压到文件之前将整个压缩数据缓存在内存中。
有时需要解析HTML,特别是当HTML通过<meta>标签覆盖HTTP头值的新值时,最重要的是数据的Charset值,这样TIdHTTP就可以使用正确的字符集对数据进行解码,以便将数据作为String返回给用户代码。启用hoNoParseMetaHTTPEquiv标志会禁用该解析,因此也会禁用HTML数据的任何缓存(除非也使用了压缩)。

1
有没有什么解决办法?肯定有,因为这似乎有些限制。 - David Heffernan
2
为了首先处理压缩数据,您需要将一个派生自TIdZLibCompressorBase的组件分配给TIdHTTP.Compressor属性。这反过来让TIdHTTP自动通知服务器支持压缩响应(除非您手动使用TIdHTTP.Request.AcceptEncodings属性覆盖)。因此,解决方法就是简单地删除/禁用压缩器,如果实际上证明这是实际问题的话。我已经在Indy的问题跟踪器中添加了票据以支持流式解压缩在未来的版本中。 - Remy Lebeau
我记得在某个地方读到过,无论是Netscape还是Internet Explorer,它们都会下载HTML文档的第一行,如果其中包含可怕的charset元标记,则重新加载整个页面以使用新的字符集。似乎与此有关。 - Leonardo Herrera
因此,总结起来,答案就是不使用压缩来避免将整个内容存储在内存中? - Leonardo Herrera
@LeonardoHerrera:TIdHTTP在完全下载字符数据之前不会对其进行解码,因此如果存在字符集更改,则无需重新加载页面。在数据下载完成后,将解析charset标记,如果检测到,则使用该字符集进行解码,否则将使用HTTP头中指定的字符集。 - Remy Lebeau

3
我找到了问题所在。在服务器端,我使用了Indys的ServeFile函数。该函数会检查是否指定了Content-Type,如果没有,则自动检测Content-Type。问题在于我没有更改Content-Type,默认情况下是text/html。更改内容类型使客户端直接写入流。
我认为serveFile函数应始终设置正确的Content-Type,以避免这种问题。
在客户端,我发现了这段代码,对我很有帮助:
  LParseHTML := IsContentTypeHtml(AResponse) and Assigned(AResponse.ContentStream) and not (hoNoParseMetaHTTPEquiv in FOptions);
  LCreateTmpContent := LParseHTML and not (AResponse.ContentStream is TCustomMemoryStream);

我无法使用最新的Indy 10 SVN快照来复现此问题。当触发OnCommandGet事件时,尚未分配默认的ContentType,因此ServeFile()应该分配文件扩展名对应的任何ContentType。 只有在TIdResponseHeaderInfo构造函数中,TIdHTTPServer才会分配默认的'text/html' ContentType,但是TIdHTTPResponseInfo构造函数会在此后将其重置为空字符串。 - Remy Lebeau
没事了,我最终还是能够复现它了。看起来很随机,有时默认的 ContentType 是空白的,有时是 text/html。我会继续调试。 - Remy Lebeau
@RemyLebeau 你认为这是一个 bug 吗? - Rafael Colucci
1
忽略我的上一条评论。当我看到空值时,我不小心看错了 TIdHTTPRequestInfo 而不是 TIdHTTPResponseInfo。所以是的,响应的默认值始终为 text/html,因此我将更新 ServeFile() 来解决这个问题。 - Remy Lebeau
@RemyLebeau 总结一下:这是serveFile函数中的一个bug,对吗? - Rafael Colucci
1
@RafealColucci:不,这不是ServeFile()中的错误。在调用ServeFile()之前,TIdHTTPServer允许用户分配自定义ContentType。只是TIdHTTPServer本身设置了自己的非空默认值,所以ServeFile()不知道区别。我现在已经删除了非空默认值(并更新了WriteHeader()以考虑到这一点,以便它可以与期望将默认值发送到客户端的用户代码保持向后兼容)。 - Remy Lebeau

1

我不知道如何做那个。 - Rafael Colucci
@RafaelColucci - 这里是 Remy Lebeau (TeamB) 的示例,网址为 http://www.delphigroups.info/2/5/211924.html。 - Pol
@Pol 我知道这可能是一个解决方案,但那不是我需要的。如果我采用你的解决方案,我将不得不在我的代码中进行大量更改,而且我无法相信indys不能直接写入流。 - Rafael Colucci

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接