如何让PHP生成分块响应

33

我在Google上搜索了这个问题,但没有找到答案。

我想让我的PHP脚本以分块(http://en.wikipedia.org/wiki/Chunked_transfer_encoding)的方式生成HTTP响应。如何操作?

更新: 我解决了这个问题。我必须指定 Transfer-encoding 头并刷新它。

header("Transfer-encoding: chunked");
flush(); 

必须进行刷新操作,否则将生成Content-Length标头。

我必须自己制作分块。使用帮助函数,这并不难。

function dump_chunk($chunk)
{
    echo sprintf("%x\r\n", strlen($chunk));
    echo $chunk;
    echo "\r\n";
}

你可以在每个块之后进行清空。 - Gumbo
你能解释一下如何从文件中获取块吗?拜托了?^_º - Jose Manuel Abarca Rodríguez
请注意:显式设置 header("Transfer-encoding: chunked"); 会破坏 PHP 的本机分块编码,它将不再计算和发送块长度! - cachius
9个回答

9
如果您没有指定内容长度头,并且发生刷新,则PHP响应将始终被分块。(在x个字节之后,自动发生刷新,但不知道确切的数量。)
这似乎是一件奇怪的事情。您是在进行某种学术/学习练习,还是在尝试解决实际问题?

我想强制使用分块传输编码,因为我正在测试一个Ruby脚本,该脚本应该能够读取它。所以如果我理解你的意思正确,我只需要定期插入一些flush();行,这将强制php以分块传输编码响应。不需要显式输出任何块大小数字? - Harry Wood
2
Apache应该自己处理这个问题。基本上,如果Apache事先不知道长度,它只能使用“分块”。 - Evert
1
Apache会在发送之前缓冲一定量的输出。如果您的整个输出都适合缓冲区,则不会进行分块,除非您强制执行。 - sosiouxme

6

这个问题有点模糊...如果你不介意大块的数据(大约0x1000八进制字节),那么PHP可以处理。

<?php

while (true) {
    # output data
    flush()
    usleep(pow(2,18));
}
?>

PHP将生成编号的章节等内容。

如果您想发送微小的块,就像使用AJAX客户端一样...嗯,我结合了OP的问题和在PHP.NET上的一些研究,看起来他确实做得很好。

$ echo -en "GET /chunked/ HTTP/1.1\r\nHost: ec\r\n\r\n" | nc localhost 80

HTTP/1.1 200 OK
Date: Wed, 23 May 2012 13:03:01 GMT
Server: Apache/2.2.9 (Debian) PHP/5.3.5-1 with Suhosin-Patch mod_ssl/2.2.9 OpenSSL/0.9.8o
X-Powered-By: PHP/5.3.5-1
Transfer-encoding: chunked
Content-Type: text/html

14
Teachers have class.
50
We secure our friends not by accepting favors but by doing them.
            -- Thucydides
48
Vulcans never bluff.
            -- Spock, "The Doomsday Machine", stardate 4202.1
31
All kings is mostly rapscallions.
            -- Mark Twain
41
Reappraisal, n.:
    An abrupt change of mind after being found out.
49
He who knows, does not speak.  He who speaks, does not know.
            -- Lao Tsu

现在还不确定是否会最终挤出它自己(不正确的)块数,但我没有看到任何迹象。

<?php
header("Transfer-encoding: chunked");
@apache_setenv('no-gzip', 1);
@ini_set('zlib.output_compression', 0);
@ini_set('implicit_flush', 1);
for ($i = 0; $i < ob_get_level(); $i++)  ob_end_flush();
ob_implicit_flush(1); flush();

function dump_chunk($chunk)
{
  printf("%x\r\n%s\r\n", strlen($chunk), $chunk);
  flush();
}

for (;;) {
  $output = array();
  exec("/usr/games/fortune", $output);
  dump_chunk(implode("\n", $output));
  usleep(pow(2,18));
}
?>

如果你的意图是要清除所有输出缓冲区,那么这种方法只会删除其中一半(四舍五入)。请改用 while(ob_get_level()){ob_end_flush();}。如果你想避免重复调用函数:for($i=ob_get_level(); $i; --$i){ob_end_flush();}$i=ob_get_level()+1;while(--$i){ob_end_flush();}(如果加1看起来奇怪,请使用后置递减)。感谢 netcat 的示例,HTTP 比我预期的简单得多,这很有趣,因为我看到过 <method> <path> <HTTP version>、头部和正文,但通常在查看时它们会分别显示。 - Chinoto Vokro

6

截至2013年2月16日的工作样本

<?php

function dump_chunk($chunk) {
    //echo sprintf("%x\r\n", strlen($chunk));
    echo sprintf("\r\n");
    echo $chunk;
    echo "\r\n";
}

ob_start();
?>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Title</title>
    </head>
    <body>
        <?php
        ob_end_flush();
        flush();
        ob_flush();
        for ($i = 0; $i < 5; $i++) {
            sleep(1);
            dump_chunk('Sending data chunk ' . ($i + 1) . ' of 1000 <br />');
            flush();
            ob_flush();
        }
        sleep(1); // needed for last animation
        ?>
    </body>
</html>

出现了一些问题,页面只有在整个页面渲染完成后才能显示,而不是逐步增加显示。你知道可能是什么问题吗? - Pacerier
你使用的是什么配置?(Apache版本,模块,PHP版本)? - Andriy F.
PHP 5.3.26,Apache 2.2.24 + Nginx http://www.hosting24.com/blog/index.php?read=Better-performance%21-New-server-setup%3A-Apache-%2B-Nginx%21&id=12 是由hosting24的silver套餐提供的。http://www.hosting24.com/features.php - Pacerier
在 http://www.apachefriends.org/en/xampp.html 上尝试代码。如果它在 XAMPP 上正常运行,则问题可能是由于 nginx 导致的(请参阅 https://dev59.com/tnA75IYBdhLWcg3wkZ-A 或 http://nginx.org/en/docs/faq/chunked_encoding_from_backend.html 获取线索)。 - Andriy F.
为什么要使用输出缓冲?除非您在输出开始后更改标题,将函数应用于输出或通过ob_get_clean()将缓冲保存到变量中,否则似乎不需要它。 - Chinoto Vokro
显示剩余2条评论

4
输出缓冲区在达到默认大小 4096 字节之前不会发送到浏览器。因此,您需要更改缓冲区大小或填充块。此外,浏览器可能在页面显示之前有自己的最小缓冲区。请注意,根据维基百科文章关于分块编码的说明,发送 0\r\n\r\n 块会终止响应。
如果要更改输出缓冲区大小设置,则无法使用 ini_set('output_buffering', $value)。相反,请通过将以下内容添加到您的 php.ini 文件来更改输出缓冲设置。
php_value output_buffering 1024

这是填充块的示例。
header("Transfer-Encoding: chunked");
header("Content-Encoding: none");

// Send chunk to browser
function send_chunk($chunk)
{
    // The chunk must fill the output buffer or php won't send it
    $chunk = str_pad($chunk, 4096);

    printf("%x\r\n%s\r\n", strlen($chunk), $chunk);
    flush();
}

// Send your content in chunks
for($i=0; $i<10; $i++)
{
    send_chunk("This is Chunk #$i.<br>\r\n");
    usleep(500000);
}

// note that if you send an empty chunk
// the browser won't display additional output
echo "0\r\n\r\n";
flush();

这是一个简短的示例,演示了0\r\n\r\n\终止输出:
$output = "hello world";
header("Transfer-Encoding: chunked");
header('Content-Encoding: none');
printf("%x\r\n%s\r\n", strlen($output), $output);
ob_flush();
print("0\r\n\r\n");
ob_flush();
flush();
sleep(10);
print('POST PROCESSING');

2

您应该能够使用以下内容:

<?php header("Transfer-Encoding: chunked");

但您需要确保输出符合规格要求。


似乎不够。我必须在最后一个header()语句后立即执行flush(),以确保不生成“Content-Length”头。 - Morgan Cheng
有时候你必须在服务器上禁用gz压缩并发送 header("Content-Encoding: identity"); - bato3

1
一个更简单的方法(不需要创建自己的数据块)是仅使用flush()函数:
<?php 
$fp = fopen('data.bin', "rb");
flush();
fpassthru($fp);
?>

这对我有用,如下所示的HTTP请求和响应。

请求:

00000000  47 45 54 20 2f 67 65 74  2d 63 68 75 6e 6b 2e 70 GET /get -chunk.p
00000010  68 70 20 48 54 54 50 2f  31 2e 31 0d 0a 55 73 65 hp HTTP/ 1.1..Use
00000020  72 2d 41 67 65 6e 74 3a  20 63 75 72 6c 2f 37 2e r-Agent:  curl/7.
00000030  32 31 2e 37 20 28 69 36  38 36 2d 70 63 2d 6c 69 21.7 (i6 86-pc-li
00000040  6e 75 78 2d 67 6e 75 29  20 6c 69 62 63 75 72 6c nux-gnu)  libcurl
00000050  2f 37 2e 32 31 2e 37 20  4f 70 65 6e 53 53 4c 2f /7.21.7  OpenSSL/
00000060  31 2e 30 2e 30 64 20 7a  6c 69 62 2f 31 2e 32 2e 1.0.0d z lib/1.2.
00000070  35 20 6c 69 62 73 73 68  32 2f 31 2e 32 2e 37 0d 5 libssh 2/1.2.7.
00000080  0a 48 6f 73 74 3a 20 67  61 69 61 0d 0a 41 63 63 .Host: g aia..Acc
00000090  65 70 74 3a 20 2a 2f 2a  0d 0a 0d 0a             ept: */* ....

响应:

00000000  48 54 54 50 2f 31 2e 31  20 32 30 30 20 4f 4b 0d HTTP/1.1  200 OK.
00000010  0a 44 61 74 65 3a 20 54  68 75 2c 20 32 32 20 53 .Date: T hu, 22 S
00000020  65 70 20 32 30 31 31 20  32 33 3a 35 33 3a 30 32 ep 2011  23:53:02
00000030  20 47 4d 54 0d 0a 53 65  72 76 65 72 3a 20 41 70  GMT..Se rver: Ap
00000040  61 63 68 65 2f 32 2e 32  2e 38 20 28 46 65 64 6f ache/2.2 .8 (Fedo
00000050  72 61 29 0d 0a 58 2d 50  6f 77 65 72 65 64 2d 42 ra)..X-P owered-B
00000060  79 3a 20 50 48 50 2f 35  2e 32 2e 36 0d 0a 54 72 y: PHP/5 .2.6..Tr
00000070  61 6e 73 66 65 72 2d 45  6e 63 6f 64 69 6e 67 3a ansfer-E ncoding:
00000080  20 63 68 75 6e 6b 65 64  0d 0a 43 6f 6e 74 65 6e  chunked ..Conten
00000090  74 2d 54 79 70 65 3a 20  74 65 78 74 2f 68 74 6d t-Type:  text/htm
000000A0  6c 3b 20 63 68 61 72 73  65 74 3d 55 54 46 2d 38 l; chars et=UTF-8
000000B0  0d 0a 0d 0a                                      ....
000000B4  61 0d 0a 54 45 53 54 20  44 41 54 41 0a 0d 0a 30 a..TEST  DATA...0
000000C4  0d 0a 0d 0a                                      ....

1

Andriy F.'s solution对我有用。以下是他答案的简化版本:

function dump_chunk($chunk)
{
    echo $chunk;
    flush();
    ob_flush();
}

header('Content-Type: text/html; charset=UTF-8');

flush();

for ($i = 0; $i < 3; $i++) {
    dump_chunk('Sending data chunk ' . ($i + 1) . ' of 1000 <br />');
    sleep(1);
}

尽管我不明白为什么需要调用ob_flush()。如果有人知道,请评论。

0
如果大小足够大,Apache会为您处理。 如果您使用ob_gzhandler,我认为这没有意义。最好让缓冲区尽快输出。如果PHP确实没有自动发送内容长度标头或内容长度来自未压缩的内容:
ob_start();
ob_start("ob_gzhandler");
...
ob_end_flush();  
header('Content-Length: '.ob_get_length()); 
ob_end_flush();

您可以使用apache的mod_buffer(或者PHP的ini_set)来减小缓冲区大小。

附注:如果内容长度未知且大小足够大,则内容将被分块发送作为权宜之计。


-1

我需要做两件额外的事情才能使它工作。如果对象尚未启动,则调用ob_start。并且还要echo出一个长的空字符串。浏览器似乎需要在其第一个块中发送一定量的内容。

header('Content-Encoding', 'chunked');
header('Transfer-Encoding', 'chunked');
header('Content-Type', 'text/html');
header('Connection', 'keep-alive');

if (ob_get_level() == 0) ob_start();
echo str_pad('',4096)."\n";
ob_flush();
flush();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接