Ben*_*XVI 43 php bash lamp zip pipe
通常,Web服务需要压缩几个大文件以供客户端下载.最明显的方法是创建一个临时zip文件,然后将echo其创建给用户或将其保存到磁盘并重定向(将来某个时间删除它).
但是,这样做有缺点:
像ZipStream-PHP这样的解决方案通过将数据按文件铲入Apache文件来改进这一点.但是,结果仍然是高内存使用率(文件完全加载到内存中),以及磁盘和CPU使用率的大幅飙升.
相反,请考虑以下bash片段:
ls -1 | zip -@ - | cat > file.zip
# Note -@ is not supported on MacOS
Run Code Online (Sandbox Code Playgroud)
这里,zip以流模式运行,导致内存占用量低.管道具有整数缓冲区 - 当缓冲区已满时,OS会暂停写入程序(管道左侧的程序).这样可确保zip其输出速度与输出速度一样快cat.
然后,最佳方式是执行相同的操作:cat使用Web服务器进程替换,将zip文件流式传输给用户,并将其动态创建.与仅流式传输文件相比,这将产生很少的开销,并且将具有无问题的,非尖峰的资源配置文件.
如何在LAMP堆栈上实现这一目标?
Lee*_*Lee 49
您可以使用popen() (docs)或proc_open() (docs)执行unix命令(例如zip或gzip),并将stdout作为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);
Run Code Online (Sandbox Code Playgroud)
你问过"其他技术":我会说,"在请求的整个生命周期中支持非阻塞i/o的任何东西".您可以使用Java或C/C++(或许多其他可用语言中的任何一种)构建这样的组件作为独立服务器,如果您愿意进入非阻塞文件访问的"潦倒"和诸如此类的东西.
如果你想要一个非阻塞的实现,但你宁愿避免"向下和脏",最简单的路径(恕我直言)将是使用nodeJS.对现有版本的nodejs所需的所有功能提供了大量支持:使用http模块(当然)为http服务器; 并使用child_process模块生成tar/zip /无论管道.
最后,如果(并且仅当)您正在运行多处理器(或多核)服务器,并且您希望从nodejs获得最多,则可以使用Spark2在同一端口上运行多个实例.不要为每个处理器核心运行多个nodejs实例.
更新 (来自Benji在此答案评论部分的出色反馈)
1.文档用于fread()指示该函数一次只读取多达8192个字节的数据,而不是常规文件.因此,8192可能是缓冲区大小的良好选择.
[编辑说明] 8192几乎可以肯定是一个平台相关值 - 在大多数平台上,fread()将读取数据,直到操作系统的内部缓冲区为空,此时它将返回,允许操作系统异步地再次填充缓冲区.8192是许多流行操作系统上的默认缓冲区的大小.
还有其他情况会导致fread返回甚至少于8192个字节 - 例如,"远程"客户端(或进程)填充缓冲区的速度很慢 - 在大多数情况下,fread()将返回输入缓冲区的内容 -没有等待它满满的.这可能意味着从0..os_buffer_size字节返回任何地方.
寓意是:你传递给值fread()作为buffsize应该被认为是"最大"大小-永远不要假设你已经接收的字节你要的号码(或与此有关的任何其他数字).
2.根据对fread docs的评论,一些警告:魔法引号可能会干扰并且必须关闭.
3.设置mb_http_output('pass') (docs)可能是一个好主意.虽然'pass'已经是默认设置,但如果您的代码或配置之前已将其更改为其他内容,则可能需要明确指定它.
4.如果您要创建zip(而不是gzip),则需要使用内容类型标题:
Content-type: application/zip
Run Code Online (Sandbox Code Playgroud)
或者......'application/octet-stream'可以代替使用.(它是用于所有不同类型的二进制下载的通用内容类型):
Content-type: application/octet-stream
Run Code Online (Sandbox Code Playgroud)
如果您希望提示用户下载并将文件保存到磁盘(而不是让浏览器尝试将文件显示为文本),那么您将需要content-disposition标头.(其中filename表示应在保存对话框中建议的名称):
Content-disposition: attachment; filename="file.zip"
Run Code Online (Sandbox Code Playgroud)
一个人也应该发送Content-length标题,但是这个技术很难,因为你事先并不知道zip的确切大小. 是否有一个标题可以设置为表示内容是"流式传输"还是长度未知?有人知道吗?
最后,这是一个修改后的示例,它使用了@ Benji的所有建议(并创建了一个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);
Run Code Online (Sandbox Code Playgroud)
更新:(2012-11-23)我发现flush()在读取/回显循环内调用可能会导致处理非常大的文件和/或非常慢的网络时出现问题.至少,在Apache后面运行PHP作为cgi/fastcgi时也是如此,并且在其他配置中运行时似乎也会出现同样的问题.当PHP将输出更快地刷新到Apache而不是Apache实际通过套接字发送它时,似乎会出现问题.对于非常大的文件(或慢速连接),这最终会导致Apache的内部输出缓冲区溢出.这会导致Apache杀死PHP进程,这当然会导致下载挂起或过早完成,只进行了部分传输.
解决方案根本不是打电话flush().我已经更新了上面的代码示例以反映这一点,我在答案顶部的文本中添加了一个注释.