PHP如何实现外部脚本下载文件时实时将内容流式返回给客户端
问题根因
你当前使用的 shell_exec() 会等待外部命令完全执行结束后才一次性返回所有输出,无法实现流式实时传输。要达到边下载边推送的效果,需要通过进程控制函数直接读取外部命令的标准输出管道,同步将数据发往客户端。
实现代码
// 提前设置无执行时间限制,避免大文件传输超时 set_time_limit(0); // 清空所有层级的PHP输出缓冲,避免内容被缓存 while (ob_get_level()) { ob_end_clean(); } // 响应头配置 header('Content-Description: File Transfer'); header('Content-Type: audio/mpeg'); // 可替换为你实际的音频MIME类型,也可以保留application/octet-stream header('Content-Disposition: attachment; filename="' . basename($filename) . '"'); header('Expires: 0'); header('Cache-Control: must-revalidate'); header('Pragma: public'); // *注意不要加Content-Length头*,流式传输无法提前获知总文件大小,添加后会导致客户端下载异常 // 配置进程描述符,分别对应标准输入、标准输出、错误输出 $descriptorspec = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'] ]; // 转义命令参数,避免注入风险 $command = escapeshellcmd("./get-file ") . escapeshellarg($file_id); // 启动外部进程 $process = proc_open($command, $descriptorspec, $pipes); if (is_resource($process)) { // 关闭不需要的标准输入管道 fclose($pipes[0]); // 把标准输出设为非阻塞模式,避免读取卡住 stream_set_blocking($pipes[1], false); // 循环读取输出直到进程结束 while (!feof($pipes[1])) { // 每次读取8KB二进制数据,可根据实际需求调整块大小 $chunk = fread($pipes[1], 8192); if (!empty($chunk)) { echo $chunk; // 强制刷新输出缓冲区,将内容推送到web服务器 flush(); } // 没有读到内容时休眠100ms,避免CPU空转占用过高 usleep(100000); } // 资源回收 fclose($pipes[1]); fclose($pipes[2]); proc_close($process); } exit;
注意事项
- 如果你使用Nginx作为web服务器,默认开启了代理缓冲,会缓存PHP输出的内容直到缓冲区满才发往客户端,需要在对应站点配置中给这个接口添加
proxy_buffering off;规则,才能实现真正的实时传输。 fread本身是二进制安全的,不管是音频还是其他二进制格式文件都可以正常读取,不需要额外做编码处理。- 如果你的场景需要保留本地文件备份,也可以用循环读取本地文件增量的方案:先启动后台进程下载文件,然后PHP循环检测文件大小变化,读取新增的字节发送给客户端,不过该方案会多占用磁盘IO,性能不如直接读取进程输出的方案。
内容的提问来源于stack exchange,提问作者fotis anonwins
相关产品推荐
相关产品推荐

