
PHP 如何用 header() 开启断点续传支持
断点续传不是靠 PHP 自己“记住进度”,而是靠 HTTP 协议配合客户端(比如浏览器、wget、curl)重新发起带 Range 头的请求。PHP 要做的是正确响应这个头,并返回对应字节段——核心就是设置 Accept-Ranges: bytes 和处理 Range 请求头。
常见错误是只加了 Content-Range 但没设 Accept-Ranges,导致客户端压根不发Range 请求;或者忽略 If-Range 校验,让过期缓存继续续传出错数据。
- 必须在输出文件内容前调用
header('Accept-Ranges: bytes') - 检查
$_SERVER['HTTP_RANGE']是否存在,格式是否为bytes=N-M,否则按完整文件响应 - 用
fopen()打开文件后,用fseek()跳转到起始位置,再用fread()读取指定长度,别用file_get_contents()全载入内存 - 响应状态码要分情况:
206 Partial Content(有 Range),200 OK(无 Range),不能一律用 200
为什么 fseek() + fread() 比readfile()更安全
readfile()会把整个文件一次性刷进输出缓冲区,对大文件(比如 2GB 视频)极易触发内存溢出或超时;而 fseek()+fread() 可以按块读取、边读边输出,内存占用稳定在几 KB。
另外 readfile() 无法精确控制起始偏移和长度,没法适配 Range 请求;哪怕你手动截断输出,HTTP 头里的 Content-Length 也容易算错,导致客户端解析失败。
立即学习“PHP 免费学习笔记(深入)”;
- 用
fopen($file, 'rb')以二进制模式打开,避免 Windows 下换行符干扰 - 计算
Content-Length时,用$end - $start + 1,不是$end - $start - 读取循环中每次
fread()不超过 8192 字节,防止阻塞太久被 Nginx/Apache 切断连接 - 记得
ob_end_clean()清空已有输出缓冲,否则header()会失败
Apache/Nginx 下 mod_xsendfile 或X-Accel-Redirect能替代 PHP 逻辑吗
能,而且更推荐。PHP 只负责校验权限、生成重定向头,真实文件传输交给 Web 服务器,既省 PHP 资源,又天然支持断点续传、gzip 压缩、缓存协商。
但要注意:启用后 PHP 不能再输出任何内容(包括空格、BOM),否则重定向失效;且路径需映射到 Web 服务器可访问的内部位置,不能暴露真实磁盘路径。
- Apache 需启用
mod_xsendfile,并配置XSendFile On和XSendFilePath /path/to/files - Nginx 用
X-Accel-Redirect时,需在 location 里配置internal,并确保响应头中的路径匹配内部别名 - PHP 中只需
header('X-Sendfile: /real/path/to/file.zip')(Apache)或header('X-Accel-Redirect: /nginx-alias/file.zip')(Nginx) - 别忘了仍要设置
Accept-Ranges: bytes——Web 服务器会自动处理 Range,但前提是这个头存在
客户端不发 Range 请求?检查这几个地方
即使 PHP 代码完全正确,客户端也可能因为缓存策略、HTTP 版本或工具限制,始终发完整请求。这不是 PHP 的问题,但排查时容易误判。
典型表现:用 wget -c 下载失败,Chrome 开发者工具 Network 面板里看不到 Range 请求头,curl -H "Range: bytes=0-1023" -I返回 200 而非 206。
- 确认客户端 HTTP 版本≥1.1(HTTP/1.0 不支持 Range)
- 检查响应头是否有
Cache-Control: no-store或Pragma: no-cache——某些旧客户端遇到强禁止缓存时,会放弃续传 - 用
curl -v -H "Range: bytes=0-1023" http://yoursite/file.zip直连测试,排除 CDN 或反向代理拦截 Range 头 - 部分 CDN(如 Cloudflare 免费版)默认剥离
Range头,需在页面规则里显式开启“Partial Request”支持
断点续传真正难的不是 PHP 怎么写,而是理清谁在哪个环节控制哪段字节——客户端决定要哪块,Web 服务器决定能不能给,PHP 只是中间那个校验权限、拼好头、不搞错偏移量的角色。漏掉 Accept-Ranges 头、用错读取方式、或者被 CDN 静默过滤 Range,都会让整套逻辑失效。






























