output_buffering = 0 是 download 失败的常见元凶:默认开启的输出缓冲会截断二进制流,需在 readfile() 前用 ini_set('output_buffering', 0) 关闭,并清除 bom、警告、空行等污染,大文件还需流式读取+flush()。

output_buffering = 0 是 download 失败的常见元凶
PHP 的 readfile() 或 fpassthru() 下载大文件时内容不全,十有八九是输出缓冲没关干净。默认开启的 output_buffering 会把所有输出暂存在内存里,等脚本结束或缓冲区满才一次性发给客户端——这对 HTML 页面可能无感,但对二进制文件就是灾难:中间任何一次缓冲溢出、超时或内存不足,都会导致响应体被截断,而浏览器只收到前半段。
验证方法很简单:var_dump(ini_get('output_buffering'))。若返回 "0" 或空字符串,说明缓冲已关;若返回数字(如 "4096"),说明缓冲开着且大小固定;若返回 "On",则启用但大小由系统决定(通常也是 4096 字节)。
- 必须在
readfile()前调用ini_set('output_buffering', 'off')或ini_set('output_buffering', 0) - CLI 模式下该设置依然生效,但注意
ob_flush()对终端无效,仅影响ob_get_contents()类函数 - 如果用的是 ThinkPHP 的
Response::download(),它内部不自动关缓冲,得手动加ob_end_clean()和flush() - 别只改代码层——检查 php.ini 中是否被其他地方设回了非零值(比如某些 SaaS 环境强制开启)
header 发送后还有输出?BOM、Warning、空行都会污染二进制流
即使 output_buffering 关了,下载文件仍损坏或大小不对,大概率是响应体里混进了不该有的字符。HTTP 响应头一旦发出,后续所有输出都算响应体;而 readfile() 输出的是纯二进制,前面哪怕多一个空格、BOM 或 PHP Warning,都会让文件开头错位,校验失败。
典型污染源:
- UTF-8 文件带 BOM:用编辑器另存为“UTF-8 无 BOM”格式,或用
xxd yourfile.php | head查看开头是否为ef bb bf - include 的配置文件末尾有空行或换行符
- 错误报告未关闭:
error_reporting(0)+ini_set('display_errors', 'Off')必须配齐 - 中文文件名没做 RFC 5987 编码,某些 UA 会 fallback 到乱码并插入不可见字符
调试技巧:headers_sent($file, $line) 能定位哪行代码提前发了 header;ob_list_handlers() 可查当前激活的缓冲层,避免嵌套未清理。
大文件下载卡死或内存溢出?不是 output_buffering 的锅,是传输方式错了
output_buffering 控制的是 PHP 层缓冲,但它不解决大文件读入内存的问题。readfile() 会把整个文件 load 进内存再吐出去,几百 MB 的 PDF 或 ZIP 很容易触发 Allowed memory size exhausted,尤其在 shared hosting 上。
正确做法是流式分块读取:
- 用
fopen($path, 'rb')打开文件,配合fread($fp, 8192)每次读 8KB - 每读一块就
echo一次,并紧跟flush()强制推送到客户端 - 必须禁用 zlib 压缩:
ini_set('zlib.output_compression', 'Off'),否则flush()会被压缩层拦截 - 设超时:
set_time_limit(0),但建议加进度校验(比如每 10MB 检查connection_status() === CONNECTION_NORMAL)
ThinkPHP 用户注意:Response::download() 默认不做流式处理,大文件务必绕过它,手写 fopen+fread 逻辑。
Content-Length 不匹配?header 和实际字节数对不上
很多下载逻辑写了 header('Content-Length: ' . filesize($file)),但最终文件大小和 header 声明不符。原因往往不是 output_buffering,而是:
- 文件在
readfile()执行中被其他进程修改(如日志轮转) - 路径指向的是符号链接,
filesize()返回目标文件大小,但readfile()实际读的是链接本身(极少见,但存在) - 用了
ob_start()但忘了ob_end_clean(),导致缓冲里残留了之前 echo 的内容,叠加在文件数据前 - Windows 下路径分隔符问题:
str_replace('\', '/', $path)不彻底,realpath()后再比对更稳
最稳妥的方案是别依赖 Content-Length:现代浏览器支持 chunked transfer encoding,去掉这行 header 反而更健壮;若必须保留,请在 readfile() 后用 ob_get_length() 校验实际输出长度是否匹配。
复杂点在于,这些环节(缓冲、编码、流式、header)经常交织在一起。一个 ob_end_clean() 没调对,可能掩盖了 BOM 问题;ini_set('output_buffering', 0) 写在 readfile() 后面,等于白设。顺序和上下文,比单点配置更重要。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!








