
本文详解php大文件(尤其是100mb+视频)分片上传中常见的文件损坏根源——内存加载全量分片、非流式合并及顺序错乱,并提供基于流式写入、原子拼接与完整性校验的工业级解决方案。
本文详解php大文件(尤其是100mb+视频)分片上传中常见的文件损坏根源——内存加载全量分片、非流式合并及顺序错乱,并提供基于流式写入、原子拼接与完整性校验的工业级解决方案。
在处理Unity3D等客户端上传的大型视频文件(1MB–2GB)时,单纯按固定大小(如10MB)切片并逐个接收,再用fread(filesize())一次性读入内存合并,极易引发静默损坏——文件大小看似一致,但播放失败、FFmpeg报错(如moov atom not found、invalid data found),根本原因在于PHP内存限制与合并逻辑缺陷。
? 根本问题定位
你当前PHP合并逻辑存在三大致命隐患:
- 内存超载:fread($myfile, filesize($filePath)) 将整个分片(最大10MB)加载进内存,多个分片叠加 + PHP自身开销,轻松突破默认 memory_limit=128M,触发OOM或截断;
- 非流式拼接:fopen(..., 'w') 覆盖写入 + 多次fread/fwrite,未使用fseek或严格二进制追加,易因缓冲区/编码干扰破坏视频二进制结构;
- 顺序不可靠:scandir() + sort() 依赖文件名字符串排序(如 "1.part", "10.part" → "1.part" < "10.part"),但分片索引若为纯数字且未补零,将导致0,1,10,11,...,2错序,直接破坏MP4等容器格式的moov头位置。
✅ 验证方式:用 xxd -l 64 your_video.mp4 对比原文件与合并后文件前64字节——若ftyp、moov签名不一致,即为合并顺序或字节流错误。
✅ 正确合并方案:流式追加 + 索引驱动 + 原子校验
1. 前端强制补零命名(关键!)
// Unity/C# 或 JS 中生成分片文件名时,统一6位补零
const chunkIndex = i; // 0, 1, 2, ..., 105
const fileName = `${fileId}_${String(i).padStart(6, '0')}.part`; // "abc123_000000.part"后端据此精确排序,杜绝字符串误判。
立即学习“PHP免费学习笔记(深入)”;
2. PHP流式合并(零内存压力)
function mergeChunkFiles(string $chunkDir, string $finalPath, int $totalChunks): bool
{
// 1. 按数字索引严格排序(非字符串)
$chunks = [];
for ($i = 0; $i < $totalChunks; $i++) {
$chunkPath = sprintf('%s/%06d.part', $chunkDir, $i);
if (!file_exists($chunkPath) || !is_readable($chunkPath)) {
error_log("Missing chunk: {$chunkPath}");
return false;
}
$chunks[] = $chunkPath;
}
// 2. 原子化流式拼接:逐块读取 → 直接追加到目标文件
$out = fopen($finalPath . '.tmp', 'wb'); // 先写临时文件
if (!$out) return false;
foreach ($chunks as $chunkPath) {
$in = fopen($chunkPath, 'rb');
if (!$in) continue;
// 流式传输,每8KB一帧,内存恒定≈8KB
while (!feof($in)) {
$buffer = fread($in, 8192);
if ($buffer === false || fwrite($out, $buffer) === false) {
fclose($in); fclose($out);
@unlink($finalPath . '.tmp');
return false;
}
}
fclose($in);
}
fclose($out);
// 3. 原子替换(避免合并中途被访问)
if (rename($finalPath . '.tmp', $finalPath)) {
// 4. 校验:比对客户端传入的总大小 & 文件MD5
$actualSize = filesize($finalPath);
if ($actualSize !== (int)$_POST['total_file_size']) {
error_log("Size mismatch: expected {$_POST['total_file_size']}, got {$actualSize}");
return false;
}
return true;
}
return false;
}3. 服务端状态强约束(防并发/中断)
- 拒绝重复上传:检查 $chunkDir/000000.part 是否存在,存在则跳过;
- 断点续传支持:前端上传前先GET /status?file_id=xxx,服务端返回已存在分片索引数组;
- 超时清理:合并成功后立即rrmdir($chunkDir);未完成的分片目录,用定时脚本清理72小时以上残留。
⚠️ 必须同步调整的服务端配置
即使采用流式合并,仍需解除基础瓶颈:
; php.ini upload_max_filesize = 20M ; 单分片上限(10MB chunk → 设20M冗余) post_max_size = 20M ; 匹配upload_max_filesize max_execution_time = 300 ; 防止大分片上传超时 memory_limit = 256M ; 流式合并仅需少量内存,256M足够
? 注意:post_max_size 无法用ini_set()运行时修改,必须改php.ini并重启WAMP服务。
✅ 最终验证清单
| 检查项 | 合格标准 |
|---|---|
| 分片命名 | 000000.part, 000001.part...(6位补零) |
| 合并方式 | fopen('wb') → fread(8192) → fwrite() 循环,绝不fread(filesize()) |
| 文件校验 | 合并后filesize() == 客户端total_file_size,且md5_file()匹配前端预计算值 |
| FFmpeg验证 | ffmpeg -v error -i video.mp4 -f null - 2>&1 \| grep -q "error" 应无输出 |
遵循此方案,100MB–2GB视频可稳定上传、精准还原,彻底规避“大小正确但无法播放”的顽疾。核心思想始终如一:让数据像水流一样穿过PHP,而非堆积在内存里等待崩溃。



















