ThinkPHP断点续传需手动处理Range请求:提取HTTP_RANGE头,用fopen('c')和fseek定位写入,校验Content-Range一致性,禁用file_put_contents追加,配置Nginx支持分片,严防并发写错乱。

ThinkPHP 上传中断后如何识别并续传 Range 请求
ThinkPHP 原生不解析 Range 请求头,也不暴露断点续传所需的文件偏移和长度信息。你看到的“上传一半失败、重试就从头开始”,根本原因就是服务端压根没读 Range,更不会返回 206 Partial Content 或校验已存在分片。
实操上必须手动接管上传入口,绕过 think\File 的默认处理流程:
- 在控制器里用
$_SERVER['HTTP_RANGE']提取范围,例如bytes=1024000-或bytes=0-1023999/5000000 - 用
fopen($filePath, 'c')(非覆盖模式)打开目标文件,再用fseek()定位到指定偏移写入 - 务必校验
Content-Range头中的总长度是否与预期一致,防止客户端伪造 - 不要依赖
$_FILES——大文件走的是php://input流,$_FILES只适用于小文件表单上传
为什么 upload_max_filesize 和 post_max_size 在断点续传里形同虚设
这两个 PHP 配置只约束完整上传的单次请求体大小,而断点续传每次只发一个分片(比如 5MB),远低于限制值。真正卡住你的,是 max_execution_time(超时)、memory_limit(大 buffer 占内存)和 Nginx 的 client_max_body_size(它默认只认整个请求体,不理解分片)。
关键动作:
立即学习“PHP免费学习笔记(深入)”;
- Nginx 配置中去掉对
Content-Length的强依赖,加underscores_in_headers on;允许接收Content-Range - 把
fastcgi_read_timeout调高到 300s 以上,避免分片写入中途被关连接 - PHP 层禁用自动输出缓冲(
ob_end_clean()),否则header()发送206会失败
file_put_contents 直接追加不安全,该用 fseek + fwrite
用 file_put_contents($path, $data, FILE_APPEND) 看似省事,但它无法精确控制写入位置,且在并发上传同一文件时极易导致数据错乱(两个请求同时 FILE_APPEND,最终偏移不可控)。
正确做法是显式控制文件指针:
if ($range = $_SERVER['HTTP_RANGE'] ?? null) {
preg_match('/bytes=(\d+)-(\d+)?\/(\d+)?/', $range, $matches);
$start = (int)$matches[1];
$fp = fopen($filePath, 'c'); // c 模式不截断,支持随机写
fseek($fp, $start);
fwrite($fp, $rawInputData);
fclose($fp);
}
注意:'c' 模式比 'r+' 更稳妥,后者要求文件必须已存在;而 'c' 不存在则创建,存在也不清空,符合续传前提。
前端传 Content-Range,后端不校验就等于裸奔
很多实现只检查 Range 头,却忽略客户端实际发送的 Content-Range: bytes 1024000-2047999/5000000 是否匹配请求体长度、是否越界、是否重叠。攻击者可故意填错总大小或跳过中间块,诱导服务端写坏文件。
必须做的三件事:
- 用
strlen(file_get_contents('php://input'))获取真实 body 长度,对比Content-Range中的区间跨度 - 检查
$start + $length ,防止写超限 - 记录每个文件的已接收分片范围(建议存 Redis),拒绝重复或冲突的
Range
断点续传不是加个 Range 头就能跑通的事——服务端得像数据库一样管理偏移、校验一致性、防并发写崩。漏掉任意一环,上传完成的文件大概率打不开。


















