PHP实现大文件分片上传

实战干货:原生PHP实现大文件分片上传,无需框架、开箱即用,支持普通大文件上传,完美解决PHP大文件上传超时、内存溢出、表单限制等核心问题,适配服务器生产环境。

在日常开发中,直接使用PHP原生上传接口传输几百MB甚至几GB的大文件,会遇到诸多致命问题:

  • PHP脚本执行超时(max_execution_time 限制)
  • 服务器POST数据大小、单文件上传大小限制
  • 一次性读取大文件导致内存溢出
  • 上传中断需要重新上传,不支持断点续传

分片上传 是解决以上问题的最优方案,本文将从零实现一套 前端分片切割 + PHP后端接收合并 的完整上传方案,代码简洁、可直接部署使用。

目录

一、分片上传核心原理

二、环境前置配置(必改)

[修改 php.ini 核心参数](#修改 php.ini 核心参数)

三、完整代码实现

前端页面:文件切片+异步上传

后端接口:接收分片+合并文件

四、项目目录结构

五、核心知识点解析

分片大小选择

分片命名规则

二进制合并原理

六、生产环境进阶优化(必备)

实现断点续传

完善安全校验

异常容错处理

性能优化

七、常见问题避坑指南

八、总结


一、分片上传核心原理

分片上传的核心思想:化整为零,分批传输,最后合并,具体流程如下:

  1. 前端切片:通过JS File API 将超大文件切割为固定大小的小分片(1MB~5MB)
  1. 唯一标识:对原文件生成MD5唯一指纹,区分不同文件的分片,避免冲突
  1. 逐片上传:异步批量上传每一个分片,携带分片索引、总分片数等参数
  1. 后端暂存:PHP接收分片,临时存储在单独目录,按规则命名区分
  1. 合并文件:所有分片上传完成后,后端按索引顺序合并所有分片,生成完整文件
  1. 清理缓存:合并完成后自动删除临时分片文件,释放服务器存储空间

二、环境前置配置(必改)

PHP默认配置严格限制文件上传,必须修改 php.ini 配置文件,否则分片上传会失效。

修改 php.ini 核心参数

php 复制代码
; 单文件最大上传大小(根据业务调整)
upload_max_filesize = 100M
; POST请求最大数据大小
post_max_size = 100M
; 脚本执行时间 0=不限制
max_execution_time = 0
; 脚本内存限制,大文件上传建议调高
memory_limit = 512M
; 关闭文件上传临时文件清理(可选)
upload_tmp_dir = /tmp

修改完成后,重启Apache/Nginx、PHP-FPM 服务生效。

三、完整代码实现

整套方案分为 前端上传页面(index.html)PHP后端接收接口(upload.php),两个文件放在同一目录即可运行。

前端页面:文件切片+异步上传

实现文件选择、自动切片、进度条展示、逐片异步上传功能,原生JS无依赖。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>PHP大文件分片上传</title>
    <style>
        .box{margin: 50px auto;width: 400px;}
        .progress{width: 100%;height: 12px;background: #eee;border-radius: 6px;margin: 15px 0;overflow: hidden;}
        .progress-bar{height: 100%;background: #1677ff;width: 0;transition: width 0.3s;}
        .tip{color: #666;margin-top: 10px;}
    </style>
</head>
<body>
<div class="box">
    <h3>大文件分片上传演示</h3>
    <input type="file" id="file" />
    <div class="progress"><div class="progress-bar" id="progress"></div></div>
    <div class="tip" id="msg">请选择需要上传的大文件</div>
</div>

<script>
    // 单个分片大小:1MB(可调整2M/5M)
    const CHUNK_SIZE = 1 * 1024 * 1024;
    const fileDom = document.getElementById('file');
    const progressDom = document.getElementById('progress');
    const msgDom = document.getElementById('msg');

    // 监听文件选择
    fileDom.addEventListener('change', async function (e) {
        const file = e.target.files[0];
        if (!file) return;

        // 初始化状态
        progressDom.style.width = '0%';
        msgDom.innerText = '正在解析文件...';

        // 生成文件唯一标识(简易MD5,生产建议使用spark-md5)
        const fileMd5 = await getFileUniqueId(file);
        // 计算总分片数
        const totalChunk = Math.ceil(file.size / CHUNK_SIZE);
        let successCount = 0;

        msgDom.innerText = `开始上传,共${totalChunk}个分片`;

        // 循环上传所有分片
        for (let i = 0; i < totalChunk; i++) {
            // 切割文件分片
            const start = i * CHUNK_SIZE;
            const end = Math.min(start + CHUNK_SIZE, file.size);
            const chunk = file.slice(start, end);

            // 组装上传参数
            const formData = new FormData();
            formData.append('chunk', chunk);         // 分片文件
            formData.append('index', i);             // 当前分片索引
            formData.append('total', totalChunk);    // 总分片数
            formData.append('md5', fileMd5);         // 文件唯一标识
            formData.append('filename', file.name);  // 原始文件名

            // 上传当前分片
            const res = await uploadChunk(formData);
            if (res.code !== 0) {
                msgDom.innerText = `第${i+1}片上传失败,请重试`;
                return;
            }

            // 更新进度
            successCount++;
            const percent = (successCount / totalChunk) * 100;
            progressDom.style.width = percent + '%';
            msgDom.innerText = `上传中 ${successCount}/${totalChunk}`;
        }

        msgDom.innerText = '✅ 文件上传完成!';
    });

    // 分片上传请求
    function uploadChunk(data) {
        return new Promise(resolve => {
            const xhr = new XMLHttpRequest();
            xhr.open('POST', 'upload.php', true);
            xhr.onload = function () {
                resolve(JSON.parse(xhr.responseText));
            };
            xhr.onerror = function () {
                resolve({code: 1, msg: '请求异常'});
            };
            xhr.send(data);
        });
    }

    // 生成文件唯一标识(简易版)
    function getFileUniqueId(file) {
        return new Promise(resolve => {
            const reader = new FileReader();
            reader.readAsArrayBuffer(file);
            reader.onload = function (e) {
                // 简易唯一ID,生产环境替换为完整MD5
                resolve(btoa(e.target.result.slice(0, 100)));
            };
        });
    }
</script>
</body>
</html>

后端接口:接收分片+合并文件

PHP核心接口,实现分片接收、临时存储、自动合并、缓存清理全逻辑。

php 复制代码
<?php
// 设置响应头
header('Content-Type: application/json; charset=utf-8');

// 基础配置
$config = [
    'upload_path' => './uploads/',  // 最终文件保存目录
    'chunk_path'  => './chunks/',   // 分片临时存储目录
];

// 初始化目录
!is_dir($config['upload_path']) && mkdir($config['upload_path'], 0777, true);
!is_dir($config['chunk_path'])  && mkdir($config['chunk_path'], 0777, true);

// 接收前端参数
$index    = intval($_POST['index']);     // 当前分片索引
$total    = intval($_POST['total']);     // 总分片数
$file_md5 = trim($_POST['md5']);         // 文件唯一标识
$filename = trim($_POST['filename']);    // 原始文件名
$chunk    = $_FILES['chunk'];            // 分片文件

// 分片保存路径:md5_索引 命名,防止文件冲突
$chunk_file = $config['chunk_path'] . $file_md5 . '_' . $index;

// 移动分片文件到临时目录
if (!move_uploaded_file($chunk['tmp_name'], $chunk_file)) {
    exit(json_encode(['code' => 1, 'msg' => '分片上传失败']));
}

// 所有分片上传完毕,执行文件合并
if ($index == $total - 1) {
    // 拼接最终文件路径
    $final_file = $config['upload_path'] . $filename;
    // 二进制方式创建文件
    $handle = fopen($final_file, 'wb');

    // 按顺序读取分片并写入最终文件
    for ($i = 0; $i < $total; $i++) {
        $temp_chunk = $config['chunk_path'] . $file_md5 . '_' . $i;
        // 写入分片内容
        fwrite($handle, file_get_contents($temp_chunk));
        // 删除已合并的分片
        unlink($temp_chunk);
    }
    fclose($handle);
}

// 返回成功响应
echo json_encode(['code' => 0, 'msg' => 'success']);
?>

四、项目目录结构

部署后自动生成目录,无需手动创建,结构清晰:

bash 复制代码
项目根目录
├── index.html      # 前端上传页面
├── upload.php      # 后端接收合并接口
├── uploads/        # 最终上传文件存储目录(自动生成)
└── chunks/         # 分片临时缓存目录(自动生成)

五、核心知识点解析

分片大小选择

  • 推荐区间:1MB - 5MB
  • 分片过小:请求次数过多,占用服务器HTTP资源,上传变慢
  • 分片过大:单请求超时概率高,失去分片意义

分片命名规则

采用 文件MD5_分片索引 命名,完美解决:

  • 多文件同时上传的分片冲突问题
  • 分片顺序错乱导致文件损坏问题

二进制合并原理

使用 fopen(xx,wb) 二进制覆写模式,逐片拼接文件二进制流,不改变文件编码、格式,支持视频、压缩包、图片、文档等所有文件格式。

六、生产环境进阶优化(必备)

上述代码为基础可用版本,生产环境需优化以下功能,提升稳定性和安全性:

实现断点续传

新增 查询已上传分片接口,上传前请求后端,获取当前文件已上传的分片索引,跳过已上传分片,中断后无需重传全部文件。

完善安全校验

  • 限制上传文件后缀,禁止php、exe、sh等恶意文件
  • 文件合并后校验整体MD5,防止文件损坏、篡改
  • 增加请求token验证,防止恶意刷接口

异常容错处理

  • 前端增加分片失败自动重试机制
  • 后端增加分片过期清理脚本,定时删除残留缓存分片
  • 增加文件大小、分片数量合法性校验

性能优化

  • 前端开启并发上传(同时上传2-3个分片),提升上传速度
  • 后端使用文件指针流式合并,替代file_get_contents,降低大文件内存占用

七、常见问题避坑指南

  • 上传超时 :务必设置 max_execution_time=0,关闭脚本执行时间限制
  • 文件合并损坏 :必须按分片索引顺序合并,不可无序拼接
  • 权限报错:给项目目录设置 0777 权限,保证PHP有读写、创建、删除权限
  • 分片残留:服务器定时清理 chunks 临时目录,避免磁盘占用过高
  • 大文件内存溢出 :调高 memory_limit,优先使用流式读写

八、总结

PHP大文件分片上传的核心逻辑就是 前端切片、后端暂存、最终合并。本文提供的源码无框架依赖、轻量化、可直接部署,彻底解决了传统PHP单文件上传的各类限制问题。

基础版本可满足中小型项目需求,搭配断点续传、安全校验、定时清理等进阶功能后,可直接用于企业级生产环境。

相关推荐
派大鑫wink1 小时前
Java 高级编程技巧(生产级实用,覆盖性能、并发、设计、JVM、语法、避坑)
开发语言·python
凤山老林1 小时前
JDK 11 升级至 JDK 17
java·开发语言·jdk17·jdk升级·jdk11
指令集梦境1 小时前
图解:单调栈算法模板(Java语言)
java·开发语言·算法
isyangli_blog1 小时前
SDN 基本应用实践 —— 使用命令行实现简易防火墙功能实验报告
服务器·php·apache
小灰灰搞电子1 小时前
C++ boost::circular_buffer 详解:原理、用法与实战
开发语言·c++·boost
Hanniel2 小时前
Python描述符(下):内置机制揭秘
开发语言·python·机器学习
Cloud_Shy6182 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第七章 Item 52 - 53)
开发语言·人工智能·笔记·python
星恒随风2 小时前
C++ string 类详解:常用接口、OJ 场景与模拟实现中的深浅拷贝
开发语言·c++·笔记·学习·状态模式