必须在readfile()前完成日志记录,使用file_append | lock_ex写入含ip、ua、文件路径、时间戳等字段的日志;推荐fopen()+fpassthru()以精细控制日志时机与错误处理,避免缓冲干扰和权限问题。

PHP 7.4中用readfile()下载文件时如何安全写入日志
直接调用readfile()会清空输出缓冲并立即发送文件,此时再用error_log()或file_put_contents()写日志,很可能因脚本提前退出而失败。必须在发送文件前完成日志记录,且不能干扰HTTP头。
- 先调用
ob_start()确保输出缓冲开启(即使默认关闭也保险) - 手动设置
Content-Disposition等头,避免readfile()触发隐式flush() - 日志写入必须放在
readfile()之前,且使用FILE_APPEND | LOCK_EX防止并发覆盖 - 记录字段建议包含:
$_SERVER['REMOTE_ADDR']、$_SERVER['HTTP_USER_AGENT']、文件路径、时间戳、$_GET['filename'](若从参数获取)
为什么fopen() + fpassthru()比readfile()更适合带日志的下载
readfile()是原子操作,无法插入日志点;而fopen() + fpassthru()允许你在打开句柄后、传输前完成日志写入,控制粒度更细,也方便做权限校验和文件存在性检查。
- 用
fopen($filepath, 'rb')打开只读二进制流,失败时可捕获错误并记录到日志而非暴露路径 - 成功后立刻调用
file_put_contents($logpath, $logline . "\n", FILE_APPEND | LOCK_EX) - 再调用
fpassthru($fp),它不会自动fclose(),记得显式fclose($fp)或用finally块(PHP 7.4支持) - 注意
fpassthru()可能受output_buffering影响,建议在ini_set('output_buffering', 'Off')后执行
日志文件路径和权限的常见坑
日志写入失败往往不是代码问题,而是www-data(或对应Web用户)对日志目录无写权限,或磁盘满、SELinux限制。
- 日志路径别写相对路径如
./logs/download.log,用__DIR__ . '/logs/download.log'确保位置明确 - 创建日志目录时运行
chown www-data:www-data logs/ && chmod 755 logs/,日志文件本身用chmod 644 - 上线前用
touch /path/to/log && chmod 644 /path/to/log测试是否可写 - 避免把日志写到
/tmp——某些系统会定期清理,且可能被其他服务轮转
怎么防止日志爆炸和性能拖慢下载
高频下载场景下,同步写磁盘会让每个请求多出几毫秒延迟,还可能因IO阻塞导致超时。得加一层轻量级缓冲或异步机制。
- 用
file_put_contents()的FILE_APPEND足够快,但不要每下载1KB就写一次日志——只在每次完整文件传输前写一行 - 不推荐用数据库记下载日志,除非已有连接池且表有合适索引;纯文件+定时切割更稳妥
- 可用
date('Y-m-d')动态生成日志名,如download_2024-06-15.log,配合crontab每日压缩归档 - 若QPS超50,考虑用
syslog()推送到rsyslog,由其批量落盘,减少PHP层IO压力
$_SERVER['HTTP_REFERER']很常见,但它对溯源盗链很重要;另外别忘了检查open_basedir限制是否拦住了日志路径。php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











