1. 视频分片上传的核心价值与挑战
在当今视频内容爆炸式增长的时代,用户上传高清视频已成为各类网站的标配功能。但传统单文件上传方式在面对大体积视频时存在明显瓶颈:网络波动导致上传失败需要重传整个文件、服务器内存不足无法处理大文件、用户无法实时查看上传进度等。这正是分片上传技术(Chunked Upload)大显身手的场景。
WebUploader作为国内广泛使用的前端上传组件,配合PHP后端处理,能够完美解决这些问题。其核心原理是将大视频文件切割为多个小块(通常每片1-5MB),通过多线程并行上传,服务端接收后按序重组。这种方案带来三大优势:
- 断点续传:即使网络中断,只需重传失败的分片而非整个文件
- 进度可控:前端可精确显示每个分片的传输状态
- 内存友好:服务器每次只处理小体积分片,避免内存溢出
2. 完整技术方案设计
2.1 前端WebUploader配置要点
初始化WebUploader时需要特别关注以下参数配置:
javascript复制const uploader = WebUploader.create({
swf: 'path/to/Uploader.swf', // Flash备用方案
server: '/upload.php', // 服务端接收地址
chunked: true, // 开启分片
chunkSize: 2 * 1024 * 1024, // 每片2MB
threads: 3, // 并发线程数
formData: { // 自定义附加参数
uid: 12345 // 用户唯一标识
}
});
关键参数说明:
chunkSize:需要根据服务器性能和网络质量平衡。过小会导致请求次数暴增,过大会失去分片意义threads:建议3-5个并发,过多可能导致浏览器阻塞formData:必须包含能唯一标识文件的字段,用于服务端分片归组
2.2 服务端PHP处理流程设计
PHP端需要实现四个核心功能模块:
-
分片接收验证
- 校验分片序号(chunk)、总分片数(chunks)
- 验证文件MD5指纹防止篡改
- 检查是否已存在成功上传的分片
-
临时存储管理
- 为每个上传会话创建临时目录
- 以
[filename]_[chunkIndex].part格式存储分片 - 设置自动清理机制(如24小时未完成上传)
-
分片合并逻辑
- 所有分片上传完成后按序号合并
- 使用二进制追加写入保证顺序正确
- 生成最终文件的MD5校验值
-
状态反馈机制
- 实时返回分片上传结果(success/error)
- 支持查询已上传分片实现断点续传
- 合并完成后触发后续处理(如转码)
3. PHP核心代码实现详解
3.1 分片接收与验证
php复制// upload.php
$targetDir = "uploads/tmp_".$_POST['uid'];
$chunk = isset($_POST['chunk']) ? intval($_POST['chunk']) : 0;
$chunks = isset($_POST['chunks']) ? intval($_POST['chunks']) : 0;
$fileName = isset($_POST['name']) ? $_POST['name'] : '';
// 创建临时目录
if (!file_exists($targetDir)) {
@mkdir($targetDir, 0777, true);
}
// 生成分片临时文件名
$targetPath = $targetDir.DIRECTORY_SEPARATOR.$fileName."_".$chunk.".part";
// 移动上传文件
if (!empty($_FILES)) {
$tmpName = $_FILES['file']['tmp_name'];
if (move_uploaded_file($tmpName, $targetPath)) {
// 返回成功响应
$response = [
'success' => true,
'chunk' => $chunk,
'message' => '分片上传成功'
];
echo json_encode($response);
}
}
重要安全提示:必须对
$_POST['name']进行严格过滤,防止目录遍历攻击。建议使用basename()函数处理文件名。
3.2 分片合并算法实现
当检测到所有分片上传完成(通过chunk == chunks-1判断),触发合并操作:
php复制function mergeFiles($targetDir, $fileName, $chunks) {
$finalPath = "uploads/final_".$fileName;
$fileSize = 0;
// 按序号读取分片并追加写入
if ($out = @fopen($finalPath, "wb")) {
for ($i = 0; $i < $chunks; $i++) {
$chunkPath = $targetDir.DIRECTORY_SEPARATOR.$fileName."_".$i.".part";
if (!$in = @fopen($chunkPath, "rb")) {
break;
}
while ($buff = fread($in, 4096)) {
fwrite($out, $buff);
}
@fclose($in);
@unlink($chunkPath); // 删除已合并分片
$fileSize += filesize($chunkPath);
}
@fclose($out);
}
// 验证文件完整性
if ($fileSize == $_POST['size']) {
return ['status' => true, 'path' => $finalPath];
} else {
@unlink($finalPath);
return ['status' => false];
}
}
3.3 断点续传支持实现
前端在上传前可先查询服务端已接收的分片:
php复制// check.php
$targetDir = "uploads/tmp_".$_GET['uid'];
$fileName = basename($_GET['name']);
$chunks = intval($_GET['chunks']);
$existChunks = [];
for ($i = 0; $i < $chunks; $i++) {
if (file_exists($targetDir.DIRECTORY_SEPARATOR.$fileName."_".$i.".part")) {
$existChunks[] = $i;
}
}
echo json_encode([
'existChunks' => $existChunks,
'chunks' => $chunks
]);
4. 生产环境优化方案
4.1 性能调优技巧
-
内存优化:
- 设置
php.ini中的memory_limit至少128M - 使用
stream_copy_to_stream()替代fread+fwrite减少内存占用
- 设置
-
并发控制:
- 使用文件锁防止分片合并冲突
php复制$lockFile = $targetDir.'/merge.lock'; $lock = fopen($lockFile, 'w'); if (flock($lock, LOCK_EX)) { // 执行合并操作 flock($lock, LOCK_UN); } fclose($lock); -
IO优化:
- 将临时目录与最终存储目录分置不同磁盘
- 使用
fastcgi_finish_request()实现异步合并
4.2 安全防护措施
-
文件类型校验:
php复制$allowedTypes = ['video/mp4', 'video/quicktime']; if (!in_array($_FILES['file']['type'], $allowedTypes)) { die(json_encode(['error' => '不支持的文件类型'])); } -
病毒扫描集成:
php复制$clamscan = '/usr/bin/clamscan'; if (file_exists($clamscan)) { exec("$clamscan --no-summary $tmpName", $output, $return); if ($return !== 0) { unlink($tmpName); die(json_encode(['error' => '文件安全检测未通过'])); } } -
权限控制:
- 使用
chmod()设置上传目录不可执行 - 定期清理过期临时文件
- 使用
5. 常见问题排查指南
5.1 分片上传失败排查
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 413错误 | Nginx/Apache限制 | 调整client_max_body_size和upload_max_filesize |
| 500错误 | 权限不足 | 确保临时目录可写(chmod -R 777 uploads) |
| 分片乱序 | 并发冲突 | 增加文件锁或使用数据库记录状态 |
| MD5校验失败 | 网络传输错误 | 启用HTTPS并增加重试机制 |
5.2 合并失败处理方案
-
文件句柄泄漏:
- 确保每个
fopen()都有对应的fclose() - 使用
try-finally保证资源释放
- 确保每个
-
磁盘空间不足:
- 合并前检查剩余空间
php复制$freeSpace = disk_free_space(dirname($finalPath)); if ($freeSpace < $_POST['size'] * 1.2) { die(json_encode(['error' => '磁盘空间不足'])); } -
超时中断:
- 设置
set_time_limit(0)取消PHP执行时间限制 - 对于超大文件建议使用队列异步处理
- 设置
6. 高级扩展方案
6.1 云端存储集成
将最终文件自动转存至云存储(以阿里云OSS为例):
php复制require_once 'oss-sdk/autoload.php';
use OSS\OssClient;
use OSS\Core\OssException;
try {
$ossClient = new OssClient(
'yourAccessKeyId',
'yourAccessKeySecret',
'yourEndpoint'
);
$ossClient->uploadFile('yourBucket', 'video/'.$fileName, $finalPath);
unlink($finalPath); // 删除本地副本
} catch (OssException $e) {
// 错误处理
}
6.2 视频转码队列
使用Redis实现转码任务队列:
php复制// 将视频信息推入队列
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$task = [
'file' => $finalPath,
'format' => 'mp4',
'resolution' => '1080p'
];
$redis->lPush('video_transcode', json_encode($task));
6.3 上传进度实时展示
前端通过WebSocket获取实时进度:
javascript复制const socket = new WebSocket('ws://yoursite.com/progress');
socket.onmessage = function(e) {
const data = JSON.parse(e.data);
if (data.uid === currentUid) {
updateProgress(data.chunk, data.total);
}
};
PHP服务端推送示例:
php复制// 在分片上传成功后
$context = new ZMQContext();
$pusher = $context->getSocket(ZMQ::SOCKET_PUSH);
$pusher->connect("tcp://localhost:5555");
$pusher->send(json_encode([
'uid' => $_POST['uid'],
'chunk' => $chunk,
'total' => $chunks
]));