PHP大文件切片上传合并

wen PHP项目 1

本文目录导读:

PHP大文件切片上传合并

  1. 为什么需要切片上传?——传统上传的痛点
  2. 切片上传的核心原理
  3. PHP后端接口设计
  4. 合并策略详解
  5. 完整代码实战(前端+PHP)
  6. 常见问题与性能优化
  7. 高频问答(QA)

**
《PHP大文件切片上传合并:从原理到实战,彻底解决超大文件传输瓶颈》


目录导读

  1. 为什么需要切片上传?——传统上传的痛点
  2. 切片上传的核心原理(HTTP协议与前端分片)
  3. PHP后端接口设计(接收、校验、临时存储)
  4. 合并策略详解(顺序合并、断点续传、秒传判断)
  5. 完整代码实战(前端+PHP后端)
  6. 常见问题与性能优化(内存、并发、安全)
  7. 高频问答(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是否在0totalChunks-1之间。
  • 检查文件大小是否与预定义的CHUNK_SIZE一致(最后一片可能较小)。

3 返回JSON响应

前端需要知道该分片是否成功,以及已上传的分片列表:

{
  "code": 200,
  "message": "chunk 3 uploaded",
  "uploadedChunks": [0,1,2,3,4]
}

合并策略详解

1 合并触发时机

前端在所有分片上传完成后,向/merge发送POST请求,携带fileHashtotalChunks

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,确保高并发下的稳定性。

抱歉,评论功能暂时关闭!