Ответ 1
Вы можете использовать popen()
(docs) или proc_open()
(docs), чтобы выполнить команду unix (например, zip или gzip) и вернуть stdout как поток php. flush()
(docs) сделает все возможное, чтобы вставить содержимое буфера вывода php в браузер.
Объединение всего этого даст вам то, что вы хотите (при условии, что ничего не мешает) см. esp. оговорки на странице документов для 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);
Вы спросили о "других технологиях": на что я скажу, "все, что поддерживает неблокирующие операции ввода-вывода для всего жизненного цикла запроса". Вы могли бы создать такой компонент как автономный сервер в Java или C/С++ (или любой другой доступный язык), если бы вы захотели войти в "нисходящее и грязное" неблокирующее доступ к файлам и многое другое.
Если вам нужна неблокирующая реализация, но вы бы предпочли избежать "вниз и грязно", самым простым путем (IMHO) было бы использовать nodeJS. Существует много поддержки всех функций, которые вам нужны в существующей версии nodejs: используйте модуль http
(конечно) для http-сервера; и используйте модуль child_process
для создания конвейера tar/zip/any.
Наконец, если (и только если) вы используете многопроцессорный (или многоядерный) сервер, и вы хотите больше всего от nodejs, вы можете использовать Spark2 для запуска нескольких экземпляров на одном и том же порту. Не запускайте более одного экземпляра nodejs для каждого процессорного ядра.
Обновить (от Benji отличная обратная связь в разделе комментариев по этому вопросу)
1. Документы для fread()
указывают, что функция будет считывать только до 8192 байтов данных за время от всего, что не является обычным файлом. Поэтому 8192 может быть хорошим выбором размера буфера.
[редакционная заметка] 8192 почти наверняка зависит от платформы - на большинстве платформ fread()
будет считывать данные до тех пор, пока внутренний буфер операционной системы не станет пустым, после чего он вернется, что позволит os заполнить буфер снова асинхронно. 8192 - размер буфера по умолчанию во многих популярных операционных системах.
Существуют и другие обстоятельства, которые могут привести к тому, что fread вернет еще меньше 8192 байт - например, клиент (или процесс) "remote" работает медленно, чтобы заполнить буфер - в большинстве случаев fread()
вернет содержимое входного буфера как есть, не дожидаясь его полного заполнения. Это может означать, что от 0..s_buffer_size байты возвращаются.
Мораль: значение, которое вы передаете на fread()
, как buffsize
, должно считаться "максимальным" размером - никогда не предполагайте, что вы получили количество байтов, которые вы просили (или любой другой номер для этого дело).
2.. Согласно комментариям к документам fread, несколько предостережений: магические кавычки могут мешать и должны быть отключен.
3. Настройка mb_http_output('pass')
(docs) может быть хорошей идеей. Хотя 'pass'
уже является настройкой по умолчанию, вам может потребоваться указать его явно, если ваш код или конфиг ранее изменил его на что-то еще.
4. Если вы создаете zip (в отличие от gzip), вам нужно использовать заголовок типа контента:
Content-type: application/zip
Вместо этого можно использовать или... 'application/octet-stream'. (это общий тип контента, используемый для двоичных загрузок всех разных типов):
Content-type: application/octet-stream
и если вы хотите, чтобы пользователю было предложено загрузить и сохранить файл на диск (вместо того, чтобы потенциально использовать браузер для отображения файла в виде текста), вам понадобится заголовок содержимого. (где filename указывает имя, которое должно быть предложено в диалоговом окне сохранения):
Content-disposition: attachment; filename="file.zip"
Нужно также отправить заголовок Content-length, но это сложно с этой техникой, поскольку вы не знаете размер почтового индекса заранее. Есть ли заголовок, который может быть установлен для указания того, что контент "потоковый" или имеет неизвестную длину? Кто-нибудь знает?
Наконец, здесь приведен пересмотренный пример, в котором используются все предложения @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);
Обновление: (2012-11-23) Я обнаружил, что вызов flush()
внутри цикла read/echo может вызвать проблемы при работе с очень большими файлами и/или очень медленными сетями. По крайней мере, это верно при запуске PHP как cgi/fastcgi за Apache, и кажется вероятным, что такая же проблема возникнет и при работе в других конфигурациях. Проблема возникает, когда PHP сбрасывает вывод в Apache быстрее, чем Apache может фактически отправить его через сокет. Для очень больших файлов (или медленных подключений) это в конечном итоге приводит к переполнению внутреннего выходного буфера Apache. Это заставляет Apache убивать процесс PHP, что, конечно, заставляет загрузку зависать или заканчиваться преждевременно, только с частичной передачей.
Решение не вызывать flush()
вообще. Я обновил приведенные выше примеры кода, чтобы отразить это, и разместил примечание в тексте в верхней части ответа.