45 votes

Compression et diffusion à la volée de fichiers volumineux, en PHP ou autrement

Imaginez un service web scénario où plusieurs gros fichiers doivent être compressés et fourni au client pour le téléchargement. Le moyen le plus évident pour ce faire sur de la LAMPE est de créer un temporaire de fichier zip en utilisant PHP natif de capacité, alors echo il pour l'utilisateur ou l'enregistrer sur le disque et de rediriger (l'obligeant à être supprimé dans le futur).

Cependant, ce schéma présente les inconvénients suivants:

  1. une période intensive du PROCESSEUR et du disque raclée
  2. inacceptable l'utilisation de la mémoire par demande
  3. substantielle de l'espace disque temporaire
  4. un considérable retard initial de l'utilisateur pendant que l'archive est préparé.

En outre, si l'utilisateur annule le téléchargement de la moitié du chemin, une grande quantité de ressources ont été gaspillées.

Paul Duncan ZipStream-PHP résout certains de ces problèmes, effectivement en train de pelleter les données dans Apache fichier par fichier. Cependant, il souffre encore de très forte utilisation de la mémoire (les fichiers sont entièrement chargé en mémoire), et les résultats dans le disque et les pics d'utilisation du PROCESSEUR.

En revanche, considérer les points suivants bash extrait:

ls -1 | zip -@ - | dd of=/dev/somewhere

Une pipe a une partie intégrante de la mémoire tampon, et quand c'est plein, le système d'exploitation suspend le programme d'envoi. Donc, ici, Info-ZIP (zip util fournis sur OS X, facilement apt-a &c. sur linux) fonctionne en mode continu, et, par conséquent, a une faible empreinte mémoire, et ne fonctionne vite que son signal de sortie peut être lu par dd.

La meilleure façon, alors, serait de faire la même chose: flux les fichiers de l'utilisateur par l'intermédiaire d'un zip utilitaire. Cela fonctionnerait avec très peu de frais généraux, et seraient beaucoup plus semblable à la façon dont gzip est appliquée par les serveurs web à la volée.

Est-il possible de réaliser cela à l'aide d'Apache et PHP?

(À part: il y a de mieux web technologies de desserte qui pourrait être mieux adapté à cette tâche?)

49voto

Lee Points 9537

Vous pouvez utiliser popen() (docs) ou proc_open() (docs) pour exécuter une commande unix (par exemple. zip ou gzip), et de revenir stdout comme php stream. flush() (docs) fera de son mieux pour pousser le contenu de php est sortie de la mémoire tampon du navigateur.

Combinant tout cela va vous donner ce que vous voulez (à condition que rien d'autre n'est dans la manière -- voir les esp. les mises en garde sur les docs page pour flush()).

(Remarque: ne pas utiliser flush(). Voir la mise à jour ci-dessous pour plus de détails.)

Quelque chose comme ce qui suit peut faire l'affaire:

<?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);

Vous m'avez demandé "d'autres technologies": à qui je vais le dire, "tout ce qui appuie le non-blocage i/o pour l'ensemble du cycle de vie de la demande". Vous pourriez construire un tel composant en tant que serveur autonome en Java ou en C/C++ (ou une des nombreuses autres langues disponibles), si vous étiez prêt à entrer dans le "down and dirty" de non-blocage de l'accès au fichier et autres joyeusetés.

Si vous voulez un non-blocage de la mise en œuvre, mais vous préférez éviter le "down and dirty", le plus simple (à mon humble avis) serait d'utiliser nodeJS. Il y a beaucoup de soutien pour toutes les fonctionnalités dont vous avez besoin dans la version existante de nodejs: utilisation de l' http module (bien sûr) pour le serveur http et à l'utilisation ( child_process module pour frayer le tar/zip/autre pipeline.

Enfin, si (et seulement si) vous êtes en cours d'exécution d'un multi-processeur (ou multi-core) du serveur, et vous voulez le plus de nodejs, vous pouvez utiliser Spark2 d'exécuter plusieurs instances sur le même port. Ne pas exécuter plus d'un nodejs instance par processeur-core.


Mise à jour (à partir de Benji excellents commentaires dans la section des commentaires sur cette réponse)

1. Les docs pour fread() indiquent que la fonction est en lecture seule jusqu'à 8192 octets de données à un moment de tout ce qui n'est pas un fichier régulier. Par conséquent, 8192 peut être un bon choix de la taille de la mémoire tampon.

[note de la rédaction] 8192 est presque certainement une plate-forme dépendante de la valeur -- sur la plupart des plates-formes, fread() va lire des données jusqu'à ce que le système d'exploitation interne de la mémoire tampon est vide, à quel point il sera de retour, permettant à l'os de remplissage de la mémoire tampon de nouveau de manière asynchrone. 8192 est la taille de la mémoire tampon par défaut sur de nombreux systèmes d'exploitation courants.

Il y a d'autres circonstances qui peuvent causer fread pour revenir encore moins de 8192 octets -- par exemple, la "distance" du client (ou de processus) est lent à remplir la mémoire tampon dans la plupart des cas, fread() renverra le contenu de la mémoire tampon d'entrée comme telle, sans attendre pour elle d'obtenir la pleine. Cela pourrait signifier n'importe où à partir de 0..os_buffer_size octets sont retournées.

La morale est: la valeur que vous avez passer à l' fread() comme buffsize doit être considéré comme un "maximum" de taille, ne supposez jamais que vous avez reçu le nombre d'octets que vous avez demandé (ou tout autre nombre d'ailleurs).

2. Selon les commentaires sur fread docs, quelques mises en garde: magic quotes peuvent interférer et doit être éteint.

3. Paramètre mb_http_output('pass') (docs) peut être une bonne idée. Si 'pass' est déjà la valeur par défaut, vous devrez peut-être spécifier explicitement si votre code ou de configuration a été modifié de quelque chose d'autre.

4. Si vous êtes à la création d'un zip (par opposition à gzip), vous souhaitez utiliser le type de contenu d'en-tête:

Content-type: application/zip

ou... "application/octet-stream" peut être utilisé à la place. (c'est un générique de type de contenu utilisé pour les téléchargements de toutes sortes):

Content-type: application/octet-stream

et si vous voulez que l'utilisateur soit invité à télécharger et enregistrer le fichier sur le disque (plutôt que risque d'avoir le navigateur essayez d'afficher le fichier en tant que texte), alors vous aurez besoin de l'-tête content-disposition. (d'où le nom de fichier indique le nom qui devrait être proposé dans la boîte de dialogue enregistrer):

Content-disposition: attachment; filename="file.zip"

On doit aussi envoyer le Contenu de l'en-tête de longueur, mais c'est dur avec cette technique que vous ne connaissez pas le zip de la taille exacte à l'avance. Est-il un en-tête qui peut être réglé pour indiquer que le contenu est "streaming" ou est de longueur inconnue? Quelqu'un sait?


Enfin, voici un exemple révisé qui utilise tous @Benji suggestions (et qui crée un fichier ZIP à la place d'un TAR.Fichier 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);

Mise à jour: (2012-11-23) j'ai découvert que l'appelant flush() dans la lecture/l'écho de la boucle peut causer des problèmes lorsque vous travaillez avec des fichiers très volumineux et/ou très lente. Au moins, cela est vrai lorsque vous utilisez PHP comme cgi/fastcgi derrière Apache, et il semble probable que le même problème peut se produire lors de l'exécution dans d'autres configurations de trop. Le problème semble résulter lorsque PHP bouffées de chaleur de sortie de Apache plus rapide qu'Apache peut effectivement envoyer sur le support. Pour les très gros fichiers (ou des connexions lentes), ce qui va entrainer un dépassement de Apache interne du tampon de sortie. Cela provoque Apache pour tuer le processus PHP, qui, bien sûr, les causes de la télécharger à accrocher, ou se terminer prématurément, avec seulement un transfert partiel ayant eu lieu.

La solution est de ne pas appeler flush() à tous. J'ai mis à jour les exemples de code ci-dessus pour en tenir compte, et j'ai mis une note dans le texte en haut de la réponse.

3voto

Emiller Points 1244

Une autre solution est mon module mod_zip pour Nginx, écrit spécifiquement à cet effet:

https://github.com/evanmiller/mod_zip

Il est extrêmement léger et n'invoque pas de processus "zip" distinct ni ne communique via des canaux. Vous pointez simplement sur un script qui répertorie les emplacements des fichiers à inclure, et mod_zip fait le reste.

2voto

Rico Sonntag Points 915

En essayant de mettre en œuvre une dynamique générée à télécharger avec beaucoup de fichiers avec différentes tailles, je suis tombé sur cette solution mais je croise les diverses erreurs de mémoire comme "Allowed memory size of 134217728 bytes exhausted à ...".

Après l'ajout d' ob_flush(); juste avant la flush(); de la mémoire des erreurs disparaissent.

Ensemble, avec l'envoi des en-têtes, ma solution finale ressemble à ceci (Juste à stocker les fichiers à l'intérieur du zip sans structure de répertoire):

<?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);

1voto

Josh Davis Points 12974

Selon le manuel PHP, l'extension ZIP fournit un zip: wrapper.

Je ne l'ai jamais utilisé et je ne connais pas son fonctionnement interne, mais logiquement, il devrait être en mesure de faire ce que vous cherchez, en supposant que les archives ZIP peuvent être diffusés, dont je ne suis pas entièrement sûr de.

Quant à votre question sur la "pile LAMP" il ne devrait pas être un problème tant que PHP n'est pas configuré pour le tampon de sortie.


Edit: je suis en train de mettre une preuve de concept, mais cela ne semble pas trivial. Si vous n'êtes pas expérimenté avec PHP de cours d'eau, il peut se révéler trop compliqué, si c'est encore possible.


Edit(2): en relisant votre question après avoir pris un coup d'oeil à ZipStream, j'ai trouvé ce que va être votre principal problème ici, quand vous dites (emphase ajoutée)

le dispositif de Compression doit fonctionner en mode continu, c'est à dire les fichiers de traitement et de fournir des données à la vitesse de la télécharger.

Cette partie sera extrêmement difficile à mettre en oeuvre car je ne pense pas que PHP fournit un moyen de déterminer comment complète d'Apache du tampon est. Donc, la réponse à votre question est non, vous ne serez probablement pas en mesure de le faire en PHP.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X