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

PHP 檔案下載實作:Header + readfile 完整範例與大檔案優化

     編輯:PHP 相關知識

前言

隱藏真實檔案路徑、實作下載授權、支援大檔案傳輸,常用「PHP 代理下載」:設定 HTTP Header 強制瀏覽器下載,再用 readfile() 或分塊輸出檔案內容。本文提供完整、安全、可擴充的下載實作,並針對大檔案記憶體優化、斷點續傳、速度限制進行進階說明。

基礎版:readfile() 一行式下載

<?php
function downloadFile(string $filePath, string $downloadName = ''): void {
    if (!is_file($filePath) || !is_readable($filePath)) {
        http_response_code(404);
        exit('File not found');
    }

    $downloadName = $downloadName ?: basename($filePath);
    $mime = mime_content_type($filePath) ?: 'application/octet-stream';
    $size = filesize($filePath);

    header('Content-Type: ' . $mime);
    header('Content-Length: ' . $size);
    header('Content-Disposition: attachment; filename="' . rawurlencode($downloadName) . '"');
    header('Content-Transfer-Encoding: binary');
    header('Accept-Ranges: bytes'); // 聲明支援 Range (實際未處理)
    header('Cache-Control: private, must-revalidate');
    header('Pragma: private');

    readfile($filePath); // 直接輸出至瀏覽器
    exit;
}

// 使用
downloadFile('/var/www/secret/report.pdf', '2024年報.pdf');
?>

缺點

  • 無斷點續傳:忽略 Range 標頭,瀏覽器/下載工具無法暫停續傳。
  • 記憶體風險:readfile() 內部使用 8KB 緩衝區,記憶體尚可,但若前有 ob_start() 會緩衝整個檔案導致 OOM。
  • 無速度控制:單一用戶可佔滿頻寬。
  • 無快取驗證:重複下載不回傳 304。

進階版:分塊輸出 + Range 斷點續傳 + 速度限制

<?php
/**
 * 進階下載器:支援 Range、速度限制、恆定記憶體
 */
function advancedDownload(string $filePath, string $downloadName = '', int $speedLimit = 0): void {
    if (!is_file($filePath) || !is_readable($filePath)) {
        http_response_code(404);
        exit('File not found');
    }

    $downloadName = $downloadName ?: basename($filePath);
    $mime = mime_content_type($filePath) ?: 'application/octet-stream';
    $fileSize = filesize($filePath);
    $lastModified = filemtime($filePath);
    $etag = md5($fileSize . '-' . $lastModified);

    // 1. 快取驗證
    if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag) {
        http_response_code(304);
        exit;
    }
    if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $lastModified) {
        http_response_code(304);
        exit;
    }

    // 2. 解析 Range
    $start = 0;
    $end = $fileSize - 1;
    $isPartial = false;

    if (isset($_SERVER['HTTP_RANGE'])) {
        if (preg_match('/^bytes=(\d*)-(\d*)$/i', $_SERVER['HTTP_RANGE'], $m)) {
            $start = $m[1] !== '' ? (int)$m[1] : 0;
            $end = $m[2] !== '' ? (int)$m[2] : $fileSize - 1;

            if ($m[1] === '' && $m[2] !== '') { // bytes=-500
                $start = $fileSize - $end;
                $end = $fileSize - 1;
            }

            $start = max(0, min($start, $fileSize - 1));
            $end = max($start, min($end, $fileSize - 1));

            if ($start <= $end) {
                $isPartial = true;
            }
        }
    }

    $contentLength = $end - $start + 1;

    // 3. 發送標頭
    if ($isPartial) {
        header('HTTP/1.1 206 Partial Content');
        header("Content-Range: bytes $start-$end/$fileSize");
    } else {
        header('HTTP/1.1 200 OK');
    }

    header('Content-Type: ' . $mime);
    header('Content-Length: ' . $contentLength);
    header('Accept-Ranges: bytes');
    header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
    header('ETag: ' . $etag);
    header('Cache-Control: public, max-age=31536000');
    header('Pragma: public');
    header('Content-Disposition: attachment; filename*=UTF-8\'\'' . rawurlencode($downloadName) . '; filename="' . rawurlencode($downloadName) . '"');
    header('Content-Transfer-Encoding: binary');

    // 4. 關閉輸出緩衝 (防止 OOM)
    while (ob_get_level() > 0) {
        ob_end_clean();
    }

    // 5. 流式輸出
    $handle = fopen($filePath, 'rb');
    fseek($handle, $start);

    $bufferSize = 8192; // 8KB
    $sent = 0;
    $packetTime = $speedLimit > 0 ? ($bufferSize * 1_000_000 / $speedLimit) : 0;

    while ($sent < $contentLength && !feof($handle) && connection_status() === CONNECTION_NORMAL) {
        if ($packetTime > 0) $t0 = microtime(true);

        $readLen = min($bufferSize, $contentLength - $sent);
        $buffer = fread($handle, $readLen);
        if ($buffer === false) break;

        echo $buffer;
        $sent += strlen($buffer);

        if (ob_get_level() > 0) ob_flush();
        flush();

        if ($packetTime > 0) {
            $t1 = microtime(true);
            $elapsed = ($t1 - $t0) * 1_000_000;
            $sleep = $packetTime - $elapsed;
            if ($sleep > 0) usleep((int)$sleep);
        }
    }

    fclose($handle);
    exit;
}

// 使用:免費用戶限速 200 KB/s
advancedDownload('/data/files/video.mp4', '課程影片.mp4', 200 * 1024);
?>

大檔案 (GB/TB 級) 優化關鍵點

問題解決方案
PHP 記憶體耗盡關閉 output_buffering (ob_end_clean()),使用 fread 分塊輸出,不使用 readfile、file_get_contents
執行逾時set_time_limit(0) 或 Nginx proxy_read_timeout 調大
Nginx 代理緩衝導致延遲/記憶體暴增proxy_buffering off; proxy_request_buffering off;
斷點續傳不工作正確解析 Range、回應 206 + Content-Range + Accept-Ranges: bytes
中文檔名亂碼RFC 5987 filename*=UTF-8''... + 相容 filename="..."
客戶端斷線腳本繼續跑檢查 connection_status() === CONNECTION_NORMAL,異常即 break

Nginx X-Accel-Redirect (零拷貝、最高效能)

若檔案儲存在伺服器本地,最佳方案是 PHP 僅驗證權限,Nginx 直接傳檔:

# PHP 端
function downloadWithXAccel(string $internalPath, string $downloadName): void {
    // $internalPath 為 Nginx internal location 對應的內部路徑,如 /protected/files/report.pdf
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename*=UTF-8\'\'' . rawurlencode($downloadName));
    header('X-Accel-Redirect: ' . $internalPath); // 告訴 Nginx 傳送此路徑
    header('X-Accel-Buffering: no'); // 禁用 Nginx 緩衝
    exit;
}

# Nginx 設定
location /protected/ {
    internal;                    # 僅允許內部重導向 (X-Accel-Redirect)
    alias /var/www/secret/;      # 實際檔案根目錄
    # 可在此層加入 limit_rate、limit_rate_after 限速
}

優點

  • PHP 進程極快結束,釋放記憶體、CPU、連線槽。
  • Nginx 使用 sendfile() 系統調用 (零拷貝),CPU 佔用極低。
  • 原生支援 Range、斷點續傳、SSL、限速。

總結

根據專案規模選型:

  1. 小檔案、低併發、簡單授權 → 基礎版 readfile()。
  2. 需斷點續傳、限速、大檔案、自建儲存 → 進階版分塊輸出 + Range 處理。
  3. 高併發、大檔案、本地儲存、追求極致效能 → Nginx X-Accel-Redirect (或 Apache mod_xsendfile),PHP 僅做權限驗證。

所有方案務必實作:路徑安全檢查 (realpath + 允許目錄前綴)、中文檔名 RFC 5987、關閉輸出緩衝、連線狀態檢測。

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