本文目录导读:

**
《PHP大文件切片上传合并:从原理到实战,彻底解决超大文件传输瓶颈》
目录导读
- 为什么需要切片上传?——传统上传的痛点
- 切片上传的核心原理(HTTP协议与前端分片)
- PHP后端接口设计(接收、校验、临时存储)
- 合并策略详解(顺序合并、断点续传、秒传判断)
- 完整代码实战(前端+PHP后端)
- 常见问题与性能优化(内存、并发、安全)
- 高频问答(QA)
为什么需要切片上传?——传统上传的痛点
在Web开发中,上传大文件(如视频、备份包、设计稿)一直是个棘手问题,传统<form>上传方式存在三大致命缺陷:
- 服务器内存压力:PHP默认
upload_max_filesize为2M,即使调大,当500MB文件进入$_FILES时,PHP会一次性读取到内存,直接导致内存溢出(memory_limit爆掉)。 - 网络中断重来:一旦网络抖动,整个文件上传失败,用户只能重新上传,体验极差。
- 超时限制:Nginx/Apache默认请求超时(如30秒),大文件必然超时。
而切片上传(Chunked Upload)的核心思想是:化整为零,聚零为整,前端将文件按固定大小(如2MB)切割成N个Blob,逐个发送到服务器;后端接收所有分片后,按顺序合并成原文件。
切片上传的核心原理
1 前端分片逻辑
使用JavaScript的Blob.slice()方法,将文件对象切割,关键代码:
const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB
let start = 0;
while (start < file.size) {
let chunk = file.slice(start, start + CHUNK_SIZE);
// 通过FormData发送chunk,携带自定义参数
formData.append('chunk', chunk);
formData.append('chunkIndex', index);
formData.append('totalChunks', totalChunks);
formData.append('fileName', file.name);
// 使用XMLHttpRequest或fetch异步上传
start += CHUNK_SIZE;
}
2 HTTP请求设计
每个分片都是一个独立的POST请求,携带:
- 文件标识:
fileHash(根据文件内容生成的MD5,用于合并和秒传判断) - 分片序号:
chunkIndex(从0开始) - 总分片数:
totalChunks
关键点:每个请求必须独立成功/失败,前端需要记录已上传成功的分片序号,用于断点续传。
PHP后端接口设计
1 接收分片并存储
在PHP中,不能直接使用$_FILES['chunk'],因为每个分片是临时文件,推荐使用php://input流接收原始数据,或者直接通过$_FILES配合move_uploaded_file()。
强烈推荐使用$_FILES方式,因为它自动处理了multipart解析,每个分片存储为临时文件:
$uploadDir = './uploads/'; $fileHash = $_POST['fileHash']; $chunkIndex = $_POST['chunkIndex']; // 为每个文件创建专属目录 $chunkDir = $uploadDir . $fileHash . '/'; if (!is_dir($chunkDir)) mkdir($chunkDir); // 保存当前分片 move_uploaded_file($_FILES['chunk']['tmp_name'], $chunkDir . $chunkIndex);
2 分片校验
务必验证分片完整性:
- 检查
chunkIndex是否在0到totalChunks-1之间。 - 检查文件大小是否与预定义的
CHUNK_SIZE一致(最后一片可能较小)。
3 返回JSON响应
前端需要知道该分片是否成功,以及已上传的分片列表:
{
"code": 200,
"message": "chunk 3 uploaded",
"uploadedChunks": [0,1,2,3,4]
}
合并策略详解
1 合并触发时机
前端在所有分片上传完成后,向/merge发送POST请求,携带fileHash和totalChunks。
2 合并核心逻辑
public function merge($fileHash, $totalChunks) {
$filePath = './uploads/' . $fileHash . '.mp4'; // 原文件类型
$chunkDir = './uploads/' . $fileHash . '/';
// 追加写入方式合并
$finalFile = fopen($filePath, 'wb');
for ($i = 0; $i < $totalChunks; $i++) {
$chunkFile = $chunkDir . $i;
if (!file_exists($chunkFile)) {
throw new Exception("chunk $i missing");
}
$fileData = file_get_contents($chunkFile);
fwrite($finalFile, $fileData);
unlink($chunkFile); // 删除分片,释放空间
}
fclose($finalFile);
rmdir($chunkDir); // 删除临时目录
return $filePath;
}
3 秒传与断点续传
- 秒传:前端计算
fileHash后,先请求/check接口,若服务器已存在该文件,直接返回“已存在”,无需上传。 - 断点续传:
/check接口返回uploadedChunks数组,前端仅上传缺失的分片。
完整代码实战(前端+PHP)
前端核心代码(使用原生XHR + Promise):
async function uploadFile(file) {
const hash = await calculateMD5(file); // 使用spark-md5库
const chunkSize = 2 * 1024 * 1024;
const totalChunks = Math.ceil(file.size / chunkSize);
// 询问服务器已上传分片
let uploaded = await request('/check', {hash});
for (let i = 0; i < totalChunks; i++) {
if (uploaded.includes(i)) continue; // 跳过已上传分片
let formData = new FormData();
formData.append('file', file.slice(i*chunkSize, (i+1)*chunkSize));
formData.append('hash', hash);
formData.append('index', i);
formData.append('total', totalChunks);
await request('/upload', formData); // 逐个上传(或并发控制)
}
// 全部上传后触发合并
await request('/merge', {hash, totalChunks});
}
PHP后端/upload接口:
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$hash = $_POST['hash'];
$index = (int)$_POST['index'];
$chunkDir = "/tmp/{$hash}/";
if (!is_dir($chunkDir)) mkdir($chunkDir);
move_uploaded_file($_FILES['file']['tmp_name'], $chunkDir . $index);
echo json_encode(['code' => 200]);
}
常见问题与性能优化
1 内存风暴
合并时若使用file_get_contents读取大分片(如10MB),会占用内存,优化:使用fread循环读64KB并写入。
2 并发控制
前端同时发送多个分片会提升速度,但需限制并发数(如5个),否则服务器连接数被占满。
3 安全防护
- 文件类型验证:合并后必须检查
mime_content_type或魔数签名,防止恶意脚本上传。 - 目录穿越攻击:对
fileHash做白名单过滤(只允许字母数字)。 - 文件大小限制:在
nginx.conf中设置client_max_body_size 5m,防止单个分片过大。
4 定时清理
分片上传中断后,残留的临时目录(如/tmp/abc123/)占空间,需用Cron任务定期清理超过24小时的临时文件。
高频问答(QA)
Q1:合并时文件顺序错了怎么办?
A:合并必须严格按照chunkIndex递增顺序,如果某个分片确实丢失,PHP会抛出异常,此时应返回错误提示前端重新上传该分片,而不是合并成损坏文件。
Q2:如何实现秒传?
A:前端计算完整文件内容的MD5哈希(如使用Spark-MD5),上传前请求/check?hash=xxx,PHP查询数据库或文件系统是否存在该hash,存在则直接返回“秒传成功”,无需再传分片。
Q3:PHP能处理10GB的大文件吗?
A:完全可以,关键在于避免一次性读入内存,合并时使用fopen+fwrite流式操作,每读256KB写一次,同时建议将PHP的memory_limit设为-1(不限制),但更推荐用fread按块处理。
Q4:如果用户中途关闭浏览器,已上传的分片怎么办?
A:分片已保存在服务器临时目录,用户再次上传同一文件时,/check接口返回已存在的分片序号,前端自动跳过这些分片,实现续传。
Q5:为什么不能用$_FILES['file']直接接收整个大文件?
A:PHP默认将upload_max_filesize设为2M,且整个文件会被解析到内存(post_max_size,默认8M),如果强行调大,当几百个用户同时上传大文件时,服务器内存直接告罄,切片后每个请求仅处理2MB,内存占用极小。
Q6:合并时如何保证数据完整性?
A:在合并完成后,重新计算整个文件的MD5,并与前端提交的fileHash比对,若不一致,删除文件并返回错误,这是最可靠的安全校验。
Q7:可以用阿里云OSS或OSS分片上传替代吗?
A:如果生产环境,建议直接使用OSS的分片上传API(如MultipartUpload),它已经处理了并发、容错、断点续传,且不占服务器带宽,本文方案适用于自建存储或需要私有化部署的场景。
PHP分片上传本质上是对“文件流”的拆分与重组,核心代码量不大,但需要考虑异常处理、并发控制和数据校验,通过上述实战,你完全可以落地一个稳定支持10GB级文件上传的系统,建议在实现后,用ApacheBench进行并发测试,优化Nginx的client_body_buffer_size参数与PHP的max_execution_time,确保高并发下的稳定性。