c.File()不适合音视频流传输,因其将整个文件读入内存导致OOM且首帧延迟高;应改用http.ServeFile配合手动设置Header、Flush缓冲区及处理Range请求实现流式响应。

为什么 c.File() 不适合音视频流传输
直接用 c.File() 返回音视频文件,会把整个文件读进内存再发出去。对 100MB 的 MP4 或 HLS 切片来说,一次请求就占掉几百 MB 内存,并发一上来立刻 OOM。更糟的是,播放器需要边下边播(尤其是移动端),而 c.File() 必须等文件全读完才开始响应,首帧延迟动辄几秒甚至超时。
用 http.ServeFile + gin.Context.Writer 实现零拷贝流式响应
Gin 的 *gin.Context 底层包装了 http.ResponseWriter,可以直接复用 Go 标准库的流式能力。关键不是“让 Gin 做流式”,而是绕过它默认的缓冲逻辑,把文件句柄直通底层 TCP 连接。
- 必须手动设置
Content-Type,例如 MP4 是"video/mp4",WebM 是"video/webm" - 禁用 Gin 的默认写入缓冲:调用
c.Status(200)后立即用c.Writer.Flush() - 用
http.ServeFile(c.Writer, c.Request, filePath)替代c.File()—— 它内部用io.Copy分块读写,不加载全文 - 务必检查文件是否存在且可读,否则
http.ServeFile会静默返回 404,前端只看到空白响应
示例片段:
func videoStreamHandler(c *gin.Context) {
fileName := c.Param("name")
filePath := "./videos/" + fileName
if _, err := os.Stat(filePath); os.IsNotExist(err) {
c.AbortWithStatus(404)
return
}
c.Header("Content-Type", "video/mp4")
c.Status(200)
c.Writer.Flush() // 清空 Gin 缓冲区
http.ServeFile(c.Writer, c.Request, filePath)
}
Range 请求支持(断点续传 / 拖动进度条)必须自己处理
浏览器拖动视频进度条、暂停后继续播放,都依赖 HTTP Range 头。Gin 默认不解析也不响应这个头,http.ServeFile 也不支持。你得手写逻辑判断 c.Request.Header.Get("Range"),计算偏移量,用 os.OpenFile + io.CopyN 精确返回片段。
- 没处理 Range 时,所有视频都只能从头播,拖动无效
- 错误解析 Range(比如负数、越界)会导致 500 或静音
- 必须返回
206 Partial Content和正确的Content-Range头,否则播放器拒绝接收 - 大文件建议用
mmap或io.Seeker配合http.ServeContent,比反复Seek更稳
注意 Content-Length 和 Transfer-Encoding 冲突
如果你手动设置了 Content-Length,又没关掉 Gin 的自动 chunked 编码,Go HTTP Server 会报错并关闭连接。Gin 在 c.Writer 已写入内容后调用 WriteHeader 会触发 chunked,但流式传输通常要明确长度(尤其 iOS Safari 对 chunked 支持差)。
- 要么完全禁用 chunked:在
c.Writer.WriteHeader前确保没写任何 body,且显式设Content-Length - 要么彻底交给
http.ServeContent处理,它会根据是否有Range自动选 200/206 和编码方式 - 别混用
c.String()或c.JSON()和流式写入 —— 它们会提前触发 header 发送,后续Write失败
真正难的不是开头几行代码,而是 Range 边界校验、并发读取时的文件锁、以及不同播放器对 Content-Range 格式的容忍度差异 —— 这些细节不压测根本暴露不出来。


















