PHP处理大文件上传时,默认配置通常无法满足需求,会出现上传大小限制、请求超时、内存占用过高的问题,需要对配置和代码逻辑进行优化。

一、修改PHP基础配置
首先需要调整PHP的配置文件php.ini中的相关参数,解除基础的大小和时间限制。
- upload_max_filesize:设置单个上传文件的最大大小,比如设置为100M
- post_max_size:设置POST请求的最大大小,需要比upload_max_filesize大,比如设置为120M
- max_execution_time:设置脚本最大执行时间,大文件上传需要更长时间,建议设置为300秒以上
- max_input_time:设置接收POST数据的最大时间,建议设置为300秒以上
- memory_limit:设置脚本最大内存占用,建议设置为256M以上
修改完成后需要重启PHP服务让配置生效。
二、分片上传实现
分片上传是将大文件拆分成多个小片段分别上传,所有片段上传完成后再合并成完整文件,避免单次请求数据量过大导致的问题。前端需要将文件按固定大小拆分,比如每片2M,然后依次发送每个分片到后端。
前端分片上传示例
// 前端分片上传逻辑
function uploadLargeFile(file) {
const chunkSize = 2 * 1024 * 1024; // 每片2M
const totalChunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const fileId = Date.now() + '_' + file.name; // 生成唯一文件标识
function uploadChunk() {
if (currentChunk >= totalChunks) {
// 所有分片上传完成,通知后端合并
mergeChunks(fileId, totalChunks, file.name);
return;
}
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('chunkIndex', currentChunk);
formData.append('totalChunks', totalChunks);
formData.append('fileId', fileId);
formData.append('fileName', file.name);
fetch('/upload_chunk.php', {
method: 'POST',
body: formData
}).then(res => res.json()).then(data => {
if (data.code === 0) {
currentChunk++;
uploadChunk(); // 上传下一片
} else {
console.error('上传分片失败', data.msg);
}
});
}
uploadChunk();
}
function mergeChunks(fileId, totalChunks, fileName) {
fetch('/merge_chunks.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileId: fileId,
totalChunks: totalChunks,
fileName: fileName
})
}).then(res => res.json()).then(data => {
if (data.code === 0) {
console.log('文件上传完成', data.filePath);
} else {
console.error('合并文件失败', data.msg);
}
});
}
后端分片接收与合并逻辑
后端接收每个分片后保存到临时目录,所有分片上传完成后按顺序合并成完整文件。
<?php
// upload_chunk.php 接收分片
$chunk = $_FILES['chunk'];
$chunkIndex = $_POST['chunkIndex'];
$totalChunks = $_POST['totalChunks'];
$fileId = $_POST['fileId'];
$fileName = $_POST['fileName'];
$tmpDir = __DIR__ . '/upload_tmp/' . $fileId . '/';
if (!is_dir($tmpDir)) {
mkdir($tmpDir, 0777, true);
}
$chunkPath = $tmpDir . $chunkIndex . '.chunk';
move_uploaded_file($chunk['tmp_name'], $chunkPath);
echo json_encode(['code' => 0, 'msg' => '分片上传成功']);
?>
<?php
// merge_chunks.php 合并分片
$data = json_decode(file_get_contents('php://input'), true);
$fileId = $data['fileId'];
$totalChunks = $data['totalChunks'];
$fileName = $data['fileName'];
$tmpDir = __DIR__ . '/upload_tmp/' . $fileId . '/';
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
$finalPath = $uploadDir . $fileName;
$fp = fopen($finalPath, 'wb');
for ($i = 0; $i < $totalChunks; $i++) {
$chunkPath = $tmpDir . $i . '.chunk';
if (file_exists($chunkPath)) {
$chunkContent = file_get_contents($chunkPath);
fwrite($fp, $chunkContent);
unlink($chunkPath); // 删除临时分片
}
}
fclose($fp);
// 删除临时目录
rmdir($tmpDir);
echo json_encode(['code' => 0, 'msg' => '文件合并成功', 'filePath' => $finalPath]);
?>
三、断点续传优化
断点续传可以在上传中断后,重新上传时只上传未完成的分片,不需要重新上传所有内容。实现思路是后端记录每个文件已上传的分片索引,前端上传前先查询已上传的分片列表,跳过已上传的部分。
后端记录已上传分片接口
<?php
// check_chunks.php 查询已上传分片
$fileId = $_GET['fileId'];
$tmpDir = __DIR__ . '/upload_tmp/' . $fileId . '/';
$uploadedChunks = [];
if (is_dir($tmpDir)) {
$files = scandir($tmpDir);
foreach ($files as $file) {
if ($file !== '.' && $file !== '..') {
$chunkIndex = str_replace('.chunk', '', $file);
$uploadedChunks[] = (int)$chunkIndex;
}
}
}
echo json_encode(['code' => 0, 'uploadedChunks' => $uploadedChunks]);
?>
前端断点续传调整
前端在上传分片前先调用查询接口,获取已上传的分片列表,上传时跳过这些分片即可。
// 调整上传逻辑,增加断点续传支持
function uploadLargeFile(file) {
const chunkSize = 2 * 1024 * 1024;
const totalChunks = Math.ceil(file.size / chunkSize);
const fileId = Date.now() + '_' + file.name;
let uploadedChunks = [];
// 先查询已上传的分片
fetch('/check_chunks.php?fileId=' + fileId)
.then(res => res.json())
.then(data => {
uploadedChunks = data.uploadedChunks || [];
uploadNextChunk();
});
function uploadNextChunk() {
// 找到下一个未上传的分片
let currentChunk = 0;
while (currentChunk < totalChunks && uploadedChunks.includes(currentChunk)) {
currentChunk++;
}
if (currentChunk >= totalChunks) {
mergeChunks(fileId, totalChunks, file.name);
return;
}
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('chunkIndex', currentChunk);
formData.append('totalChunks', totalChunks);
formData.append('fileId', fileId);
formData.append('fileName', file.name);
fetch('/upload_chunk.php', {
method: 'POST',
body: formData
}).then(res => res.json()).then(data => {
if (data.code === 0) {
uploadedChunks.push(currentChunk);
uploadNextChunk();
} else {
console.error('上传分片失败', data.msg);
}
});
}
}
四、注意事项
- 临时分片目录需要设置合理的权限,避免写入失败
- 合并文件时需要校验分片数量是否完整,避免文件损坏
- 可以对上传的文件做类型校验,避免上传恶意文件
- 如果服务器使用Nginx,还需要调整Nginx的client_max_body_size配置,和PHP的上传限制保持一致