PHP脚本下载public_html外文件时,低速网络下强制下载中途失败
这种低速网络下的下载中断问题我之前帮朋友排查过,核心原因基本都是超时限制或者输出缓冲机制拖了后腿——毕竟低速环境下下载大文件的耗时会远超PHP和Web服务器的默认超时阈值,另外分块读取的策略也可能需要调整。下面给你一步步拆解解决方案:
1. 彻底禁用PHP的执行超时
默认情况下PHP的max_execution_time一般设为30秒或者60秒,低速下载大文件肯定不够用。你需要在脚本开头添加这几行,完全取消超时限制:
set_time_limit(0); ini_set('max_execution_time', 0); ini_set('max_input_time', 0);
注意:如果你的服务器有全局的超时强制限制(比如某些托管服务商),可能需要联系服务商调整,但大部分情况下这几行就能解决PHP层面的超时问题。
2. 关闭输出缓冲,实时刷新内容
输出缓冲会把内容攒到一定量才发送给客户端,低速网络下缓冲满了之后可能会触发连接中断。你需要禁用缓冲并开启实时刷新:
ob_end_clean(); // 清空并关闭之前的缓冲 ob_implicit_flush(true); // 自动刷新输出
在分块输出文件内容的时候,还要手动调用flush()和ob_flush()确保内容立刻发送给客户端。
3. 优化分块读取策略,适配低速网络
之前的分块如果太大,可能会导致客户端接收超时;太小又会增加服务器的IO压力。建议用16KB-32KB的分块大小,同时每次输出后检查客户端连接状态,如果用户断开连接就停止输出,避免浪费资源:
$chunkSize = 1024 * 16; // 16KB分块 $file = fopen($filePath, 'rb'); while (!feof($file)) { // 检查客户端是否已断开连接 if (connection_aborted()) { fclose($file); exit; } echo fread($file, $chunkSize); flush(); ob_flush(); // 给服务器一点缓冲时间,避免IO过载 usleep(1000); } fclose($file);
4. 调整Web服务器的超时设置
光PHP超时取消还不够,Web服务器(Apache/Nginx)本身也有超时限制,比如:
- Apache:修改
httpd.conf里的Timeout指令,设为300秒以上(比如Timeout 600) - Nginx:在配置文件里添加或修改
proxy_read_timeout和fastcgi_read_timeout,比如proxy_read_timeout 600s;
这些设置确保服务器不会主动断开长时间的下载连接。
5. 可选但推荐:添加断点续传支持
低速网络下用户很可能中途断开后重新下载,支持断点续传不仅能避免从头开始,还能降低连接中断的概率。通过处理HTTP_RANGE请求头来实现,完整的示例代码整合了上面所有优化点:
<?php // 禁用超时限制 set_time_limit(0); ini_set('max_execution_time', 0); ini_set('max_input_time', 0); // 关闭输出缓冲 ob_end_clean(); ob_implicit_flush(true); // 目标文件路径(public_html外) $filePath = '/absolute/path/to/your/file'; $fileName = basename($filePath); if (!file_exists($filePath) || !is_readable($filePath)) { http_response_code(404); exit("File not found or unreadable"); } $fileSize = filesize($filePath); $chunkSize = 1024 * 16; // 16KB分块 // 处理断点续传 $start = 0; $end = $fileSize - 1; if (isset($_SERVER['HTTP_RANGE'])) { if (preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $matches)) { $start = intval($matches[1]); $end = !empty($matches[2]) ? intval($matches[2]) : $fileSize - 1; // 确保范围合法 $end = min($end, $fileSize - 1); $start = max($start, 0); http_response_code(206); // 部分内容响应 header("Content-Range: bytes $start-$end/$fileSize"); } } // 设置下载响应头 header("Content-Type: application/octet-stream"); header("Content-Disposition: attachment; filename=\"$fileName\""); header("Content-Length: " . ($end - $start + 1)); header("Cache-Control: no-cache"); header("Pragma: no-cache"); header("Accept-Ranges: bytes"); // 声明支持断点续传 // 读取并输出文件 $file = fopen($filePath, 'rb'); fseek($file, $start); $currentPos = $start; while ($currentPos <= $end && !feof($file)) { if (connection_aborted()) { fclose($file); exit; } $readBytes = min($chunkSize, $end - $currentPos + 1); echo fread($file, $readBytes); flush(); ob_flush(); $currentPos += $readBytes; usleep(1000); } fclose($file); exit; ?>
最后还要排查一下服务器的防火墙、安全插件(比如ModSecurity),有些会拦截长时间的HTTP请求;另外确保PHP的memory_limit足够,不过分块读取的方式不会占用太多内存,一般默认值就够。
内容的提问来源于stack exchange,提问作者Eduardo Amaral

