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

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

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

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

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

walkor

赞同来自: Qmore泽 pader

建议:
要发送给客户端的数据如果有几个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['REQUEST_URI'] == '/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 - phper

赞同来自:

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

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

walkor

赞同来自:

不客气

要回复问题请先登录注册