本文目录导读:

在PHP中实现大文件下载的断点续传,核心是利用HTTP协议的Range请求头和Content-Range响应头,以下是完整的实现方案:
基本断点续传实现
<?php
function downloadFile($filePath) {
if (!file_exists($filePath)) {
header("HTTP/1.1 404 Not Found");
exit;
}
$fileSize = filesize($filePath);
$fileName = basename($filePath);
// 设置响应头
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"{$fileName}\"");
header("Accept-Ranges: bytes");
header("Content-Length: {$fileSize}");
// 处理断点续传
if (isset($_SERVER['HTTP_RANGE'])) {
$range = $_SERVER['HTTP_RANGE'];
// 解析Range头
preg_match('/bytes=(\d+)-(\d*)/', $range, $matches);
$startPos = intval($matches[1]);
$endPos = $matches[2] === '' ? $fileSize - 1 : intval($matches[2]);
// 验证范围有效性
if ($startPos > $endPos || $startPos >= $fileSize) {
header("HTTP/1.1 416 Range Not Satisfiable");
header("Content-Range: bytes */{$fileSize}");
exit;
}
// 设置206 Partial Content状态码
header("HTTP/1.1 206 Partial Content");
header("Content-Range: bytes {$startPos}-{$endPos}/{$fileSize}");
header("Content-Length: " . ($endPos - $startPos + 1));
header("Cache-Control: no-cache");
// 输出文件内容
$fp = fopen($filePath, 'rb');
fseek($fp, $startPos);
$bytesToSend = $endPos - $startPos + 1;
$chunkSize = 8192; // 8KB chunks
while ($bytesToSend > 0 && !feof($fp)) {
$readSize = min($chunkSize, $bytesToSend);
echo fread($fp, $readSize);
$bytesToSend -= $readSize;
flush();
ob_flush();
}
fclose($fp);
} else {
// 完整文件下载
readfile($filePath);
}
exit;
}
// 使用示例
$filePath = '/path/to/large/file.zip';
downloadFile($filePath);
?>
增强版(支持重试、多线程)
<?php
class DownloadManager {
private $filePath;
private $fileName;
private $fileSize;
public function __construct($filePath) {
$this->filePath = $filePath;
$this->fileName = basename($filePath);
$this->fileSize = filesize($filePath);
}
public function handleRequest() {
if (!file_exists($this->filePath)) {
header("HTTP/1.1 404 Not Found");
exit("File not found");
}
$this->setHeaders();
if (isset($_SERVER['HTTP_RANGE'])) {
$this->handleRangeRequest();
} else {
$this->handleFullDownload();
}
}
private function setHeaders() {
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"{$this->fileName}\"");
header("Accept-Ranges: bytes");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Expires: 0");
header("Pragma: no-cache");
}
private function handleRangeRequest() {
$range = $_SERVER['HTTP_RANGE'];
if (!preg_match('/bytes=(\d+)-(\d*)/', $range, $matches)) {
header("HTTP/1.1 400 Bad Request");
exit("Invalid Range header");
}
$startPos = intval($matches[1]);
$endPos = $matches[2] === '' ? $this->fileSize - 1 : intval($matches[2]);
// 验证范围
if ($startPos > $endPos || $startPos >= $this->fileSize) {
header("HTTP/1.1 416 Range Not Satisfiable");
header("Content-Range: bytes */{$this->fileSize}");
exit;
}
// 设置206状态码
header("HTTP/1.1 206 Partial Content");
header("Content-Range: bytes {$startPos}-{$endPos}/{$this->fileSize}");
header("Content-Length: " . ($endPos - $startPos + 1));
// 输出文件
$this->outputFile($startPos, $endPos);
}
private function handleFullDownload() {
header("Content-Length: {$this->fileSize}");
$this->outputFile(0, $this->fileSize - 1);
}
private function outputFile($startPos, $endPos) {
$fp = fopen($this->filePath, 'rb');
if (!$fp) {
header("HTTP/1.1 500 Internal Server Error");
exit("Unable to open file");
}
fseek($fp, $startPos);
$bytesRemaining = $endPos - $startPos + 1;
$bufferSize = 8192; // 8KB
set_time_limit(0); // 取消脚本执行时间限制
while ($bytesRemaining > 0 && !feof($fp)) {
$readSize = min($bufferSize, $bytesRemaining);
echo fread($fp, $readSize);
$bytesRemaining -= $readSize;
// 刷新输出缓冲区
if (ob_get_level()) {
ob_flush();
}
flush();
// 防止内存溢出
if (connection_aborted()) {
break;
}
}
fclose($fp);
}
}
// 使用示例
$downloader = new DownloadManager('/path/to/large/file.mkv');
$downloader->handleRequest();
?>
客户端断点续传(JavaScript)
class ResumeDownload {
constructor(url, outputStream) {
this.url = url;
this.outputStream = outputStream;
this.downloadedBytes = 0;
this.abortController = new AbortController();
}
async start() {
try {
const response = await this.downloadChunk();
await this.processResponse(response);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Download cancelled');
} else {
console.error('Download failed:', error);
// 尝试恢复
await this.resume();
}
}
}
async downloadChunk(startPos = 0) {
const headers = {};
if (startPos > 0) {
headers['Range'] = `bytes=${startPos}-`;
}
return fetch(this.url, {
headers: headers,
signal: this.abortController.signal
});
}
async processResponse(response) {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentLength = response.headers.get('content-length');
const totalSize = parseInt(contentLength) + this.downloadedBytes;
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log('Download complete');
break;
}
this.downloadedBytes += value.length;
const progress = ((this.downloadedBytes / totalSize) * 100).toFixed(2);
// 写入到输出流
this.outputStream.write(value);
// 更新进度
this.onProgress(progress);
}
}
onProgress(percent) {
console.log(`Download progress: ${percent}%`);
}
async resume() {
// 从上次下载位置继续
await this.downloadChunk(this.downloadedBytes);
}
cancel() {
this.abortController.abort();
}
}
// 使用示例
async function downloadLargeFile() {
const downloader = new ResumeDownload(
'http://example.com/large-file.zip',
new WritableStream()
);
try {
await downloader.start();
} catch (error) {
console.error('Download failed:', error);
}
}
性能优化建议
使用X-Sendfile(推荐)
// 使用Apache mod_xsendfile
header("X-Sendfile: {$filePath}");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"{$fileName}\"");
// 使用Nginx X-Accel-Redirect
header("X-Accel-Redirect: /download/{$fileName}");
header("Content-Disposition: attachment; filename=\"{$fileName}\"");
数据库下载记录
// 记录下载进度到数据库
class DownloadTracker {
private $db;
public function saveProgress($fileId, $downloadedBytes) {
$stmt = $this->db->prepare("UPDATE download_progress
SET downloaded_bytes = ?
WHERE file_id = ?");
$stmt->execute([$downloadedBytes, $fileId]);
}
public function getProgress($fileId) {
$stmt = $this->db->prepare("SELECT downloaded_bytes
FROM download_progress
WHERE file_id = ?");
$stmt->execute([$fileId]);
return $stmt->fetchColumn() ?: 0;
}
}
注意事项
- 内存管理:使用chunk方式读取,避免一次性加载大文件到内存
- 超时设置:取消脚本执行时间限制
set_time_limit(0) - 错误处理:处理连接中断、范围无效等情况
- 缓存头:设置正确的Cache-Control头,防止代理缓存
- 会话锁:使用
session_write_close()避免会话锁
这个实现支持标准的断点续传功能,也兼容多线程下载工具(如IDM、迅雷等)的并发请求。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播