我看了 Workerman 自帶的簡(jiǎn)單的 WebServer,現(xiàn)在輸出文件主要是用 $connection->close(file_get_contents($file)); 來(lái)實(shí)現(xiàn)的。
這里假設(shè)有很大的文件(假設(shè)幾個(gè)G)需要給用戶去下載,PHP 進(jìn)程就會(huì)報(bào)內(nèi)存超出的錯(cuò)誤,這種情況下可能用 nginx 單架一個(gè)靜態(tài)文件的輸出更好。
但是打個(gè)比方我們并不簡(jiǎn)單的是下載一個(gè)文件,而是要輸出某些超大內(nèi)容,可能來(lái)自多個(gè)文件組合,又或者經(jīng)過(guò)計(jì)算的動(dòng)態(tài)超大結(jié)果,再可能需要隱藏真實(shí)文件的 URL 而動(dòng)態(tài)輸出,而在 apache 或者 nginx 下可以使用 x-sendfile 或者讓 php 一段一段的輸出來(lái)實(shí)現(xiàn),但在 Workerman 下除了 $connection->close() 來(lái)輸出內(nèi)容,有什么辦法可以分段輸出嗎?
或者是有別的某些方法來(lái)實(shí)現(xiàn)類似效果?
建議:
要發(fā)送給客戶端的數(shù)據(jù)如果有幾個(gè)G,這幾個(gè)G的數(shù)據(jù)最好存儲(chǔ)在本地磁盤(pán)上,避免占用服務(wù)器內(nèi)存。然后根據(jù)客戶端網(wǎng)絡(luò)數(shù)據(jù)擁堵情況分段載入內(nèi)存并發(fā)送。
注意:簡(jiǎn)單的將大文件分段發(fā)送不能避免內(nèi)存爆的問(wèn)題
假如10個(gè)G的文件發(fā)送給客戶端,客戶端接收速度很慢,雖然服務(wù)端將10G文件分成多個(gè)小文件發(fā)送,但是如果客戶端接收速度遠(yuǎn)遠(yuǎn)低于服務(wù)端發(fā)送速度,仍然會(huì)導(dǎo)致服務(wù)端要發(fā)送的數(shù)據(jù)堆積在發(fā)送緩沖區(qū)中,導(dǎo)致內(nèi)存爆掉。就像客戶端帶寬為10k/S,服務(wù)端以1M/S的速度發(fā)送,仍然會(huì)導(dǎo)致數(shù)據(jù)積壓在服務(wù)器發(fā)送緩沖區(qū)導(dǎo)致內(nèi)存爆掉。
正確的做法應(yīng)該是根據(jù)客戶端網(wǎng)絡(luò)數(shù)據(jù)擁堵情況控制發(fā)送速度。
如何判斷客戶端網(wǎng)絡(luò)數(shù)據(jù)放生擁堵?如何發(fā)送?
workerman提供了網(wǎng)絡(luò)擁堵控制機(jī)制,即 onBufferFull和onBufferDrain事件(具體說(shuō)明參見(jiàn)手冊(cè)),當(dāng)服務(wù)端向客戶端的發(fā)送緩沖區(qū)滿時(shí)(緩沖區(qū)大小可控制 參見(jiàn)手冊(cè))會(huì)產(chǎn)生onBufferFull事件,這時(shí)服務(wù)端應(yīng)該停止向這個(gè)客戶端再發(fā)送數(shù)據(jù)(停止從磁盤(pán)read數(shù)據(jù)到內(nèi)存),因?yàn)閛nBufferFull發(fā)生時(shí)說(shuō)明發(fā)送給客戶端的數(shù)據(jù)發(fā)生擁堵。
而當(dāng)發(fā)送緩沖區(qū)的數(shù)據(jù)全部發(fā)送給客戶端后(發(fā)送緩沖區(qū)空了),將會(huì)放生onBufferDrain事件,這時(shí)服務(wù)端可以繼續(xù)從磁盤(pán)read數(shù)據(jù),繼續(xù)向客戶端發(fā)送。
通過(guò)onBufferFull和onBufferDrain事件可以方便控制網(wǎng)絡(luò)擁堵,既能夠減少內(nèi)存消耗,又能以最快的速度將數(shù)據(jù)發(fā)送給客戶端。
示例:
從磁盤(pán)發(fā)送大文件到客戶端參見(jiàn)下面示例(使用的是http協(xié)議,其它協(xié)議也適用)
<?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);
}
// 這里發(fā)送的是一個(gè)大的pdf文件,如果是其它格式的文件,請(qǐng)修改下面代碼中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;
}
// ======發(fā)送http頭======
$file_size = filesize($file_name);
$header = "HTTP/1.1 200 OK\r\n";
// 這里寫(xiě)的Content-Type是pdf,如果不是pdf文件請(qǐng)修改Content-Type的值
// mime對(duì)應(yīng)關(guān)系參見(jiàn) 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);
// ======分段發(fā)送文件內(nèi)容=======
$connection->fileHandler = fopen($file_name, 'r');
$do_write = function()use($connection)
{
// 對(duì)應(yīng)客戶端的連接發(fā)送緩沖區(qū)未滿時(shí)
while(empty($connection->bufferFull))
{
// 從磁盤(pán)讀取文件
$buffer = fread($connection->fileHandler, 8192);
// 讀不到數(shù)據(jù)說(shuō)明文件讀到末尾了
if($buffer === '' || $buffer === false)
{
return;
}
$connection->send($buffer, true);
}
};
// 發(fā)生連接發(fā)送緩沖區(qū)滿事件時(shí)設(shè)置一個(gè)標(biāo)記bufferFull
$connection->onBufferFull = function($connection)
{
// 賦值一個(gè)bufferFull臨時(shí)變量給鏈接對(duì)象,標(biāo)記發(fā)送緩沖區(qū)滿,暫停do_write發(fā)送
$connection->bufferFull = true;
};
// 當(dāng)發(fā)送緩沖區(qū)數(shù)據(jù)發(fā)送完畢時(shí)觸發(fā)
$connection->onBufferDrain = function($connection)use($do_write)
{
$connection->bufferFull = false;
$do_write();
};
// 執(zhí)行發(fā)送
$do_write();
}
Worker::runAll();
以上例子親測(cè)ok,請(qǐng)?jiān)囉?/p>
懂了,可以多次使用 send 并且利用緩沖區(qū)是否滿,來(lái)控制輸出。
非常感謝 walker 這么細(xì)心的回復(fù)!