Часто веб-службе необходимо закрепить несколько больших файлов для загрузки клиентом. Самый очевидный способ сделать это – создать временный zip-файл, затем либо echo
его пользователю, либо сохранить его на диск и перенаправить (удалив его некоторое время в будущем).
Однако делать то, что имеет место, имеет свои недостатки:
Такие решения, как ZipStream-PHP, улучшают это путем перетаскивания данных в файл Apache по файлу. Тем не менее, результат по-прежнему является большим объемом использования памяти (файлы полностью загружаются в память), а также большие, мощные всплески на диске и использование ЦП.
Напротив, рассмотрим следующий фрагмент bash:
ls -1 | zip -@ - | cat > file.zip # Note -@ is not supported on MacOS
Здесь zip
работает в потоковом режиме, что приводит к низкому объему памяти. Труба имеет встроенный буфер – когда буфер заполнен, ОС приостанавливает программу записи (программа слева от трубы). Это гарантирует, что zip
работает только так быстро, как его вывод может быть записан cat
.
Таким образом, оптимальным способом было бы сделать то же самое: замените cat
на веб-серверный процесс, передав zip-файл пользователю, создав его на лету. Это создавало бы небольшие накладные расходы по сравнению с просто потоковой передачей файлов и имело бы беспроблемный, непрозрачный профиль ресурсов.
Как вы можете добиться этого в стеке LAMP?
Вы можете использовать команды popen()
(docs) или proc_open()
(docs) для выполнения команды unix (например, zip или gzip) и вернуть stdout как поток php. flush()
(docs) сделает все возможное, чтобы вывести содержимое выходного буфера php в браузер.
Сочетание всего этого даст вам то, что вы хотите (при условии, что ничего не мешает) см. Esp. Оговорки на странице docs для 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 / C ++ (или любой другой доступный язык), если бы вы захотели войти в «нисходящее и грязное» неблокирующее доступ к файлам и многое другое.
Если вы хотите неблокирующую реализацию, но лучше избегаете «вниз и грязно», самым простым путем (IMHO) будет использование nodeJS . Существует много поддержки всех функций, которые вам нужны в существующей версии nodejs: используйте http
модуль (конечно) для http-сервера; и используйте модуль child_process
для создания конвейера tar / zip / whatever.
Наконец, если (и только если) вы используете многопроцессорный (или многоядерный) сервер, и вы хотите больше всего от nodejs, вы можете использовать Spark2 для запуска нескольких экземпляров на одном и том же порту. Не запускайте более одного экземпляра nodejs для каждого процессора.
Обновление (от превосходной обратной связи Бенджи в разделе комментариев по этому вопросу)
1. Документы для fread()
указывают на то, что функция будет считывать только до 8192 байтов данных за время от всего, что не является обычным файлом. Поэтому 8192 может быть хорошим выбором размера буфера.
[редакционная заметка] 8192 почти наверняка зависит от платформы – на большинстве платформ fread()
будет считывать данные до тех пор, пока внутренний буфер операционной системы не будет пуст, после чего он вернется, что позволит os снова заполнить буфер асинхронно. 8192 – размер буфера по умолчанию во многих популярных операционных системах.
Существуют и другие обстоятельства, которые могут привести к тому, что fread вернет даже меньше 8192 байт – например, «удаленный» клиент (или процесс) медленно заполняет буфер – в большинстве случаев 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, но это сложно с этим методом, поскольку вы не знаете точный размер 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);
Обновление : (2012-11-23) Я обнаружил, что вызов flush()
в цикле чтения / эха может вызвать проблемы при работе с очень большими файлами и / или очень медленными сетями. По крайней мере, это верно при запуске PHP как cgi / fastcgi за Apache, и кажется вероятным, что такая же проблема возникнет и при работе в других конфигурациях. Проблема возникает, когда PHP сбрасывает вывод в Apache быстрее, чем Apache может фактически отправить его через сокет. Для очень больших файлов (или медленных подключений) это в конечном итоге приводит к переполнению внутреннего выходного буфера Apache. Это заставляет Apache убивать процесс PHP, что, конечно, заставляет загрузку зависать или заканчиваться преждевременно, только с частичной передачей.
Решение состоит не в том, чтобы вызвать flush()
вообще. Я обновил приведенные выше примеры кода, чтобы отразить это, и разместил примечание в тексте в верхней части ответа.
Другим решением является мой mod_zip-модуль для Nginx, написанный специально для этой цели:
https://github.com/evanmiller/mod_zip
Он чрезвычайно легкий и не вызывает отдельный процесс «zip» или обменивается данными по трубам. Вы просто указываете на сценарий, в котором перечислены местоположения файлов, которые будут включены, а mod_zip делает все остальное.
Пытаясь реализовать динамически сгенерированную загрузку с большим количеством файлов с разными размерами, я столкнулся с этим решением, но я столкнулся с различными ошибками памяти, такими как «Разрешенная память размером 134217728 байт, израсходованная на …».
После добавления ob_flush();
прямо перед flush();
ошибки памяти исчезают.
Вместе с отправкой заголовков мое окончательное решение выглядит следующим образом (просто сохраняя файлы внутри zip без структуры каталогов):
<?php // Sending headers header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="download.zip"'); header('Content-Transfer-Encoding: binary'); ob_clean(); flush(); // On the fly zip creation $fp = popen('zip -0 -j -q -r - file1 file2 file3', 'r'); while (!feof($fp)) { echo fread($fp, 8192); ob_flush(); flush(); } pclose($fp);
Я написал этот s3-файл для защиты от молнии на молнии в прошлые выходные – может быть полезно: http://engineroom.teamwork.com/how-to-securely-provide-a-zip-download-of-a-s3-file-bundle/
Согласно руководству PHP , расширение ZIP предоставляет почтовый индекс: wrapper.
Я никогда не использовал его, и я не знаю его внутренних компонентов, но логически он должен быть способен делать то, что вы ищете, предполагая, что ZIP-архивы могут быть потоковыми, что я не совсем уверен.
Что касается вашего вопроса о «стек LAMP», это не должно быть проблемой, если PHP не настроен на буферизацию вывода .
Редактировать: Я пытаюсь объединить доказательство концепции, но это кажется не тривиальным. Если вы не знакомы с потоками PHP, это может оказаться слишком сложным, если это даже возможно.
Редактировать (2): перечитав свой вопрос, взглянув на ZipStream, я нашел, что будет вашей главной проблемой здесь, когда вы скажете (выделено курсивом)
оперативный Zipping должен работать в потоковом режиме, то есть обрабатывать файлы и предоставлять данные со скоростью загрузки .
Эта часть будет чрезвычайно сложной для реализации, потому что я не думаю, что PHP предоставляет способ определить, насколько полный буфер Apache. Итак, ответ на ваш вопрос – нет, вы, вероятно, не сможете это сделать в PHP.
Кажется, вы можете устранить любые проблемы, связанные с выходным буфером, используя fpassthru () . Я также использую -0
для экономии времени процессора, так как мои данные уже компактны. Я использую этот код для обслуживания целой папки, застегнутой на молнию:
chdir($folder); $fp = popen('zip -0 -r - .', 'r'); header('Content-Type: application/octet-stream'); header('Content-disposition: attachment; filename="'.basename($folder).'.zip"'); fpassthru($fp);