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

PHP 實現大文件下載斷點續傳:HTTP Range 請求完整實現指南

     編輯:PHP 相關知識

PHP 實現大文件下載斷點續傳:HTTP Range 請求完整實現指南

大文件下載斷點續傳是提升用戶體驗、節省頻寬的關鍵技術。本文詳細實現基於 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: 500
  • 關鍵頭:Accept-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

效能優化建議

  • X-Sendfile / X-Accel-Redirect:Nginx/Apache 直接發送文件,PHP 僅做權限驗證,零拷貝、零記憶體佔用
  • 預讀緩衝:增大 chunk_size 至 64KB-256KB,減少系統調用
  • 限速控制:配合 usleep() 實現下載限速
  • 多線程下載支援:正確處理多個 Range 請求,返回 multipart/byteranges

Nginx X-Accel-Redirect 方案 (推薦生產環境)

<?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 服務器協作優化,能構建企業級的文件分發服務。

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