功能说明
视频拖动进度、断点续传和大文件下载都依赖 HTTP 范围请求。手写 Range 解析容易在开放区间、多个区间和非法范围上出错,标准库的 http.ServeContent 已经实现了完整语义:校验 Range、返回 206 与 Content-Range、处理 If-Modified-Since 和 If-None-Match,并在范围非法时返回 416。
示例把文件访问限制在指定根目录内,先把请求路径规范化,再拼接到根目录,最后确认结果仍在根目录下,防止路径穿越读取任意文件。
关键代码
path.Clean 把双点和多余斜杠消解,filepath.FromSlash 转换为当前系统分隔符。前缀检查使用根目录加路径分隔符,避免相似前缀的兄弟目录被误放行。os.Open 之后再次确认不是目录,因为 ServeContent 需要可寻址的文件。ServeContent 自动设置 Content-Length、Accept-Ranges,并根据请求头生成 206 或 304 响应。main 用临时目录和测试服务器发起 bytes=6-10 的请求。
使用注意
ServeContent 依赖文件实现 io.ReadSeeker,压缩包装或加密流需要额外处理。路径检查只做字符串前缀,若根目录本身是符号链接,应先用 filepath.EvalSymlinks 解析。生产环境还应限制文件大小、设置缓存头和内容类型,并对下载做鉴权。多区间请求会返回 multipart 响应,客户端需要按边界解析。HEAD 请求由 ServeContent 自动处理。
验证方式
文件内容为 hello range world,共十七字节。请求第六到第十字节返回状态 206,Content-Range 为 bytes 6-10/17,响应体为 range。把 Range 改为超出文件长度会得到 416;不传 Range 则返回 200 和完整内容,并带有 Accept-Ranges 响应头。
代码示例
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path"
"path/filepath"
"strings"
)
func fileHandler(root string) http.HandlerFunc {
absoluteRoot, err := filepath.Abs(root)
if err != nil {
panic(err)
}
return func(w http.ResponseWriter, r *http.Request) {
cleaned := path.Clean("/" + r.URL.Path)
full := filepath.Join(absoluteRoot, filepath.FromSlash(strings.TrimPrefix(cleaned, "/")))
if !strings.HasPrefix(full, absoluteRoot+string(os.PathSeparator)) {
http.Error(w, "invalid path", http.StatusBadRequest)
return
}
file, err := os.Open(full)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
defer file.Close()
info, err := file.Stat()
if err != nil || info.IsDir() {
http.Error(w, "not found", http.StatusNotFound)
return
}
http.ServeContent(w, r, info.Name(), info.ModTime(), file)
}
}
func main() {
root, err := os.MkdirTemp("", "range-server-*")
if err != nil {
panic(err)
}
defer os.RemoveAll(root)
if err := os.WriteFile(filepath.Join(root, "report.txt"), []byte("hello range world"), 0o644); err != nil {
panic(err)
}
server := httptest.NewServer(fileHandler(root))
defer server.Close()
request, err := http.NewRequest(http.MethodGet, server.URL+"/report.txt", nil)
if err != nil {
panic(err)
}
request.Header.Set("Range", "bytes=6-10")
response, err := http.DefaultClient.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
panic(err)
}
fmt.Println("status:", response.StatusCode)
fmt.Println("content-range:", response.Header.Get("Content-Range"))
fmt.Println("body:", string(body))
}