Golang Web开发中的大文件分片下载 Go语言HTTP Range协议深度应用

2次阅读

gohttp.servecontent 对大文件分片下载不友好,因其默认全量读入内存且不解析 range 头或设置 content-range 响应头,需手动实现 range 解析、206 状态码返回、响应头组装及 seek+copyn 分段读取,并确保 accept-ranges、content-range、content-Length 三者准确自洽。

Golang Web开发中的大文件分片下载 Go语言HTTP Range协议深度应用

Go 的 http.ServeContent 为什么对大文件分片下载不友好

它默认把整个文件读进内存做 io.Copy,遇到几十 GB 的文件,直接 OOM;更麻烦的是,它不主动解析 Range 请求头,也不设置 Content-Range 响应头,完全交由上层自己处理分片逻辑。

实际开发中,你得绕开 http.ServeContent,手动实现 Range 解析、状态码返回、响应头组装和分段读取 —— 这才是可控的起点。

  • 必须检查请求是否带 Range 头,没带就走完整响应流程(200 + 全量内容)
  • Content-Length 在分片时不能设为文件总大小,而得是当前片段字节数
  • 响应状态码必须是 206 Partial Content,不是 200 OK,否则浏览器/客户端不会续传
  • 别用 os.Open 后直接 io.Copy,要用 os.OpenFile 配合 file.Seek 跳转到起始偏移再读

如何安全解析 HTTP Range 请求头并校验边界

Range 头格式是 bytes=0-1023bytes=500-bytes=-500,但客户端可能乱填:负数起始、超长结束、非法字符、多个 range(multipart 不在本文讨论范围)。

Go 标准库没有现成解析函数,得自己写。核心是提取两个数字,再和文件大小比对,避免越界或 panic。

立即学习go语言免费学习笔记(深入)”;

  • strings.SplitN(header, "=", 2) 拆出 range 值,再按 - 拆两端
  • 空开头(如 -500)表示“末尾 N 字节”,需换算成 fileSize - 500
  • 空结尾(如 500-)表示“从 500 到末尾”,结束偏移就是 fileSize - 1
  • 最终得到的 start 才合法,否则返回 <code>416 Range Not Satisfiable

io.CopyN + file.Seek 实现高效分片读取

大文件不能一次性 io.Copy,也不能用 bufio.Reader 缓存全量——缓存大小难控,且 Seek 后缓冲区失效。最稳的方式是定位 + 定长读取。

io.CopyN 是关键:它能精确拷贝 N 字节,并自动处理底层 Reader 的多次 Read 调用,比手写循环更简洁可靠。

  • 先调 file.Seek(start, io.SeekStart) 定位到分片起点
  • 计算要读的字节数:end - start + 1
  • io.CopyN(w, file, int64(length)) 写入响应体,whttp.ResponseWriter
  • 注意:如果文件被并发修改(如日志轮转),file.Stat().Size() 和实际读取时长度可能不一致,建议加锁或用只读快照

chrome / curl 下载中断后不续传?检查这三个响应头

即使 Range 解析和读取都对,客户端仍可能忽略断点续传,常见原因是响应头缺失或错误。

浏览器判断能否续传,只看三个头:Accept-RangesContent-RangeContent-Length,缺一不可,且值必须自洽。

  • w.Header().Set("Accept-Ranges", "bytes") 必须显式设置,不能省略
  • Content-Range 格式必须是 bytes 0-1023/10000(注意空格和斜杠),不能写成 bytes 0-1023/ 或漏掉总大小
  • Content-Length 必须等于当前片段字节数(即 end - start + 1),不是文件总大小
  • 别在分片响应里写 Cache-Control: no-cache,某些旧版客户端会因此拒绝续传

最易被忽略的是:当文件大小动态变化(比如实时日志),Content-Range 中的总长度必须反映「本次响应时」的真实大小,而不是初始 stat 结果 —— 否则客户端校验失败就重下。

text=ZqhQzanResources