gzcompress()随机插入额外数据?

4

我已经研究了整个早上,决定最后一搏,也许Stack Overflow上有人可以给我一个“曾经历过”的答案。

背景

最近,我在我们(面向内部网络)的Apache(2.2)服务器上使用过滤器实现了压缩,以便所有基于文本的文件都通过mod_deflate进行压缩(css、js、txt、html等),并未提及php脚本。在对如何最好地压缩PHP输出进行了大量研究之后,我决定使用gzcompress()方法,因为PHP文档建议使用zlib库和gzip(使用deflate算法等等),而不是ob_gzipwhatever()。

所以我像这样复制了别人的方法:

<?php # start each page by enabling output buffering and disabling automatic flushes
ob_start();ob_implicit_flush(0);

(program logic)

print_gzipped_page();

function print_gzipped_page() {
 if (headers_sent())
    $encoding = false;
 elseif(strpos($_SERVER['HTTP_ACCEPT_ENCODING'],'x-gzip') !== false )
    $encoding = 'x-gzip';
 elseif(strpos($_SERVER['HTTP_ACCEPT_ENCODING'],'gzip') !== false )
    $encoding = 'gzip';
 else
    $encoding = false;

 if($encoding){
    $contents = ob_get_contents(); # get contents of buffer
    ob_end_clean(); # turn off OB and flush buffer
    $size = strlen($contents);
    if ($size < 512) { # too small to be worth a compression
        echo $contents;
        exit();
    } else {
        header("Content-Encoding: $encoding");
        header('Vary: Accept-Encoding');
        # 8-byte file header: g-zip file (1f 8b) compression type deflate (08), next 5 bytes are padding
        echo "\x1f\x8b\x08\x00\x00\x00\x00\x00"; 
        $contents = gzcompress($contents, 9);
        $contents = substr($contents, 0,$size); # faster than not using a substr, oddly
        echo $contents;
        exit();
    }
} else {
    ob_end_flush();
    exit();
 }
}

非常标准的事情,对吧?

问题

有10%到33%的PHP页面请求通过Firefox发送后成功返回并被压缩,只有Firefox显示压缩的ASCII而没有解压它。而且,最奇怪的部分是返回的内容大小总是比正确呈现的页面大小多30或31字节。例如,当脚本被正确显示时,Firebug显示内容大小为1044;当Firefox显示一大块二进制无意义字符时,Firebug显示的内容大小为1074。

这种情况发生在一些运行着32位老版Fedora 12的用户上,他们使用Firefox 3.3。然后它发生在FF5、FF6以及一些新的7.1用户身上!我一直想要将他们全部升级到FF7.1,所以每当他们出现问题时,我就会将其升级,但是FF7.1仍然表现出相同的行为,只是更少发生。

诊断

我一直在一些电脑上安装Firebug以观察头文件,这也是我感到困惑的地方:

正常工作的页面响应头:

  • HTTP/1.1 200 OK
  • Date: Fri, 21 Oct 2011 18:40:15 GMT
  • Server: Apache/2.2.15 (Fedora)
  • X-Powered-By: PHP/5.3.2
  • Expires: Thu, 19 Nov 1981 08:52:00 GMT
  • Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
  • Pragma: no-cache
  • Content-Encoding: gzip
  • Vary: Accept-Encoding
  • Content-Length: 1045
  • Keep-Alive: timeout=10, max=75
  • Connection: Keep-Alive
  • Content-Type: text/html; charset=UTF-8

(注意,内容长度是自动生成的)

当页面出现故障时:

  • HTTP/1.1 200 OK
  • (其他所有内容都相同)
  • Content-Length: 1075

发送的头文件总是包括Accept-Encoding: gzip、deflate。

我尝试过以下方法来修复这个问题:

  • 显式声明有无压缩的内容长度
  • 不使用$contents的substr()
  • 移除$contents末尾的校验和

我不想使用gzencode,因为我的测试结果显示它比gzcompress慢得多(9%),这可能是因为它会生成额外的校验和和其他Web浏览器不需要或使用的内容。

我无法在运行Firefox 7.1的64位Fedora 14框上复制此行为。在我将压缩代码发布之前的测试中,Chrome和Firefox都没有发生过这种情况。(编辑:在发布此帖子后不久,我留下的一个窗口每30秒发送一次meta刷新,在Firefox中刷新了约60次后终于崩溃了)我们手头的几台Windows XP电脑表现与Fedora 12相同。搜索Firefox的Bugzilla会出现一两个与这种情况有些类似的bug请求,但那是针对3.3之前的版本,并且是所有gzipped内容,而我们的Apache gzipped css和js文件每次下载和显示时都没有错误。

事实上,每次返回的内容长度增加了30/31个字节,这让我想到我的脚本/gzcompress()内部出了问题,导致Firefox无法解析响应。当然,如果你尝试改变echo'd gzip header,Firefox会抛出“Content Encoding Error”,所以我真的倾向于问题在gzcompress()内部。

我注定要失败吗?我必须放弃这个实现并使用不受欢迎的ob_start("ob_gzhandler")方法吗?

我猜我的“适用于多种情况”的问题是:PHP的zlib压缩库中是否存在已知的bug,当接收到非常特定的输入时会发生一些奇怪的事情?

编辑:糟糕。我readgzfile()了其中一个Firefox下载的损坏的、未压缩的页面,结果每个东西都完美地回显了回来。=(这意味着这一定是...不,我什么都没有。


自然而然地,我一发帖子,我的电脑上的Firefox终于从服务器检索到了损坏的数据。 ‹xÚµ™moÛ6ÇßûS<´Ù€ÙRšlhI@ 到无限远处,超越极限。 - Eric L.
2个回答

0
首先,您似乎没有设置内容长度标头,这会导致问题。相反,您正在使gzip内容变得更长,以便与您最初接收到的内容长度大小匹配。这将变得非常丑陋。我的建议是替换这些行。
# 8-byte file header: g-zip file (1f 8b) compression type deflate (08), next 5 bytes are padding
echo "\x1f\x8b\x08\x00\x00\x00\x00\x00"; 
$contents = gzcompress($contents, 9);
$contents = substr($contents, 0,$size); # faster than not using a substr, oddly
echo $contents;

使用

$compressed = gzcompress($contents, 9);
$compressed_length = strlen($compressed); /* contains no nulls i believe */
header("Content-length: $compressed_length");
echo "\x1f\x8b\x08\x00\x00\x00\x00\x00", $compressed; 

看看它是否有助于解决问题。


好的,我已经尝试过显式声明内容长度,但没有改变任何东西。在压缩内容之前回显gzip头应该没问题,因为调用了ob_end_clean()函数,它会擦除缓冲区的内容并停止输出缓冲,所以头部不会与其余内容一起编码。我已重新启用显式声明内容长度,但这之前并没有解决问题,所以我对此并不抱有太高的期望。 - Eric L.
“instead, you are making the gzip content longer so that it matches the content length size that you were receiving in the 1st place,” 这句话的意思是什么? - Eric L.
是的,这仍然无法防止错误(如果它真的是一个错误)发生。似乎需要刷新几次 Firefox 才能出现问题。 - Eric L.

0
叮!叮!叮!在整个周末思考这个问题后,我终于在第N次阅读PHP手册时找到了答案...从zlib PHP文档中,"是否透明地压缩页面。" 透明!也就是说,只要将zlib.output_compression设置为"On",PHP无需其他任何操作即可压缩其输出。是的,很尴尬。
出于未知原因,从PHP脚本显式调用的代码正在压缩已经压缩过的内容,浏览器只是解开一层压缩并显示结果。有趣的是,当output_compression打开或关闭时,内容的strlen()没有变化,因此透明压缩必须发生在显式压缩之后,但它偶尔决定不压缩已经压缩的内容?
无论如何,通过简单地让PHP自己处理,所有问题都得到了解决。zlib不需要输出缓冲区或任何东西来压缩输出。
希望这能帮助那些在HTTP压缩的美妙世界中挣扎的人们。

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