通常,Web 服务需要压缩多个大文件以供客户端下载。最明显的方法是创建一个临时 zip 文件,然后是 echo
将其发送给用户或将其保存到磁盘并重定向(在将来的某个时间删除它)。
但是,这样做有缺点:
解决方案如 ZipStream-PHP通过逐个文件将数据铲入 Apache 来改进这一点。然而,结果仍然是高内存使用率(文件完全加载到内存中),以及磁盘和 CPU 使用率的大而剧烈的峰值。
相比之下,请考虑以下 bash 代码段:
ls -1 | zip -@ - | cat > file.zip
# Note -@ is not supported on MacOS
在这里,
zip
在流模式下运行,从而减少内存占用。管道有一个完整的缓冲区——当缓冲区已满时,操作系统会暂停写入程序(管道左侧的程序)。这在这里确保 zip
仅在其输出可以由 cat
写入时才起作用.那么,最佳方法是执行相同的操作:替换
cat
使用 Web 服务器进程,将 zip 文件流式传输给用户,并即时创建。与仅流式传输文件相比,这将产生很少的开销,并且具有无问题、无尖峰的资源配置文件。您如何在 LAMP 堆栈上实现这一目标?
最佳答案
您可以使用 popen()
(docs)或 proc_open()
(docs)执行 unix 命令(例如 zip 或 gzip),并将标准输出作为 php 流返回。 flush()
(docs)将尽最大努力将 php 输出缓冲区的内容推送到浏览器。
结合所有这些将给你你想要的东西(前提是没有其他东西妨碍 - 尤其参见文档页面上关于 flush()
的警告)。
( 注意 :不要使用 flush()
。有关详细信息,请参阅下面的更新。)
像下面这样的东西可以解决问题:
<?php
// make sure to send all headers first
// Content-Type is the most important one (probably)
//
header('Content-Type: application/x-gzip');
// use popen to execute a unix command pipeline
// and grab the stdout as a php stream
// (you can use proc_open instead if you need to
// control the input of the pipeline too)
//
$fp = popen('tar cf - file1 file2 file3 | gzip -c', 'r');
// pick a bufsize that makes you happy (64k may be a bit too big).
$bufsize = 65535;
$buff = '';
while( !feof($fp) ) {
$buff = fread($fp, $bufsize);
echo $buff;
}
pclose($fp);
您询问了“其他技术”:对此我会说,“在请求的整个生命周期中支持非阻塞 i/o 的任何东西”。您可以使用 Java 或 C/C++(或许多其他可用语言中的任何一种)构建这样一个组件作为独立服务器,如果您愿意进入非阻塞文件访问等的“低级和肮脏”。
如果你想要一个非阻塞的实现,但你宁愿避免“低落和肮脏”,最简单的路径(恕我直言)是使用 nodeJS .在现有的 nodejs 版本中,您需要的所有功能都有大量支持:使用
http
模块(当然)用于 http 服务器;并使用 child_process
生成 tar/zip/whatever 管道的模块。最后,如果(且仅当)您正在运行多处理器(或多核)服务器,并且您希望从 nodejs 中获得最大 yield ,您可以使用 Spark2在同一个端口上运行多个实例。不要为每个处理器核心运行一个以上的 nodejs 实例。
更新 (来自 Benji 在此答案的评论部分中的出色反馈)
1.
fread()
的文档表示该函数一次最多只能从非常规文件中读取 8192 字节的数据。因此,8192 可能是缓冲区大小的不错选择。[编者注] 8192 几乎肯定是一个平台相关的值——在大多数平台上,
fread()
将读取数据直到操作系统的内部缓冲区为空,此时它将返回,允许操作系统再次异步填充缓冲区。 8192 是许多流行操作系统上默认缓冲区的大小。还有其他情况可能导致 fread 返回甚至少于 8192 字节——例如,“远程”客户端(或进程)填充缓冲区的速度很慢——在大多数情况下,
fread()
将按原样返回输入缓冲区的内容,而无需等待它变满。这可能意味着返回 0..os_buffer_size 字节的任何地方。寓意是:你传递给
fread()
的值如 buffsize
应该被视为“最大”大小——永远不要假设您已经收到了您要求的字节数(或任何其他数字)。2. 根据对 fread 文档的评论,一些警告:magic quotes可能会干扰,必须是 turned off .
3. 设置
mb_http_output('pass')
(docs)可能是个好主意。虽然 'pass'
已经是默认设置,如果您的代码或配置之前已将其更改为其他内容,您可能需要明确指定它。4. 如果您要创建 zip(而不是 gzip),则需要使用内容类型 header :
Content-type: application/zip
或者...可以使用“application/octet-stream”代替。 (它是用于所有不同类型的二进制下载的通用内容类型):
Content-type: application/octet-stream
如果您希望提示用户下载文件并将其保存到磁盘(而不是可能让浏览器尝试将文件显示为文本),那么您将需要 content-disposition header 。 (其中文件名表示应在保存对话框中建议的名称):
Content-disposition: attachment; filename="file.zip"
还应该发送 Content-length header ,但是使用这种技术很难做到这一点,因为您事先不知道 zip 的确切大小。 是否可以设置一个 header 来指示内容是“流式传输”或长度未知?有人知道吗?
最后,这是一个使用所有@ Benji's 的修改示例。建议(并创建一个 ZIP 文件而不是 TAR.GZIP 文件):
<?php
// make sure to send all headers first
// Content-Type is the most important one (probably)
//
header('Content-Type: application/octet-stream');
header('Content-disposition: attachment; filename="file.zip"');
// use popen to execute a unix command pipeline
// and grab the stdout as a php stream
// (you can use proc_open instead if you need to
// control the input of the pipeline too)
//
$fp = popen('zip -r - file1 file2 file3', 'r');
// pick a bufsize that makes you happy (8192 has been suggested).
$bufsize = 8192;
$buff = '';
while( !feof($fp) ) {
$buff = fread($fp, $bufsize);
echo $buff;
}
pclose($fp);
更新 : (2012-11-23) 我发现打电话
flush()
在处理非常大的文件和/或非常慢的网络时,在读取/echo 循环中可能会导致问题。至少,在 Apache 后面将 PHP 作为 cgi/fastcgi 运行时确实如此,并且在其他配置中运行时似乎也会出现同样的问题。当 PHP 将输出刷新到 Apache 的速度比 Apache 通过套接字实际发送它的速度快时,就会出现这个问题。对于非常大的文件(或慢速连接),这最终会导致 Apache 的内部输出缓冲区溢出。这会导致 Apache 终止 PHP 进程,这当然会导致下载挂起或过早完成,只进行了部分传输。解决办法是不打电话
flush()
根本。我已经更新了上面的代码示例以反射(reflect)这一点,并在答案顶部的文本中放置了一个注释。
关于php - LAMP:如何为用户动态创建 .Zip 大文件,而不会出现磁盘/CPU 抖动,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/4357073/