You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.06 17:15:03