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

PHP 實作檔案下載速度限制:節省頻寬與實作進度條的關鍵技術

     編輯:PHP 相關知識

限制檔案下載速度不僅能避免瞬間頻寬耗盡導致網路擁塞,更是實作下載進度條的基礎。本文詳細解析 PHP 限速下載的核心原理與完整實作代碼。

為什麼需要限速下載?

  • 頻寬保護:防止大檔案一次性傳輸佔滿伺服器頻寬,影響其他服務
  • 公平分享:多用戶同時下載時確保各連線獲得合理速度
  • 進度條實作:分塊輸出配合 flush() 才能即時更新前端進度
  • 防止中斷:避免大量資料一次衝入緩衝區導致腳本超時或連線斷開

核心技術原理

<?php
/**
 * 限速下載核心邏輯
 */
$file_path = '/path/to/large_file.zip';
$download_speed = 20.5 * 1024;  // 限制 20.5 KB/s (位元組/秒)

if (!file_exists($file_path)) {
    header('HTTP/1.1 404 Not Found');
    exit('檔案不存在');
}

$file_size = filesize($file_path);
$file_name = basename($file_path);

// 關鍵 Header 設定
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $file_name . '"');
header('Content-Length: ' . $file_size);
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');

// 開啟檔案
$fp = fopen($file_path, 'rb');
if (!$fp) {
    header('HTTP/1.1 500 Internal Server Error');
    exit('無法開啟檔案');
}

// 分塊輸出迴圈
$buffer_size = 1024 * 8;  // 8KB 緩衝區
$start_time = microtime(true);
$bytes_sent = 0;

while (!feof($fp) && connection_status() === CONNECTION_NORMAL) {
    $chunk = fread($fp, $buffer_size);
    echo $chunk;
    
    // 關鍵:強制輸出緩衝區到瀏覽器
    flush();
    
    $bytes_sent += strlen($chunk);
    
    // 限速計算:已傳輸位元組 / 目標速度 = 應耗費時間
    $elapsed = microtime(true) - $start_time;
    $target_elapsed = $bytes_sent / $download_speed;
    
    if ($target_elapsed > $elapsed) {
        // 睡眠補足時間差(微秒)
        usleep(($target_elapsed - $elapsed) * 1000000);
    }
}

fclose($fp);

// 連線中斷處理
if (connection_status() !== CONNECTION_NORMAL) {
    // 記錄未完成下載、清理暫存資源等
    error_log("下載中斷: $file_name, 已傳輸 $bytes_sent / $file_size bytes");
}
?>

關鍵函數解析

函數/特性作用
flush()將 PHP 輸出緩衝區強制發送至 Web Server(再由 Web Server 發送給瀏覽器)
connection_status()檢查客戶端連線狀態,CONNECTION_NORMAL 表示正常
usleep()微秒級睡眠,精確控制傳輸節奏
Content-Length告知瀏覽器檔案總大小,啟用進度條顯示
Accept-Ranges: bytes宣支援斷點續傳(需額外實作 Range 請求處理)

完整可用的下載類封裝

<?php
class ThrottledDownload {
    private $speed_limit;   // bytes/second
    private $buffer_size;   // bytes
    
    public function __construct($speed_kb = 100, $buffer_kb = 8) {
        $this->speed_limit = $speed_kb * 1024;
        $this->buffer_size = $buffer_kb * 1024;
    }
    
    public function download($file_path, $download_name = null) {
        if (!is_file($file_path) || !is_readable($file_path)) {
            throw new Exception('檔案不存在或不可讀');
        }
        
        $file_size = filesize($file_path);
        $download_name = $download_name ?: basename($file_path);
        
        // 發送 Header
        $this->sendHeaders($file_size, $download_name);
        
        // 執行限速傳輸
        return $this->streamFile($file_path, $file_size);
    }
    
    private function sendHeaders($file_size, $file_name) {
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="' . $file_name . '"');
        header('Content-Length: ' . $file_size);
        header('Content-Transfer-Encoding: binary');
        header('Accept-Ranges: bytes');
        header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
        header('Pragma: public');
    }
    
    private function streamFile($file_path, $file_size) {
        $fp = fopen($file_path, 'rb');
        if (!$fp) throw new Exception('無法開啟檔案');
        
        $bytes_sent = 0;
        $start_time = microtime(true);
        
        try {
            while (!feof($fp) && connection_status() === CONNECTION_NORMAL) {
                $chunk = fread($fp, $this->buffer_size);
                if ($chunk === false) break;
                
                echo $chunk;
                flush();
                
                $bytes_sent += strlen($chunk);
                $this->throttle($bytes_sent, $start_time);
            }
        } finally {
            fclose($fp);
        }
        
        return [
            'completed' => connection_status() === CONNECTION_NORMAL,
            'bytes_sent' => $bytes_sent,
            'total_size' => $file_size
        ];
    }
    
    private function throttle($bytes_sent, $start_time) {
        $elapsed = microtime(true) - $start_time;
        $target_elapsed = $bytes_sent / $this->speed_limit;
        
        if ($target_elapsed > $elapsed) {
            usleep(($target_elapsed - $elapsed) * 1000000);
        }
    }
}

// 使用範例
$downloader = new ThrottledDownload(50);  // 限速 50 KB/s
$result = $downloader->download('/var/files/large_video.mp4', 'my_video.mp4');

if ($result['completed']) {
    echo "下載完成";
} else {
    echo "下載中斷,已傳輸 {$result['bytes_sent']} bytes";
}
?>

前端進度條整合提示

配合限速下載,前端可使用 XMLHttpRequest 或 fetch 的 progress 事件即時更新進度條:

// JavaScript 範例
const xhr = new XMLHttpRequest();
xhr.open('GET', '/download.php?file=large.zip');
xhr.responseType = 'blob';

xhr.addEventListener('progress', (e) => {
    if (e.lengthComputable) {
        const percent = (e.loaded / e.total * 100).toFixed(1);
        document.getElementById('progress-bar').style.width = percent + '%';
        document.getElementById('progress-text').textContent = `${percent}%`;
    }
});

xhr.addEventListener('load', () => {
    // 處理 blob 下載儲存
    const url = URL.createObjectURL(xhr.response);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'large.zip';
    a.click();
});

xhr.send();

注意事項與最佳實踐

  • 關閉輸出緩衝:在腳本開頭加入 @ob_end_clean(); @ob_implicit_flush(true); 確保 flush() 生效
  • FastCGI/PHP-FPM 緩衝:Nginx fastcgi_buffering off; 或 Apache SetOutputFilter DEFLATE 可能干擾即時輸出
  • 大檔案記憶體:使用 fread 分塊讀取而非 file_get_contents 載入整個檔案
  • 斷點續傳:生產環境建議實作 HTTP Range 請求支援
  • 安全性:嚴格驗證檔案路徑,防止目錄遍歷攻擊(../etc/passwd)
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved