在共享帶寬環境下,大文件下載佔滿頻寬會影響其他服務。PHP 通過控制輸出緩衝區刷新頻率與休眠間隔,可實現應用層面的下載限速。本文提供完整實現代碼並分析原理與局限。
<?php
// 配置參數
$localFile = 'test-file.zip'; // 服務器本地文件路徑
$downloadFile = 'your-download-name.zip'; // 用戶下載時的文件名
$downloadRate = 20.5; // 限速 KB/s
if (file_exists($localFile) && is_file($localFile)) {
// 1. 發送 HTTP 頭
header('Cache-Control: private');
header('Content-Type: application/octet-stream');
header('Content-Length: ' . filesize($localFile));
header('Content-Disposition: attachment; filename="' . $downloadFile . '"');
// 2. 刷新輸出緩衝,確保頭部先發送
flush();
// 3. 打開文件流
$handle = fopen($localFile, 'rb');
// 4. 分塊讀取、輸出、休眠循環
while (!feof($handle)) {
// 每次讀取 1 秒允許的字節數
$chunkSize = round($downloadRate * 1024);
$buffer = fread($handle, $chunkSize);
if ($buffer === false || $buffer === '') break;
echo $buffer;
// 關鍵:刷新到瀏覽器
flush();
// 休眠 1 秒,實現限速
sleep(1);
}
// 5. 關閉文件句柄
fclose($handle);
} else {
die('Error: The file ' . $localFile . ' does not exist!');
}
| 步驟 | 代碼 | 作用 |
|---|---|---|
| 發送頭部 | header() | 告知瀏覽器文件大小、類型、下載文件名 |
| 首次刷新 | flush() | 將頭部立即發送給客戶端,啟動下載對話框 |
| 分塊讀取 | fread($handle, $chunkSize) | 每次讀取限速允許的字節數 |
| 輸出數據 | echo $buffer | 寫入 PHP 輸出緩衝區 |
| 強制發送 | flush() | 將緩衝區數據推送到 Web Server 再到客戶端 |
| 時間控制 | sleep(1) | 暫停腳本執行 1 秒,實現每秒固定傳輸量 |
<?php
// 項目入口或 php.ini
// 禁用 PHP 輸出緩衝
ini_set('output_buffering', 'off');
// 或運行時
while (ob_get_level()) ob_end_clean();
// 禁用 Web Server 緩衝(Nginx)
// nginx.conf: proxy_buffering off; (反向代理場景)
// fastcgi_buffering off; (PHP-FPM 場景)
若不關閉緩衝,數據會在內存累積達到閾值才發送,導致限速失效、內存飆升。
function smartThrottle($handle, $rateKBps) {
$chunkSize = 8192; // 固定 8KB 塊
$interval = $chunkSize / ($rateKBps * 1024); // 每塊理想間隔(秒)
$lastTime = microtime(true);
while (!feof($handle)) {
$buffer = fread($handle, $chunkSize);
if ($buffer === false || $buffer === '') break;
echo $buffer;
flush();
$elapsed = microtime(true) - $lastTime;
$sleepTime = $interval - $elapsed;
if ($sleepTime > 0) {
usleep($sleepTime * 1_000_000); // 微秒級休眠
}
$lastTime = microtime(true);
}
}
優勢:傳輸更平滑,避免「傳輸-停頓」鋸齒模式,用戶體驗更好。
function sendFileWithRange($filePath, $rateKBps = 0) {
$fileSize = filesize($filePath);
$rangeHeader = $_SERVER['HTTP_RANGE'] ?? '';
$start = 0;
$end = $fileSize - 1;
$statusCode = 200;
if (preg_match('/bytes=(\d+)-(\d*)/', $rangeHeader, $matches)) {
$start = (int)$matches[1];
$end = $matches[2] !== '' ? (int)$matches[2] : $fileSize - 1;
$statusCode = 206; // Partial Content
}
$length = $end - $start + 1;
header('Accept-Ranges: bytes');
header('Content-Length: ' . $length);
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize);
header('Content-Type: application/octet-stream');
http_response_code($statusCode);
flush();
$handle = fopen($filePath, 'rb');
fseek($handle, $start);
$remaining = $length;
$chunkSize = 8192;
while (!feof($handle) && $remaining > 0) {
$readSize = min($chunkSize, $remaining);
$buffer = fread($handle, $readSize);
if ($buffer === false || $buffer === '') break;
echo $buffer;
flush();
if ($rateKBps > 0) {
$interval = $readSize / ($rateKBps * 1024);
usleep($interval * 1_000_000);
}
$remaining -= strlen($buffer);
}
fclose($handle);
}
sleep() 精度(通常 10ms)、調度延遲、網絡抖動
# nginx.conf
location /download/ {
limit_rate 200k; # 限速 200 KB/s
limit_rate_after 1m; # 前 1MB 不限速(體驗優化)
# 結合 auth_request 做權限驗證
auth_request /auth/check_download;
}
優勢:內核級限速、不佔用應用進程、支持斷點續傳、配置靈活。
<?php
// PHP 僅做權限檢查,交給 Nginx/Apache 發送文件
if ($userCanDownload($fileId)) {
$filePath = getRealPath($fileId);
// Nginx
header('X-Accel-Redirect: /protected_files/' . basename($filePath));
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $originalName . '"');
// Apache (mod_xsendfile)
// header('X-Sendfile: ' . $filePath);
exit;
}
Nginx 配置:
location /protected_files/ {
internal; # 只允許內部重定向訪問
alias /var/www/files/;
limit_rate 200k;
}
阿里云 OSS、AWS S3、騰訊云 COS 均支持生成帶有效期、限速參數的預簽名 URL,完全卸載帶寬壓力。
PHP 實現下載限速適合低併發、無反向代理、快速原型場景。核心要點:關閉所有緩衝、分塊輸出+flush+sleep、處理斷點續傳。生產環境強烈建議遷移到 Nginx limit_rate + X-Accel-Redirect 架構,將流量控制下沉到網關層,PHP 僅負責業務邏輯鑒權,實現關注點分離與高併發支撐。