能否支持大文件的输出,或超大内容的输出

pader

我看了 Workerman 自带的简单的 WebServer,现在输出文件主要是用 $connection->close(file_get_contents($file)); 来实现的。

这里假设有很大的文件(假设几个G)需要给用户去下载,PHP 进程就会报内存超出的错误,这种情况下可能用 nginx 单架一个静态文件的输出更好。

但是打个比方我们并不简单的是下载一个文件,而是要输出某些超大内容,可能来自多个文件组合,又或者经过计算的动态超大结果,再可能需要隐藏真实文件的 URL 而动态输出,而在 apache 或者 nginx 下可以使用 x-sendfile 或者让 php 一段一段的输出来实现,但在 Workerman 下除了 $connection->close() 来输出内容,有什么办法可以分段输出吗?

或者是有别的某些方法来实现类似效果?

7074 3 4
3个回答

walkor

建议:
要发送给客户端的数据如果有几个G,这几个G的数据最好存储在本地磁盘上,避免占用服务器内存。然后根据客户端网络数据拥堵情况分段载入内存并发送。

注意:简单的将大文件分段发送不能避免内存爆的问题
假如10个G的文件发送给客户端,客户端接收速度很慢,虽然服务端将10G文件分成多个小文件发送,但是如果客户端接收速度远远低于服务端发送速度,仍然会导致服务端要发送的数据堆积在发送缓冲区中,导致内存爆掉。就像客户端带宽为10k/S,服务端以1M/S的速度发送,仍然会导致数据积压在服务器发送缓冲区导致内存爆掉。
正确的做法应该是根据客户端网络数据拥堵情况控制发送速度。

如何判断客户端网络数据放生拥堵?如何发送?
workerman提供了网络拥堵控制机制,即 onBufferFull和onBufferDrain事件(具体说明参见手册),当服务端向客户端的发送缓冲区满时(缓冲区大小可控制 参见手册)会产生onBufferFull事件,这时服务端应该停止向这个客户端再发送数据(停止从磁盘read数据到内存),因为onBufferFull发生时说明发送给客户端的数据发生拥堵。

而当发送缓冲区的数据全部发送给客户端后(发送缓冲区空了),将会放生onBufferDrain事件,这时服务端可以继续从磁盘read数据,继续向客户端发送。

通过onBufferFull和onBufferDrain事件可以方便控制网络拥堵,既能够减少内存消耗,又能以最快的速度将数据发送给客户端。

示例:
从磁盘发送大文件到客户端参见下面示例(使用的是http协议,其它协议也适用)

<?php
use Workerman\Worker;
require_once './Workerman/Autoloader.php';

$worker = new Worker('http://0.0.0.0:4236');
$worker->onMessage = function($connection, $data)
{
    if($_SERVER == '/favicon.ico')
    {
        return $connection->send("HTTP/1.0 404 Not Found\r\nContent-Length: 0\r\n\r\n", true);
    }
    // 这里发送的是一个大的pdf文件,如果是其它格式的文件,请修改下面代码中http头
    send_file($connection, "/your/path/xxx.pdf");
};

function send_file($connection, $file_name)
{
    if(!is_file($file_name))
    {
        $connection->send("HTTP/1.0 404 File Not Found\r\nContent-Length: 18\r\n\r\n404 File Not Found", true);
        return;
    }

    // ======发送http头======
    $file_size = filesize($file_name);
    $header = "HTTP/1.1 200 OK\r\n";
    // 这里写的Content-Type是pdf,如果不是pdf文件请修改Content-Type的值
    // mime对应关系参见 https://github.com/walkor/Workerman/blob/master/Protocols/Http/mime.types#L30
    $header .= "Content-Type: application/pdf\r\n";
    $header .= "Connection: keep-alive\r\n";
    $header .= "Content-Length: $file_size\r\n\r\n";
    $connection->send($header, true);

    // ======分段发送文件内容=======
    $connection->fileHandler = fopen($file_name, 'r');
    $do_write = function()use($connection)
    {
        // 对应客户端的连接发送缓冲区未满时
        while(empty($connection->bufferFull))
        {
            // 从磁盘读取文件
            $buffer = fread($connection->fileHandler, 8192);
            // 读不到数据说明文件读到末尾了
            if($buffer === '' || $buffer === false)
            {
                return;
            }
            $connection->send($buffer, true);
        }
    };
    // 发生连接发送缓冲区满事件时设置一个标记bufferFull
    $connection->onBufferFull = function($connection)
    {
        // 赋值一个bufferFull临时变量给链接对象,标记发送缓冲区满,暂停do_write发送
        $connection->bufferFull = true;
    };
    // 当发送缓冲区数据发送完毕时触发
    $connection->onBufferDrain = function($connection)use($do_write)
    {
        $connection->bufferFull = false;
        $do_write();
    };
    // 执行发送
    $do_write();
}
Worker::runAll();

以上例子亲测ok,请试用

  • 暂无评论
pader

懂了,可以多次使用 send 并且利用缓冲区是否满,来控制输出。

非常感谢 walker 这么细心的回复!

  • 暂无评论
walkor

不客气

  • 暂无评论
年代过于久远,无法发表回答
🔝