使用 PHP 和 Guzzle 流式传输远程文件
Stream remote file with PHP and Guzzle
我的应用程序应该将一个大文件流式传输回浏览器,即远程服务器。目前该文件由本地 NodeJS 服务器提供。
我正在使用 25GB 的 VirtualBox 磁盘映像,只是为了确保它在流式传输时没有存储在内存中。
这是我正在努力处理的相关代码
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Stream\LimitStream;
$client = new \GuzzleHttp\Client();
logger('==== START REQUEST ====');
$res = $client->request('GET', 'http://localhost:3002/', [
'on_headers' => function (\Psr\Http\Message\ResponseInterface $response) use ($res) {
$length = $response->getHeaderLine('Content-Length');
logger('Content length is: ' . $length);
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="testfile.zip"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . $length);
}
]);
$body = $res->getBody();
$read = 0;
while(!$body->eof()) {
logger("Reading chunk. " . $read);
$chunk = $body->read(8192);
$read += strlen($chunk);
echo $chunk;
}
logger('Read ' . $read . ' bytes');
logger("==== END REQUEST ====\n\n");
function logger($string) {
$myfile = fopen("log.txt", "a") or die ('Unable to open log file');
fwrite($myfile, "[" . date("d/m/Y H:i:s") . "] " . $string . "\n");
fclose($myfile);
}
即使 $body = $res->getBody();
应该 return 一个流,它很快用交换数据填满了磁盘,这意味着它试图在流回客户端之前将其保存在内存中,但这是不是预期的行为。我错过了什么?
$res = $client->request('GET', 'http://localhost:3002/', [
'stream' => true,
'sink' => STDOUT, // Default output stream.
'on_headers' => ...
]);
添加这些内容后,您将能够逐块流式传输响应,而无需任何其他代码将响应 body 流复制到 STDOUT(使用 echo
)。
但通常你不想这样做,因为你需要为每个活动客户端设置一个 PHP(php-fpm 或 Apache 的 mod_php)进程。
如果您只想提供秘密文件,请尝试使用 "internal redirect":通过 X-Accel-Redirect header 用于 nginx 或 X-Sendfile 用于 Apache。您将获得相同的行为,但资源使用较少(因为在 nginx 的情况下高度优化的事件循环)。有关配置的详细信息,您可以阅读官方文档,当然也可以阅读其他 SO 问题(例如 this one)。
我的应用程序应该将一个大文件流式传输回浏览器,即远程服务器。目前该文件由本地 NodeJS 服务器提供。
我正在使用 25GB 的 VirtualBox 磁盘映像,只是为了确保它在流式传输时没有存储在内存中。 这是我正在努力处理的相关代码
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Stream\LimitStream;
$client = new \GuzzleHttp\Client();
logger('==== START REQUEST ====');
$res = $client->request('GET', 'http://localhost:3002/', [
'on_headers' => function (\Psr\Http\Message\ResponseInterface $response) use ($res) {
$length = $response->getHeaderLine('Content-Length');
logger('Content length is: ' . $length);
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="testfile.zip"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . $length);
}
]);
$body = $res->getBody();
$read = 0;
while(!$body->eof()) {
logger("Reading chunk. " . $read);
$chunk = $body->read(8192);
$read += strlen($chunk);
echo $chunk;
}
logger('Read ' . $read . ' bytes');
logger("==== END REQUEST ====\n\n");
function logger($string) {
$myfile = fopen("log.txt", "a") or die ('Unable to open log file');
fwrite($myfile, "[" . date("d/m/Y H:i:s") . "] " . $string . "\n");
fclose($myfile);
}
即使 $body = $res->getBody();
应该 return 一个流,它很快用交换数据填满了磁盘,这意味着它试图在流回客户端之前将其保存在内存中,但这是不是预期的行为。我错过了什么?
$res = $client->request('GET', 'http://localhost:3002/', [
'stream' => true,
'sink' => STDOUT, // Default output stream.
'on_headers' => ...
]);
添加这些内容后,您将能够逐块流式传输响应,而无需任何其他代码将响应 body 流复制到 STDOUT(使用 echo
)。
但通常你不想这样做,因为你需要为每个活动客户端设置一个 PHP(php-fpm 或 Apache 的 mod_php)进程。
如果您只想提供秘密文件,请尝试使用 "internal redirect":通过 X-Accel-Redirect header 用于 nginx 或 X-Sendfile 用于 Apache。您将获得相同的行为,但资源使用较少(因为在 nginx 的情况下高度优化的事件循环)。有关配置的详细信息,您可以阅读官方文档,当然也可以阅读其他 SO 问题(例如 this one)。