使用 readfile() 提供大檔案下載時,瀏覽器進度條卡住、記憶體飆升、甚至 PHP 進程 OOM。
@set_time_limit(0); // 移除執行時間限制
ini_set('memory_limit', '16M'); // 限制 PHP 記憶體使用
ob_end_clean(); // 【關鍵】清空並關閉輸出緩衝區
@readfile($path); // 直接輸出檔案流
output_buffering)readfile() 讀取檔案內容寫入緩衝區,而非直接發送給客戶端php.net 官方文檔:This function discards the contents of the topmost output buffer and turns off this output buffering.
// 錯誤順序:readfile 先寫入緩衝,ob_end_clean 再清空 → 已佔用記憶體
readfile($path);
ob_end_clean();
// 正確順序:先關閉緩衝,readfile 直接流式傳輸
ob_end_clean();
readfile($path);
function downloadFile($filePath, $fileName = '') {
if (!is_file($filePath)) {
header('HTTP/1.1 404 Not Found');
exit('File not found');
}
$fileName = $fileName ?: basename($filePath);
$fileSize = filesize($filePath);
// 關鍵設定
@set_time_limit(0);
@ini_set('memory_limit', '16M');
ob_end_clean(); // 必須在 header 之前
// HTTP 標頭
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . rawurlencode($fileName) . '"');
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . $fileSize);
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Expires: 0');
// 分塊讀取輸出 (更穩健)
$handle = fopen($filePath, 'rb');
while (!feof($handle)) {
echo fread($handle, 8192); // 8KB 塊
flush(); // 強制刷新到客戶端
}
fclose($handle);
exit;
}
if (isset($_SERVER['HTTP_RANGE'])) {
$range = $_SERVER['HTTP_RANGE'];
// 解析 Range: bytes=start-end
// 回應 206 Partial Content
// header('Content-Range: bytes start-end/filesize');
// fseek($handle, $start);
}
| 錯誤做法 | 問題 | 正確做法 |
|---|---|---|
| 只用 readfile() | 大檔案 OOM、卡死 | ob_end_clean() + 分塊讀取 |
| ob_start() 再 readfile() | 雙重緩衝,記憶體雙倍 | ob_end_clean() 關閉緩衝 |
| 未設 memory_limit | 受限於 php.ini 128M 預設值 | ini_set('memory_limit', '16M') |
| 未設 set_time_limit(0) | 大檔案超過 max_execution_time 中斷 | 移除時間限制 |
ob_end_clean() 是 PHP 大檔案下載的**核心關鍵**:它徹底關閉輸出緩衝,讓 readfile() 或分塊 fread/echo/flush 實現真正的流式傳輸,避免記憶體堆積與回應延遲。配合記憶體限制與時間限制移除,構建穩健的下載服務。