
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,都会让整套逻辑失效。











