PHP 8.0 限速下载须禁用 readfile(),改用 fopen()+fread() 分块读取并动态 usleep() 补偿;前置清理输出缓冲;中文文件名用 RFC 5987 编码(filename*=UTF-8'');显式禁用超时、关闭缓冲、忽略用户中断;路径须 realpath() 归一化并白名单校验。

PHP 8.0 中限速下载必须避开 readfile() 直接调用
因为 readfile() 是一次性把整个文件读进内存再输出,对大文件极易触发 memory_limit 超限或超时(max_execution_time),尤其在 PHP 8.0 默认更严格内存管理下。限速逻辑(如 sleep(1))若套在 readfile() 外层,实际无效——它根本没分块,压根不给你插手机会。
正确做法是:用 fopen() + fread() 手动分块读取,每块后加限速控制。同时必须前置清理缓冲:ob_end_clean() 或 while (ob_get_level()) ob_end_clean();,否则任何已缓存的空格/BOM 都会让 header 失效。
限速核心:用 fread() 控制每秒字节数,别依赖 sleep(1) 粗粒度等待
sleep(1) 看似简单,但实际速率波动极大:网络延迟、IO 调度、PHP 进程抢占都会导致“1 秒内发不出预期数据”,最终下载速度远低于设定值,甚至卡顿。更稳的方式是计算每次 fread() 的字节数,并用 usleep() 补偿误差。
- 设目标速率 512KB/s → 每次读 512 × 1024 字节,读完立刻
usleep(1000000)(1 秒) - 若某次
fread()实际只读了 300KB(比如文件尾部),则按比例缩短等待:usleep((300 / 512) * 1000000) - 务必检查
fread()返回值是否为false或 0,避免死循环
中文文件名在 PHP 8.0 下必须用 RFC 5987 编码,不能只靠 urlencode()
Chrome/Firefox 对 filename="中文.pdf" 已基本不兼容;urlencode() 在 UTF-8 下生成的字符串(如 %E4%B8%AD%E6%96%87.pdf)仍会被部分浏览器截断。PHP 8.0 应直接构造 Content-Disposition 的 filename* 字段:
立即学习“PHP免费学习笔记(深入)”;
header('Content-Disposition: attachment; filename="' . rawurlencode($basename) . '"; filename*=UTF-8\'\'' . rawurlencode($basename));注意:rawurlencode() 比 urlencode() 更符合 RFC,且不转义 / 和 .;filename* 后的单引号不可省略,否则 Firefox 解析失败。
PHP 8.0 运行时需显式放宽超时与内存限制
限速下载本质是延长脚本生命周期,PHP 8.0 默认 max_execution_time=30 秒,一个 100MB 文件以 100KB/s 下载需约 17 分钟,必然超时。必须在脚本开头主动调整:
-
ini_set('max_execution_time', 0);(禁用超时) -
ini_set('output_buffering', 'Off');(关闭输出缓冲,避免阻塞flush()) -
ignore_user_abort(true);(用户关浏览器也不中断传输) - 避免在函数内调用
ini_set()—— PHP 8.0 对某些配置项的运行时修改已被弃用,必须放在最外层作用域
真正难处理的不是代码逻辑,而是路径校验和权限控制:限速掩盖不了越权风险。传入的 $file_path 必须经 realpath() 归一化,并确认其前缀落在白名单目录内(如 /var/www/app/storage/),否则 ../../etc/passwd 一类攻击会直接绕过限速,变成任意文件读取。



















