您可以使用
popen()
(文档)或
proc_open()
(文档)来执行一个Unix命令(例如zip或gzip),并将stdout返回为PHP流。
flush()
(文档)将尽最大努力将PHP的输出缓冲区内容推送到浏览器。
将所有这些组合在一起可以得到您想要的结果(前提是没有其他东西阻碍 - 请查看
flush()
文档页面上的警告)。
(注意:不要使用
flush()
。有关详细信息,请参见下面的更新。)
类似以下代码可以实现该功能:
<?php
header('Content-Type: application/x-gzip');
$fp = popen('tar cf - file1 file2 file3 | gzip -c', 'r');
$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中获得最大收益,你可以使用
Spark2在同一端口上运行多个实例。不要在每个处理器核心上运行多个nodejs实例。
更新(根据Benji在此答案评论部分提供的出色反馈)
1. fread()
的文档表明该函数仅会从非常规文件中读取最多8192个字节的数据。因此,8192可能是一个很好的缓冲区大小选择。
[编辑说明] 8192几乎肯定是一个平台相关的值--在大多数平台上,fread()
将读取数据,直到操作系统的内部缓冲区为空,然后它将返回,允许操作系统异步地再次填充缓冲区。8192是许多流行操作系统的默认缓冲区大小。
还有其他情况可能导致fread返回少于8192个字节--例如,“远程”客户端(或进程)填充缓冲区较慢--在大多数情况下,fread()
将返回输入缓冲区的内容,而不必等待其变满。这可能意味着返回0..os_buffer_size个字节。
道德是:将buffsize
作为传递给fread()
的值应被视为“最大”大小--永远不要假设您已经收到了所请求的字节数(或任何其他数字)。
2. 根据fread文档的评论,有一些注意事项:magic quotes可能会干扰并且必须关闭。
3. 设置mb_http_output('pass')
(文档)可能是个好主意。尽管'pass'
已经是默认设置,但如果您的代码或配置先前将其更改为其他内容,则可能需要明确指定它。
4. 如果您正在创建zip文件(而不是gzip),则需要使用内容类型标题:
Content-type: application/zip
或者...可以使用"application/octet-stream"代替。(它是用于各种不同类型的二进制下载的通用内容类型):
Content-type: application/octet-stream
如果您希望用户提示下载并将文件保存到磁盘(而不是可能让浏览器尝试将文件显示为文本),则需要使用content-disposition头。 (其中文件名指示在保存对话框中建议的名称):
Content-disposition: attachment
在使用这种技术时,应该发送Content-length头部,但由于您事先不知道zip文件的确切大小,因此这很困难。 是否有一种可以设置的头部来指示内容是"流式传输"或长度未知?有人知道吗?
最后,这是一个经过修订的示例,使用了@Benji的所有建议(并创建ZIP文件而不是TAR.GZIP文件):
<?php
header('Content-Type: application/octet-stream');
header('Content-disposition: attachment; filename="file.zip"');
$fp = popen('zip -r - file1 file2 file3', 'r');
$bufsize = 8192;
$buff = '';
while( !feof($fp) ) {
$buff = fread($fp, $bufsize);
echo $buff;
}
pclose($fp);
更新:(2012年11月23日)我发现在读取/回显循环内调用
flush()
可能会在处理非常大的文件和/或非常慢的网络时导致问题。至少在Apache后面运行PHP作为cgi/fastcgi时是这样的,似乎在其他配置中运行时也可能发生同样的问题。当PHP将输出刷新到Apache比Apache实际通过套接字发送更快时,该问题似乎会出现。对于非常大的文件(或慢速连接),这最终会导致Apache内部输出缓冲区的溢出。这会导致Apache杀死PHP进程,这当然会导致下载挂起,或者过早地完成,只完成了部分传输。
解决方案不是根本不调用flush()
。我已经更新了上面的代码示例以反映这一点,并在答案顶部的文本中放置了一个注释。