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

PHP 文件下載響應實現:Headers 機制、大文件分塊傳輸與安全實踐

     編輯:PHP 相關知識

PHP 文件下載響應實現:Headers 機制、大文件分塊傳輸與安全實踐

PHP 實現文件下載的核心在於發送正確的 HTTP 響應頭,告知瀏覽器「這是附件、文件名、大小、類型」,並以二進制流輸出內容。但生產環境常面臨 大文件內存溢出、斷點續傳、中文文件名編碼、權限校驗、並發限流 等挑戰。本文從基礎 Headers 機制到生產級分塊傳輸類,構建完整知識體系。

一、核心響應頭詳解

Header作用推薦值
Content-TypeMIME 類型application/octet-stream(通用二進制)或具體類型如 application/pdf
Content-Disposition指定下載行為與文件名attachment; filename="file.pdf"; filename*=UTF-8''file.pdf
Content-Length文件大小(字節),支持進度條filesize($path)
Content-Transfer-Encoding傳輸編碼binary
Accept-Ranges聲明支持斷點續傳bytes
Cache-Control緩存策略must-revalidate, post-check=0, pre-check=0(下載不緩存)
Pragma / Expires兼容 HTTP/1.0 緩存控制public / 0

二、基礎版實現(小文件、全量讀入內存)

<?php
function downloadFile(string $filePath, string $fileName = ''): void
{
    if (!is_file($filePath) || !is_readable($filePath)) {
        http_response_code(404);
        exit('文件不存在或不可讀');
    }

    $fileName = $fileName ?: basename($filePath);
    $fileSize = filesize($filePath);

    // 發送響應頭
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="' . rawurlencode($fileName) . '"; filename*=UTF-8\'\'' . rawurlencode($fileName));
    header('Content-Length: ' . $fileSize);
    header('Content-Transfer-Encoding: binary');
    header('Accept-Ranges: bytes');
    header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    header('Pragma: public');
    header('Expires: 0');

    // 輸出文件內容
    readfile($filePath); // 內部已優化,比 file_get_contents + echo 更省內存
    exit;
}

三、生產級分塊傳輸類(大文件、斷點續傳、限速、權限)

<?php
/**
 * 生產級文件下載響應器
 * 支持:分塊讀取、Range 斷點續傳、限速、權限回調、中文文件名
 */
class FileDownloader
{
    private const CHUNK_SIZE = 1024 * 1024; // 1MB 分塊
    private const DEFAULT_SPEED_LIMIT = 0;  // 0 = 不限速 (B/s)

    private string $filePath;
    private string $fileName;
    private int $fileSize;
    private ?callable $authCallback;
    private int $speedLimit;
    private int $start = 0;
    private int $end = 0;

    public function __construct(
        string $filePath,
        string $fileName = '',
        ?callable $authCallback = null,
        int $speedLimit = self::DEFAULT_SPEED_LIMIT
    ) {
        $this->filePath = $filePath;
        $this->fileName = $fileName ?: basename($filePath);
        $this->authCallback = $authCallback;
        $this->speedLimit = $speedLimit;
    }

    public function send(): void
    {
        $this->validate();
        $this->parseRangeHeader();
        $this->sendHeaders();
        $this->streamBody();
    }

    private function validate(): void
    {
        if (!is_file($this->filePath) || !is_readable($this->filePath)) {
            $this->error(404, '文件不存在');
        }
        $this->fileSize = filesize($this->filePath);
        if ($this->authCallback && !$this->authCallback($this->filePath)) {
            $this->error(403, '權限不足');
        }
    }

    private function parseRangeHeader(): void
    {
        $range = $_SERVER['HTTP_RANGE'] ?? '';
        if (preg_match('/bytes=(\d+)-(\d*)/', $range, $matches)) {
            $this->start = (int)$matches[1];
            $this->end = $matches[2] !== '' ? (int)$matches[2] : $this->fileSize - 1;
            $this->start = max(0, min($this->start, $this->fileSize - 1));
            $this->end = max($this->start, min($this->end, $this->fileSize - 1));
            header('HTTP/1.1 206 Partial Content');
            header('Content-Range: bytes ' . $this->start . '-' . $this->end . '/' . $this->fileSize);
        } else {
            $this->start = 0;
            $this->end = $this->fileSize - 1;
        }
    }

    private function sendHeaders(): void
    {
        $encodedName = rawurlencode($this->fileName);
        header('Content-Type: application/octet-stream');
        header("Content-Disposition: attachment; filename=\"$encodedName\"; filename*=UTF-8''$encodedName");
        header('Content-Length: ' . ($this->end - $this->start + 1));
        header('Content-Transfer-Encoding: binary');
        header('Accept-Ranges: bytes');
        header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
        header('Pragma: public');
        header('Expires: 0');
    }

    private function streamBody(): void
    {
        $handle = fopen($this->filePath, 'rb');
        if (!$handle) $this->error(500, '無法打開文件');

        fseek($handle, $this->start);
        $remaining = $this->end - $this->start + 1;
        $lastTime = microtime(true);

        while ($remaining > 0 && !feof($handle) && connection_status() === CONNECTION_NORMAL) {
            $chunkSize = min(self::CHUNK_SIZE, $remaining);
            $data = fread($handle, $chunkSize);
            if ($data === false) break;
            
            echo $data;
            @flush(); // 刷新輸出緩衝區
            
            $remaining -= strlen($data);
            
            // 限速邏輯
            if ($this->speedLimit > 0) {
                $elapsed = microtime(true) - $lastTime;
                $expected = strlen($data) / $this->speedLimit;
                if ($elapsed < $expected) {
                    usleep(($expected - $elapsed) * 1_000_000);
                }
                $lastTime = microtime(true);
            }
        }
        fclose($handle);
        exit;
    }

    private function error(int $code, string $message): void
    {
        http_response_code($code);
        header('Content-Type: text/plain; charset=utf-8');
        exit($message);
    }
}

// 使用示例
$downloader = new FileDownloader(
    '/data/exports/report_2024.xlsx',
    '年度報表.xlsx',
    function ($path) { return checkUserPermission($path); }, // 權限回調
    1024 * 512 // 限速 512 KB/s
);
$downloader->send();

四、關鍵細節深度解析

1. 中文文件名編碼:RFC 5987 標準

// 兼容舊瀏覽器 + 現代標準
header("Content-Disposition: attachment; filename=\"$encodedName\"; filename*=UTF-8''$encodedName");
// $encodedName = rawurlencode($fileName)  // 空格→%20,非 ASCII→%XX

舊版 IE 只認 filename,現代瀏覽器優先用 filename*。雙重寫法全兼容。

2. 斷點續傳:HTTP Range 請求

  • 客戶端發送 Range: bytes=1000-(從 1000 字節到結尾)
  • 服務端響應 206 Partial Content + Content-Range: bytes 1000-1999/2000
  • 支持下載工具、瀏覽器斷點續傳、視頻拖拽播放

3. 內存控制:分塊讀取 + flush()

readfile() 內部已分塊,但無法控制限速、Range。手動 fread(1MB) + flush() 可精確控制內存峰值 ≈ 分塊大小,支撐 GB 級文件下載不爆內存。

4. 安全實踐清單

  • ✅ 路徑遍歷防護:realpath($path) 確保在允許目錄內,禁止 ../../etc/passwd
  • ✅ 權限校驗:下載前回調業務邏輯(如訂單歸屬、會員等級、簽名驗證)
  • ✅ 限速防盜鏈:單 IP 限速、單用戶並發連接數限制
  • ✅ 簽名 URL:生成帶過期時間、HMAC 簽名的一次性下載鏈接,避免熱鏈接
  • ✅ 日誌審計:記錄用戶、文件、IP、時間、Range、User-Agent

五、Nginx X-Accel-Redirect / Apache X-Sendfile 卸載方案(高併發首選)

PHP 負責權限校驗、生成內部路徑,Web 服務器直接發送文件,PHP 進程立即釋放:

<?php
// PHP 僅校驗權限、設置內部重定向頭
header('X-Accel-Redirect: /protected/files/report.xlsx'); // Nginx
// header('X-Sendfile: /protected/files/report.xlsx');     // Apache (需 mod_xsendfile)
header('Content-Disposition: attachment; filename="report.xlsx"');
header('Content-Type: application/octet-stream');
exit;
// Nginx 配置:
// location /protected/files/ {
//     internal;                    // 禁止直接外部訪問
//     alias /data/exports/;        // 真實路徑映射
//     sendfile on;                 // 內核零拷貝
//     tcp_nopush on;
// }

優勢:PHP 進程毫秒級釋放、內核零拷貝零 CPU、支持超大文件、天然斷點續傳。生產環境大文件下載強烈推薦此架構。

六、總結選型指南

| 文件大小 | 併發量 | 推薦方案 | |---|---|---| | < 10MB | 低 | readfile() 基礎版 | | 10MB - 1GB | 中 | FileDownloader 分塊類(支持 Range、限速) | | > 1GB 或 高併發 | 高 | Nginx X-Accel-Redirect 卸載到 Web 服務器 | | 需權限控制 | 任意 | PHP 校驗 + X-Accel-Redirect 混合模式 |

掌握從基礎 Headers 到生產級分塊傳輸、再到 Web 服務器卸載的完整鏈路,能讓你根據業務體量靈活選型,構建高性能、安全、用戶體驗優異的文件下載服務。

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