提供大型檔案 (安裝包、視頻、備份) 下載時,**斷點續傳** 是用戶體驗與頻寬節省的關鍵。核心在於正確處理 HTTP **Range 請求標頭**,回應 **206 Partial Content** 並輸出指定位元組範圍。本文提供生產級完整實現與細節解析。
| 標頭 | 方向 | 格式範例 | 說明 |
|---|---|---|---|
| Range | 請求 | `bytes=0-499` / `bytes=500-` / `bytes=-500` | 客戶端請求特定位元組範圍 |
| Accept-Ranges | 回應 | `bytes` | 伺服器宣告支援範圍請求 |
| Content-Range | 回應 | `bytes 0-499/2000` | 回應實際輸出範圍/總大小 |
| Content-Length | 回應 | `500` | 本次回應實體長度 (非總檔案大小) |
| ETag / Last-Modified | 回應 | `"abc123"` / `Wed, 21 Oct 2015 07:28:00 GMT` | 快取驗證,配合 If-Range 使用 |
<?php
/**
* 檔案下載斷點續傳核心函數
* @param string $file 檔案絕對路徑
* @param array $options 選項: ['chunkSize'=>8192, 'speedLimit'=>0, 'disposition'=>'attachment']
*/
function dl_file_resume(string $file, array $options = []) {
// 1. 基礎驗證
if (!is_file($file) || !is_readable($file)) {
http_response_code(404);
exit('File not found or not readable');
}
$fileSize = filesize($file);
$fileName = basename($file);
$mimeType = getMimeType($file); // 見下方輔助函數
// 2. 基礎回應標頭 (無論是否 Range 請求都需發送)
header('Accept-Ranges: bytes');
header('Content-Type: ' . $mimeType);
header('Cache-Control: public, must-revalidate');
header('Pragma: public'); // HTTP 1.0 相容
// Content-Disposition: 处理文件名编码 (RFC 5987)
$encodedName = rawurlencode($fileName);
header("Content-Disposition: attachment; filename*=UTF-8''{$encodedName}; filename=\"{$fileName}\"");
// 3. 處理 Range 請求
$rangeHeader = $_SERVER['HTTP_RANGE'] ?? '';
$start = 0;
$end = $fileSize - 1;
$statusCode = 200;
if ($rangeHeader) {
// 解析 Range: bytes=start-end
if (!preg_match('/^bytes=(\d*)-(\d*)$/i', $rangeHeader, $matches)) {
// 格式錯誤:忽略 Range,返回完整檔案
error_log("Invalid Range header: $rangeHeader");
} else {
$start = $matches[1] !== '' ? (int)$matches[1] : 0;
$end = $matches[2] !== '' ? (int)$matches[2] : $fileSize - 1;
// 邊界檢查
if ($start >= $fileSize || $end >= $fileSize || $start > $end) {
// 範圍不滿足:返回 416 Range Not Satisfiable
header('Content-Range: bytes */' . $fileSize);
http_response_code(416);
exit;
}
$statusCode = 206; // Partial Content
$contentLength = $end - $start + 1;
// 關鍵標頭
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize);
header('Content-Length: ' . $contentLength);
}
} else {
// 無 Range:完整檔案
header('Content-Length: ' . $fileSize);
}
// 4. 發送狀態碼 (必須在輸出內容前)
http_response_code($statusCode);
// 5. 輸出檔案內容
$fp = fopen($file, 'rb');
if (!$fp) {
http_response_code(500);
exit('Cannot open file');
}
// 定位到起始位置
if ($start > 0) {
fseek($fp, $start);
}
$chunkSize = $options['chunkSize'] ?? 8192; // 8KB
$remaining = ($statusCode === 206) ? ($end - $start + 1) : $fileSize;
$speedLimit = $options['speedLimit'] ?? 0; // 0 = 不限速
try {
while (!feof($fp) && $remaining > 0) {
// 限速邏輯
if ($speedLimit > 0) {
$startTime = microtime(true);
}
$readSize = min($chunkSize, $remaining);
$buffer = fread($fp, $readSize);
if ($buffer === false || $buffer === '') break;
echo $buffer;
// 刷新輸出緩衝 (關鍵:讓瀏覽器即時收到資料)
@ob_flush();
@flush();
$remaining -= strlen($buffer);
// 限速延遲
if ($speedLimit > 0) {
$elapsed = microtime(true) - $startTime;
$targetTime = strlen($buffer) / $speedLimit; // 秒
if ($elapsed < $targetTime) {
usleep(($targetTime - $elapsed) * 1000000);
}
}
// 檢查客戶端是否斷開連線
if (connection_aborted()) {
break; // 正常中斷,不報錯
}
}
} finally {
fclose($fp);
}
// 正常結束
exit;
}
/**
* 簡易 MIME 類型映射
*/
function getMimeType(string $file): string {
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
$map = [
'pdf' => 'application/pdf',
'zip' => 'application/zip',
'rar' => 'application/x-rar-compressed',
'7z' => 'application/x-7z-compressed',
'exe' => 'application/octet-stream',
'mp3' => 'audio/mpeg',
'mp4' => 'video/mp4',
'avi' => 'video/x-msvideo',
'mov' => 'video/quicktime',
'jpg' => 'image/jpeg',
'jpeg'=> 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'txt' => 'text/plain',
'csv' => 'text/csv',
'doc' => 'application/msword',
'docx'=> 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls' => 'application/vnd.ms-excel',
'xlsx'=> 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
];
return $map[$ext] ?? 'application/octet-stream';
}
// 使用範例
// dl_file_resume('/var/www/files/video.mp4', ['speedLimit' => 1024*1024]); // 限速 1MB/s
?>
bytes=0-499 // 前 500 位元組 (0~499)
bytes=500-999 // 第 500~999 位元組
bytes=-500 // 最後 500 位元組
bytes=500- // 第 500 位元組到結尾
bytes=0-0,-1 // 第一個和最後一個位元組 (多範圍,本實現不支援,返回 200 全檔)
// 關鍵三行:分塊讀取、輸出、強制刷新
$buffer = fread($fp, $chunkSize);
echo $buffer;
@ob_flush(); @flush(); // 穿透 PHP/Server 緩衝,直達瀏覽器
// 現代瀏覽器支援 filename*=UTF-8''encoded_name
$encoded = rawurlencode($fileName); // 空格→%20 非 ASCII→%XX
header("Content-Disposition: attachment; filename*=UTF-8''{$encoded}; filename=\"{$fileName}\"");
// 目標:平均速度 ≤ $speedLimit bytes/秒
$elapsed = microtime(true) - $chunkStartTime;
$expected = strlen($buffer) / $speedLimit;
if ($elapsed < $expected) usleep(($expected - $elapsed) * 1e6);
**生產環境強烈建議**將檔案傳輸交給 Web Server 處理,PHP 僅做權限驗證與標頭設置:
# nginx.conf
location /protected/ {
internal; // 僅允許內部重定向
alias /var/www/files/;
# 可選:限速、斷點續傳由 Nginx 原生支援
# limit_rate 1m;
}
# PHP
header('X-Accel-Redirect: /protected/video.mp4');
header('Content-Type: video/mp4');
header('Content-Disposition: ...');
exit; // PHP 結束,Nginx 接管傳輸
# httpd.conf
XSendFile on
XSendFilePath /var/www/files
# PHP
header('X-Sendfile: /var/www/files/video.mp4');
header('Content-Type: video/mp4');
exit;
<script>
// 簡易斷點續傳下載器 (現代瀏覽器原生支援 Range,但手動控制可實現多線程)
async function downloadWithResume(url, fileName) {
const response = await fetch(url, {method: 'HEAD'});
const totalSize = parseInt(response.headers.get('Content-Length'));
const supportsRange = response.headers.get('Accept-Ranges') === 'bytes';
if (!supportsRange) {
// 不支援斷點續傳,直接下載
window.location.href = url;
return;
}
// 這裡可實現多線程分段下載 + Blob 合併
// 參考成熟庫:StreamSaver.js, FileSaver.js
}
</script>
<?php
function simpleDownload($file) {
if (!is_file($file)) die('404');
$size = filesize($file);
$name = basename($file);
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $name . '"');
header('Content-Length: ' . $size);
header('Cache-Control: public');
readfile($file); // 內部已處理分塊輸出,但無 Range 支援
exit;
}
?>
掌握此完整實現,即能為用戶提供企業級的大檔案下載體驗:斷點續傳、多線程下載支援、限速控制、高併發穩定傳輸。