本文目录导读:

我来详细解释PHP实现断点上传的几种方案:
基础原理
断点上传的核心是:
- 前端将文件切片
- 后端接收并保存每个分片
- 所有分片上传完成后合并
简单分片上传实现
前端代码(HTML + JavaScript)
<!DOCTYPE html>
<html>
<head>断点上传</title>
</head>
<body>
<input type="file" id="fileInput">
<button onclick="uploadFile()">开始上传</button>
<div id="progress"></div>
<script>
const chunkSize = 1 * 1024 * 1024; // 1MB 每片
async function uploadFile() {
const file = document.getElementById('fileInput').files[0];
if (!file) return;
const totalChunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
// 检查已上传的分片
const checkResponse = await fetch('check.php', {
method: 'POST',
body: JSON.stringify({
fileName: file.name,
totalChunks: totalChunks
})
});
const uploadedChunks = await checkResponse.json();
for (let i = 0; i < totalChunks; i++) {
if (uploadedChunks.includes(i)) continue;
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('fileName', file.name);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
try {
await fetch('upload.php', {
method: 'POST',
body: formData
});
const progress = Math.round(((i + 1) / totalChunks) * 100);
document.getElementById('progress').textContent =
`上传进度: ${progress}%`;
} catch (error) {
console.error(`分片 ${i} 上传失败`);
break;
}
}
// 通知服务器合并文件
await fetch('merge.php', {
method: 'POST',
body: JSON.stringify({
fileName: file.name,
totalChunks: totalChunks
})
});
}
</script>
</body>
</html>
后端PHP代码
upload.php(接收分片)
<?php
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$fileName = $_POST['fileName'];
$chunkIndex = (int)$_POST['chunkIndex'];
$uploadDir = 'uploads/chunks/';
// 创建上传目录
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
if (isset($_FILES['file'])) {
$chunkName = $fileName . '_chunk_' . $chunkIndex;
$chunkPath = $uploadDir . md5($chunkName);
// 保存分片
if (move_uploaded_file($_FILES['file']['tmp_name'], $chunkPath)) {
echo json_encode([
'success' => true,
'message' => '分片上传成功',
'chunkIndex' => $chunkIndex
]);
} else {
echo json_encode([
'success' => false,
'message' => '分片上传失败'
]);
}
}
}
check.php(检查已上传分片)
<?php
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
$fileName = $input['fileName'];
$totalChunks = $input['totalChunks'];
$uploadDir = 'uploads/chunks/';
$uploadedChunks = [];
// 遍历检查已上传的分片
for ($i = 0; $i < $totalChunks; $i++) {
$chunkName = $fileName . '_chunk_' . $i;
$chunkPath = $uploadDir . md5($chunkName);
if (file_exists($chunkPath)) {
$uploadedChunks[] = $i;
}
}
echo json_encode($uploadedChunks);
merge.php(合并文件)
<?php
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
$fileName = $input['fileName'];
$totalChunks = $input['totalChunks'];
$uploadDir = 'uploads/chunks/';
$finalDir = 'uploads/files/';
if (!file_exists($finalDir)) {
mkdir($finalDir, 0777, true);
}
$finalPath = $finalDir . $fileName;
// 创建并写入最终文件
$finalFile = fopen($finalPath, 'wb');
for ($i = 0; $i < $totalChunks; $i++) {
$chunkName = $fileName . '_chunk_' . $i;
$chunkPath = $uploadDir . md5($chunkName);
if (file_exists($chunkPath)) {
$chunkContent = file_get_contents($chunkPath);
fwrite($finalFile, $chunkContent);
// 删除已合并的分片
unlink($chunkPath);
} else {
fclose($finalFile);
echo json_encode([
'success' => false,
'message' => "分片 {$i} 缺失,合并失败"
]);
return;
}
}
fclose($finalFile);
echo json_encode([
'success' => true,
'message' => '文件合并成功',
'filePath' => $finalPath
]);
改进版本(带MD5校验)
<?php
// upload.php - 带MD5校验版本
function getFileMd5($filePath) {
if (file_exists($filePath)) {
return md5_file($filePath);
}
return null;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$fileName = $_POST['fileName'];
$chunkIndex = (int)$_POST['chunkIndex'];
$chunkMd5 = isset($_POST['md5']) ? $_POST['md5'] : '';
$uploadDir = 'uploads/chunks/';
$chunkName = $fileName . '_chunk_' . $chunkIndex;
$chunkPath = $uploadDir . md5($chunkName);
if (isset($_FILES['file'])) {
$tmpName = $_FILES['file']['tmp_name'];
// 计算上传分片的MD5
$uploadedMd5 = md5_file($tmpName);
// 校验MD5
if ($uploadedMd5 === $chunkMd5) {
if (move_uploaded_file($tmpName, $chunkPath)) {
echo json_encode([
'success' => true,
'message' => '分片上传成功,校验通过'
]);
}
} else {
echo json_encode([
'success' => false,
'message' => 'MD5校验失败,请重新上传'
]);
}
}
}
使用腾讯云/阿里云OSS断点续传
<?php
// 使用阿里云OSS SDK示例
require_once 'aliyun-oss-php-sdk/autoload.php';
use OSS\OssClient;
use OSS\Core\OssException;
$accessKeyId = "your-access-key-id";
$accessKeySecret = "your-access-key-secret";
$endpoint = "oss-cn-hangzhou.aliyuncs.com";
$bucket = "your-bucket";
try {
$ossClient = new OssClient($accessKeyId, $accessKeySecret, $endpoint);
// 分片上传
$object = "uploads/example.zip";
$file = __DIR__ . "/example.zip";
// 初始化分片上传
$uploadId = $ossClient->initiateMultipartUpload($bucket, $object);
// 上传分片
$partSize = 10 * 1024 * 1024; // 10MB
$uploadFileSize = filesize($file);
$partCount = ceil($uploadFileSize / $partSize);
$parts = array();
for ($i = 1; $i <= $partCount; $i++) {
$start = ($i - 1) * $partSize;
$length = min($partSize, $uploadFileSize - $start);
$content = file_get_contents($file, false, null, $start, $length);
$result = $ossClient->uploadPart($bucket, $object, $uploadId, array(
'PartNumber' => $i,
'Content' => $content
));
$parts[] = array(
'PartNumber' => $i,
'ETag' => $result['ETag']
);
}
// 完成分片上传
$result = $ossClient->completeMultipartUpload($bucket, $object, $uploadId, $parts);
echo "文件上传成功: " . $result['Location'];
} catch (OssException $e) {
echo "上传失败: " . $e->getMessage();
}
最佳实践建议
前端优化
- 使用Web Worker处理分片,避免阻塞UI
- 添加重试机制
- 显示实时进度条
- 支持拖拽上传
后端优化
- 使用Redis记录已上传分片状态
- 定期清理过期分片
- 使用消息队列异步处理合并
- 限制文件大小和并发数
// 使用Redis记录分片状态
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = "upload:{$fileName}";
$redis->sAdd($key, $chunkIndex);
$redis->expire($key, 3600); // 1小时后过期
// 检查分片是否存在
if ($redis->sIsMember($key, $chunkIndex)) {
echo "该分片已上传";
}
这套方案可以满足大多数断点上传需求,如果需要处理超大文件,建议使用云存储服务商的SDK,它们提供了更完善的断点续传机制。