【问题标题】:Extending Golang's http.Resp.Body to handle large files扩展 Golang 的 http.Resp.Body 来处理大文件
【发布时间】:2017-09-14 16:39:19
【问题描述】:

我有一个客户端应用程序,它将 http 响应的全部内容读入缓冲区并对其执行一些处理:

body, _ = ioutil.ReadAll(containerObject.Resp.Body)

问题在于该应用程序在嵌入式设备上运行,因此太大的响应会填满设备 RAM,导致 Ubuntu 终止该进程。

为了避免这种情况,如果文档太大,我会检查 content-length 标头并绕过处理。然而,一些服务器(我在看你,微软)在没有设置内容长度的情况下发送非常大的 html 响应并导致设备崩溃。

我能看到解决此问题的唯一方法是将响应正文读取到一定长度。如果达到此限制,则可以创建一个新的读取器,它首先流式传输内存缓冲区,然后继续从原始 Resp.Body 读取。理想情况下,我会将这个新读取器分配给 containerObject.Resp.Body,这样调用者就不会知道其中的区别。

我是 GoLang 的新手,不知道如何编写此代码。任何建议或替代解决方案将不胜感激。

编辑 1:调用者需要一个 Resp.Body 对象,因此解决方案需要与该接口兼容。

编辑 2:我无法解析文档的小块。要么处理整个文档,要么将其原封不动地传递给调用者,而不将其加载到内存中。

【问题讨论】:

  • 不设置 Content-Length 是相当标准的,因为大多数大型响应将使用分块编码。
  • 为什么要尝试有条件地缓冲部分响应,而不是始终使用io.Reader 接口?
  • 为了进一步推荐@JimB,我会开始看这个:stackoverflow.com/questions/31857891/…这也可能有用:tip.golang.org/pkg/encoding/json/#Decoder.Token
  • 从 Resp.Body 读取任何内容后,您必须将其缓冲在内存中(如果您想对它做任何事情,那就是),因为您正在从网络流中读取。这意味着大响应被完全读入内存并使设备崩溃。现在另一部分是这个函数的调用者期望 Resp.Body 是完整的——就像它是直接从 http 对象中读取的一样。所以我需要向它发送一些与该对象一样的东西,但不会尝试将大型文档完全读入 RAM。
  • 感谢克里斯蒂安的建议。不幸的是,我不能一次处理小批量的文档。这是一个全有或全无的事情。如果我看不到整个文档,那么我想原封不动地传递它。我将编辑问题以反映这一点。

标签: go


【解决方案1】:

如果您需要读取响应正文的一部分,然后为其他调用者就地重构它,您可以使用io.MultiReaderioutil.NopCloser 的组合

resp, err := http.Get("http://google.com")
if err != nil {
    return err
}
defer resp.Body.Close()

part, err := ioutil.ReadAll(io.LimitReader(resp.Body, maxReadSize))
if err != nil {
    return err
}

// do something with part

// recombine the buffered part of the body with the rest of the stream
resp.Body = ioutil.NopCloser(io.MultiReader(bytes.NewReader(part), resp.Body))

// do something with the full Response.Body as an io.Reader

如果您不能 defer resp.Body.Close() 因为您打算在完整读取响应之前返回响应,则需要扩充替换正文,以便 Close() 方法适用于原始正文。与其将ioutil.NopCloser 用作io.ReadCloser,不如创建自己的引用正确方法调用的方法。

type readCloser struct {
    io.Closer
    io.Reader
}

resp.Body = readCloser{
    Closer: resp.Body,
    Reader: io.MultiReader(bytes.NewReader(part), resp.Body),
}

【讨论】:

  • 这看起来正是我所需要的。我会尽快实施并回复您。
  • 关于这个的一个问题。如果将 resp.Body 替换为新对象,“defer resp.Body.Close()”将关闭新对象,同时保持原始主体打开,从而导致内存泄漏。你知道这会不会有同样的问题?还是会在原始的 resp.Body 上调用 Close?
  • 这里的defer resp.Body.Close() 是在原始resp.Body 上进行评估的;稍后更换它不会影响它。唯一需要注意的是,您不能将resp 返回到任何消耗身体的东西,因为身体将在那时关闭。如果调用者要再次阅读正文,我仍然不明白先阅读正文有什么好处。您也不会阻止下一个调用者阅读过多并且内存不足。为什么不首先限制响应大小?
  • 奇怪的是,替换 resp.Body.Close 并没有按预期关闭原始的 resp.Body。不知道为什么,但在 Golang 论坛上有一个帖子,我能够用我自己的代码确认这一点(一旦原始 resp.Body 在重新分配之前关闭,它就不再显示泄漏)。
  • 至于评论的后半部分,设备本身会处理 http 响应,然后通过网络将其传递给客户端,客户端将其直接写入磁盘。我很确定这种情况主要发生在微软传输二进制更新并将它们标记为 html 页面时。
猜你喜欢
  • 2013-10-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多