大文件下載斷點續傳是提升用戶體驗、節省頻寬的關鍵技術。本文詳細實現基於 HTTP/1.1 Range 請求頭的標準斷點續傳方案,涵蓋完整的協議交互、邊界處理、效能優化。
Range: bytes=0-499 (首 500 字節)、bytes=500- (500 字節之後)、bytes=-500 (最後 500 字節)206 Partial Content + Content-Range: bytes 0-499/5000 + Content-Length: 500Accept-Ranges: bytes 告知客戶端支援範圍請求<?php
/**
* 大文件斷點續傳下載
*
* @param string $filePath 文件絕對路徑
* @param array $options 配置選項
* @throws Exception 文件不存在、權限不足等
*/
function downloadWithResume(string $filePath, array $options = []): void {
// 1. 基礎驗證
if (!is_file($filePath) || !is_readable($filePath)) {
http_response_code(404);
throw new Exception('文件不存在或不可讀');
}
$fileSize = filesize($filePath);
$fileName = basename($filePath);
$mimeType = getMimeType($filePath);
// 2. 發送通用頭
header("Cache-Control: public");
header("Content-Type: $mimeType");
header("Accept-Ranges: bytes");
// 3. 處理文件名編碼 (解決中文文件名亂碼)
$encodedName = rawurlencode($fileName);
$disposition = "attachment; filename*=UTF-8''$encodedName";
// IE 兼容
if (str_contains($_SERVER['HTTP_USER_AGENT'] ?? '', 'MSIE') || str_contains($_SERVER['HTTP_USER_AGENT'] ?? '', 'Trident')) {
$disposition = 'attachment; filename="' . str_replace('.', '%2e', $fileName, substr_count($fileName, '.') - 1) . '"';
}
header("Content-Disposition: $disposition");
// 4. 處理 Range 請求
$start = 0;
$end = $fileSize - 1;
$statusCode = 200;
if (isset($_SERVER['HTTP_RANGE'])) {
$rangeHeader = $_SERVER['HTTP_RANGE'];
// 解析 bytes=start-end
if (preg_match('/bytes=(\d*)-(\d*)/', $rangeHeader, $matches)) {
$start = $matches[1] !== '' ? (int)$matches[1] : 0;
$end = $matches[2] !== '' ? (int)$matches[2] : $fileSize - 1;
// 邊界檢查
if ($start >= $fileSize || $end >= $fileSize || $start > $end) {
header('Content-Range: bytes */' . $fileSize);
http_response_code(416); // Range Not Satisfiable
return;
}
$statusCode = 206;
$contentLength = $end - $start + 1;
header("Content-Length: $contentLength");
header("Content-Range: bytes $start-$end/$fileSize");
}
} else {
header("Content-Length: $fileSize");
header("Content-Range: bytes 0-$end/$fileSize");
}
http_response_code($statusCode);
// 5. 如果是 HEAD 請求,直接返回
if ($_SERVER['REQUEST_METHOD'] === 'HEAD') {
return;
}
// 6. 分塊輸出文件 (避免大文件佔用記憶體)
$chunkSize = $options['chunk_size'] ?? 8192; // 8KB
$handle = fopen($filePath, 'rb');
if ($start > 0) {
fseek($handle, $start);
}
$remaining = $end - $start + 1;
while (!feof($handle) && $remaining > 0) {
// 防止腳本超時
set_time_limit(0);
$readSize = min($chunkSize, $remaining);
$buffer = fread($handle, $readSize);
if ($buffer === false || $buffer === '') break;
echo $buffer;
@flush(); // 輸出緩衝
$remaining -= strlen($buffer);
// 檢查客戶端是否斷開連接
if (connection_aborted()) {
break;
}
}
fclose($handle);
}
/** 獲取 MIME 類型 */
function getMimeType(string $filePath): string {
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
$mimes = [
'exe' => 'application/octet-stream',
'zip' => 'application/zip',
'gz' => 'application/gzip',
'pdf' => 'application/pdf',
'mp3' => 'audio/mpeg',
'mp4' => 'video/mp4',
'avi' => 'video/x-msvideo',
'jpg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'txt' => 'text/plain',
'html' => 'text/html',
'js' => 'application/javascript',
'css' => 'text/css',
];
return $mimes[$ext] ?? 'application/octet-stream';
}
// 使用示例
try {
downloadWithResume('/data/files/large-video.mp4');
} catch (Exception $e) {
error_log('下載錯誤: ' . $e->getMessage());
}
| 技術點 | 實現細節 | 注意事項 |
|---|---|---|
| Range 解析 | 正則匹配 bytes=(\d*)-(\d*) | 支援開區間、單側範圍、多範圍(本實現僅處理單範圍) |
| 邊界檢查 | 416 Range Not Satisfiable | 必須返回 Content-Range: bytes */$fileSize |
| 分塊讀取 | fread($handle, 8192) | 固定緩衝區,避免記憶體溢出 |
| 連接檢測 | connection_aborted() | 客戶端取消下載時及時停止輸出 |
| 中文文件名 | RFC 5987 filename*=UTF-8'' | 現代瀏覽器支援,IE 需特殊處理 |
| 輸出緩衝 | flush() + ob_flush() | 需關閉 PHP 輸出緩衝或配合 ob_implicit_flush |
chunk_size 至 64KB-256KB,減少系統調用usleep() 實現下載限速multipart/byteranges<?php
// PHP 僅做權限驗證
if ($userCanDownload) {
header('X-Accel-Redirect: /protected/files/' . $fileName);
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename*=UTF-8\'\'' . rawurlencode($fileName));
return;
}
```nginx
# Nginx 配置
location /protected/files/ {
internal; # 只允許內部重定向
alias /data/files/;
sendfile on; # 零拷貝
sendfile_max_chunk 1m; # 分塊發送
tcp_nopush on;
tcp_nodelay on;
}
```
斷點續傳不僅是功能實現,更是對 HTTP 協議深度理解的體現。掌握 Range 請求處理、分塊輸出、Web 服務器協作優化,能構建企業級的文件分發服務。