首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 實作檔案分塊下載與斷點續傳完整解決方案

     編輯:PHP 綜合知識

支援 HTTP Range 請求的檔案下載功能,能讓使用者暫停/繼續下載、多執行緒下載、拖曳播放影音進度。本文提供生產級 PHP 實作,包含 MIME 偵測、Range 解析、206 Partial Content 回應、大檔案記憶體控制等完整邏輯。

核心實作:支援斷點續傳的下載類

<?php
/**
 * 檔案下載器 - 支援 Range/斷點續傳/大檔案/MIME 自動偵測
 */
class FileDownloader {
    private $filePath;
    private $fileName;
    private $mimeType;
    private $fileSize;
    private $fp;
    
    public function __construct(string $filePath, string $fileName = '') {
        $this->filePath = $filePath;
        $this->fileName = $fileName ?: basename($filePath);
        $this->fileSize = filesize($filePath);
        $this->mimeType = $this->detectMimeType($filePath);
    }
    
    public function download(): void {
        // 基礎檢查
        if (!file_exists($this->filePath) || !is_readable($this->filePath)) {
            $this->sendError(404, '檔案不存在或不可讀');
        }
        
        $this->fp = fopen($this->filePath, 'rb');
        if (!$this->fp) {
            $this->sendError(500, '無法開啟檔案');
        }
        
        // 解析 Range 標頭
        $range = $this->parseRangeHeader();
        [$start, $end, $length] = $range;
        
        // 發送回應標頭
        $this->sendHeaders($start, $end, $length);
        
        // 輸出檔案內容
        $this->outputFile($start, $length);
        
        fclose($this->fp);
        exit;
    }
    
    /**
     * 解析 HTTP_RANGE 標頭
     * @return array [start, end, length]
     */
    private function parseRangeHeader(): array {
        $start = 0;
        $end = $this->fileSize - 1;
        $isRange = false;
        
        if (isset($_SERVER['HTTP_RANGE'])) {
            $rangeHeader = $_SERVER['HTTP_RANGE'];
            if (preg_match('/^bytes=(\d+)-(\d*)$/i', $rangeHeader, $matches)) {
                $isRange = true;
                $start = (int)$matches[1];
                $end = $matches[2] !== '' ? (int)$matches[2] : $this->fileSize - 1;
            } elseif (preg_match('/^bytes=-(\d+)$/i', $rangeHeader, $matches)) {
                // 最後 N 位元組:bytes=-1024
                $isRange = true;
                $end = $this->fileSize - 1;
                $start = $this->fileSize - (int)$matches[1];
            }
        }
        
        // 邊界修正
        $start = max(0, min($start, $this->fileSize - 1));
        $end = max($start, min($end, $this->fileSize - 1));
        $length = $end - $start + 1;
        
        return [$start, $end, $length];
    }
    
    /**
     * 發送 HTTP 標頭
     */
    private function sendHeaders(int $start, int $end, int $length): void {
        // 清除輸出緩衝
        if (ob_get_level()) ob_end_clean();
        
        // 通用標頭
        header('Content-Type: ' . $this->mimeType);
        header('Content-Disposition: attachment; filename="' . $this->fileName . '"');
        header('Accept-Ranges: bytes');
        header('Cache-Control: no-cache, must-revalidate');
        header('Pragma: no-cache');
        
        if ($start > 0 || $end < $this->fileSize - 1) {
            // 部分內容回應
            header('HTTP/1.1 206 Partial Content');
            header('Content-Range: bytes ' . $start . '-' . $end . '/' . $this->fileSize);
            header('Content-Length: ' . $length);
        } else {
            // 完整檔案
            header('Content-Length: ' . $this->fileSize);
        }
        
        // 針對舊版 IE 的快取修正
        if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== false) {
            header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
            header('Pragma: public');
        }
    }
    
    /**
     * 分塊輸出檔案內容
     */
    private function outputFile(int $start, int $length): void {
        fseek($this->fp, $start);
        
        $bufferSize = 1024 * 16; // 16KB
        $remaining = $length;
        
        while ($remaining > 0 && !feof($this->fp) && connection_status() === CONNECTION_NORMAL) {
            $readSize = min($bufferSize, $remaining);
            echo fread($this->fp, $readSize);
            $remaining -= $readSize;
            flush(); // 立即發送給客戶端
        }
    }
    
    /**
     * MIME 類型偵測
     */
    private function detectMimeType(string $path): string {
        $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
        
        $mimeMap = [
            // 圖片
            'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png',
            'gif' => 'image/gif', 'webp' => 'image/webp', 'bmp' => 'image/bmp',
            'svg' => 'image/svg+xml', 'ico' => 'image/vnd.microsoft.icon',
            // 文件
            'pdf' => 'application/pdf', 'doc' => 'application/msword',
            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'xls' => 'application/vnd.ms-excel',
            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            'ppt' => 'application/vnd.ms-powerpoint',
            'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
            'txt' => 'text/plain', 'csv' => 'text/csv', 'rtf' => 'application/rtf',
            // 壓縮
            'zip' => 'application/zip', 'rar' => 'application/x-rar-compressed',
            '7z' => 'application/x-7z-compressed', 'gz' => 'application/gzip',
            'tar' => 'application/x-tar',
            // 音視訊
            'mp3' => 'audio/mpeg', 'wav' => 'audio/wav', 'ogg' => 'audio/ogg',
            'mp4' => 'video/mp4', 'webm' => 'video/webm', 'avi' => 'video/x-msvideo',
            'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'mkv' => 'video/x-matroska',
            // 程式碼
            'js' => 'application/javascript', 'json' => 'application/json',
            'xml' => 'application/xml', 'css' => 'text/css', 'html' => 'text/html',
            'php' => 'text/html',
            // 執行檔
            'exe' => 'application/x-msdownload', 'msi' => 'application/x-msdownload',
            'apk' => 'application/vnd.android.package-archive',
        ];
        
        return $mimeMap[$ext] ?? 'application/octet-stream';
    }
    
    private function sendError(int $code, string $message): void {
        http_response_code($code);
        header('Content-Type: text/plain; charset=utf-8');
        exit($message);
    }
}

// 使用範例
$downloader = new FileDownloader('/var/www/files/video.mp4', '我的影片.mp4');
$downloader->download();
?>

進階功能擴展

1. 多 Range 支援 (RFC 7233)

瀏覽器極少發送多範圍,但標準要求支援。回應需使用 multipart/byteranges:

// 簡易實作概念
if (preg_match_all('/bytes=(\d+)-(\d*)/i', $rangeHeader, $matches, PREG_SET_ORDER)) {
    if (count($matches) > 1) {
        header('Content-Type: multipart/byteranges; boundary=THIS_STRING_SEPARATES');
        foreach ($matches as $m) {
            echo "--THIS_STRING_SEPARATES\r\n";
            echo "Content-Type: {$this->mimeType}\r\n";
            echo "Content-Range: bytes {$m[1]}-{$m[2]}/{$this->fileSize}\r\n\r\n";
            // 輸出該範圍內容...
        }
        echo "--THIS_STRING_SEPARATES--\r\n";
        exit;
    }
}

2. 限速下載

// 在 outputFile 迴圈中加入
$bytesPerSecond = 1024 * 1024; // 1MB/s
$startTime = microtime(true);
$totalSent = 0;

while (...) {
    // ...輸出資料...
    $totalSent += $readSize;
    $elapsed = microtime(true) - $startTime;
    $expectedTime = $totalSent / $bytesPerSecond;
    if ($expectedTime > $elapsed) {
        usleep(($expectedTime - $elapsed) * 1000000);
    }
}

3. 下載計數與記錄

// 下載前記錄
$log = [
    'file' => $this->fileName,
    'ip' => $_SERVER['REMOTE_ADDR'],
    'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
    'range' => $_SERVER['HTTP_RANGE'] ?? 'full',
    'time' => date('Y-m-d H:i:s'),
];
file_put_contents('/var/log/downloads.log', json_encode($log) . "\n", FILE_APPEND | LOCK_EX);

Nginx/Apache 整合建議 (X-Sendfile / X-Accel-Redirect)

PHP 讀取檔案輸出會佔用 PHP-FPM 進程,高併發下影響效能。生產環境建議交給 Web Server 處理:

Nginx + X-Accel-Redirect

# nginx.conf
location /protected/ {
    internal;                    # 禁止直接外部存取
    alias /var/www/files/;       # 實際檔案目錄
    # 可加入限速、權限驗證等
}
<?php
// PHP 僅驗證權限,發送特殊 Header 交給 Nginx 處理傳輸
if ($userHasPermission) {
    header('X-Accel-Redirect: /protected/video.mp4');
    header('Content-Type: video/mp4');
    header('Content-Disposition: attachment; filename="video.mp4"');
    exit;
}
?>

Apache + mod_xsendfile

# httpd.conf
XSendFile On
XSendFilePath /var/www/files/
<?php
header('X-Sendfile: /var/www/files/video.mp4');
header('Content-Type: video/mp4');
header('Content-Disposition: attachment; filename="video.mp4"');
exit;
?>

效能調優清單

  • ✅ 使用 fread + flush 分塊輸出,避免 readfile 佔用大量記憶體
  • ✅ 設定 set_time_limit(0) 防止大檔案傳輸中途被殺
  • ✅ 啟用 ob_end_clean() 清除輸出緩衝
  • ✅ 生產環境改用 X-Sendfile/X-Accel-Redirect 卸載 PHP
  • ✅ 針對影音檔案啟用 Accept-Ranges: bytes 支援拖曳播放
  • ✅ 設定合理的 Content-Length 讓下載進度條正確顯示

總結:完整的斷點續傳下載需要正確解析 Range 標頭、回應 206 狀態碼、發送 Content-Range、分塊輸出控制記憶體。生產環境強烈建議配合 Nginx/Apache 的 X-Sendfile 機制,將檔案傳輸卸載給 Web Server,PHP 僅負責權限驗證與 Header 發送。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved