Archive for the ‘PHP’ Category
Große Dateien komprimiert als Download streamen
Bei großen Dateien gibt es mehrere Probleme. Sie belegen viel Speicherplatz auf der Festplatte, bei der Komprimierung vergeht viel Zeit, die fertig komprimierte Datei belegt wiederum viel Speicherplatz, und eventuell wird auch viel Arbeitsspeicher benötigt beim Komprimieren oder ausliefern via PHP.
Viele dieser Probleme lassen sich lösen, wie ich gleich zeigen möchte. Wie vieles auf dieser Welt ist das jedoch auch mit kleinen Nachteilen versehen, sodass man abwägen muss, was einem wichtig ist.
Mein kleines Beispiel hier geht davon aus dass es eine oder mehrere große Dateien gibt, die gezippt heruntergeladen werden sollen. Die zu komprimierenden Dateien liegen auf einem anderen Server (hier: Netzlaufwerk), es könnte aber auch ein FTP-Server, ein Upload oder sonst irgendeine Quelle sein. Natürlich funktioniert das ganze auch mit lokalen Dateien oder gerade erst erzeugten Daten (beispielsweise MySQL Dump).
Die erste Lösung wäre diese:
<?php set_time_limit(0); copy('\\\\192.168.1.33\\Dateien\\seinfeld.avi', 'seinfeld1.avi'); $zip = new ZipArchive(); $filename = "seinfeld1.zip"; if ($zip->open($filename, ZIPARCHIVE::CREATE)!==TRUE) { exit("cannot open <$filename>\n"); } $zip->addFile('seinfeld1.avi'); $zip->close(); header('Content-type: application/octetstream'); header('Content-Disposition: attachment; filename="' . $filename . '"'); readfile($filename);
Hier haben wir ein 700MB Video als Quelle, das auf einem Netzlaufwerk liegt. Wir kopieren es zuerst auf unsere lokale Festplatte (mit 30MB/s dank Gigabit-Netzwerk) und beginnen dann mit der Komprimierung. Durch die Komprimierung werden temporär weitere 700MB belegt, und danach starten wir direkt mit der Ausgabe der Headern und dem Inhalt der Datei.
Findige Programmierer sehen sofort: Wir können uns das Kopieren der Datei sparen, und die Datei direkt vom Netzlaufwerk an addFile() übergeben. Dadurch sparen wir die 20 Sekunden für die Kopieraktion und beginnen direkt mit der Komprimierung. Das funktioniert auch dank Wrappern mit einigen anderen Protokollen, aber bei einer „unüblichen Quelle“ wie beispielsweise dem MySQL-Dump nicht.
Lösung 2:
<?php set_time_limit(0); $zip = new ZipArchive(); $filename = "seinfeld2.zip"; if ($zip->open($filename, ZIPARCHIVE::CREATE)!==TRUE) { exit("cannot open <$filename>\n"); } $zip->addFile('\\\\192.168.1.33\\Dateien\\seinfeld.avi'); $zip->close(); header('Content-type: application/octetstream'); header('Content-Disposition: attachment; filename="' . $filename . '"'); readfile($filename);
Garnicht schlecht, wir haben den Festplattenbedarf halbiert, und auch der Download startet früher. Aber wir haben nach wie vor das Problem dass der Download erst nach 58 Sekunden beginnt, so lange dauert nämlich die Komprimierung. Das ist eigentlich nicht zumutbar. Das wollen wir verbessern.
Wie kann das gehen? Wir komprimieren nicht die ganze Datei und beginnen mit dem Download wenn die ganze Datei komprimiert wurde, sondern wir komprimieren in 100KB Häppchen und senden permanent die Zwischenergebnisse zum Download. Leider hat das ZIP Format den Nachteil dass es direkt am Anfang die CRC-Prüfsumme der Datei wissen will. Das bedeutet wiederum dass wir nicht darum herum kommen, diese anfangs zu berechnen. Dies dauert 20 Sekunden, der Download startet also nach 20 Sekunden. Es werden dann 100KB vom Netzlaufwerk gelesen, komprimiert und an den Browser ausgeliefert. Das geht solange bis die komplette Datei komprimiert wurde.
Da ZipArchive diese Streaming Technik nicht unterstützt muss manuell Hand angelegt werden und die ZIP-Datei erstellt werden. Dazu gehört der Gesamtheader der ZIP Datei sowie zu jeder Datei innerhalb des Archivs die Dateiheader. Das Projekt ZipStream-PHP von Paul Duncan kann das ganz gut. Leider treten bei der Benutzung einige Notices auf, die aber schnell behoben sind, sie treten auch nur auf wenn man nicht alle optionalen Parameter nutzt. Außerdem gibt es ein Problem mit der crc32b Berechnung, die aber auch leicht zu lösen war. Eine funktionierende Version befindet sich hier.
Der Ablauf:
Nutzung von hash_file() um die CRC32-Prüfsumme zu berechnen und den ZIP-Datei-Header zu generieren. Dann werden jeweils 100KB gelesen, komprimiert und ausgegeben. Dies wird wiederholt bis die Datei vollständig komprimiert und ausgegeben wurde.
In der ZipStream Klasse können natürlich noch einige Parameter angepasst werden, beispielsweise die Größe der Päckchen (hier 100KB), man kann aber auch ein Limit festlegen, ab dem dieses Zerhackstückeln passieren soll, unterhalb dieser Grenze wird die Datei als ganze gelesen und komprimiert.
Und hier nun das Script, welches ZipStream nutzt (Lösung 3):
<?php set_time_limit(0); require 'zipstream.php'; $zip = new ZipStream('seinfeld3.zip'); $zip->add_file_from_path('seinfeld.avi', '\\\\192.168.1.33\\Dateien\\seinfeld.avi'); $zip->finish();
So einfach kann es sein. Es gibt keine temporäre Datei, der Download beginnt früh, und der Arbeitsspeicher wird auch nicht beansprucht.
Ich habe auch nach einer Lösung gesucht, um „richtiges“ Streaming zu machen und dann mit stream_filter_append() und zlib.deflate zu arbeiten, aber da habe ich nichts gefunden. Vielleicht könnte man ZipStream-PHP so erweitern dass es damit funktioniert.
Aber wie bereits gesagt haben diese Streaming Lösungen auch Nachteile. Ein Nachteil ist beispielsweise die fehlende Möglichkeit, den Download zu pausieren und später fortzufahren (HTTP Ranges). Würde man die Datei nicht streamen sondern wie in den ersten Beispielen erstmal als Datei abspeichern würde das funktionieren, ebenso könnte man dann den eigentlichen Download an einen leichtgewichtigen Webserver wie nginx oder lighttpd übergeben oder mittels Sendfile die Datei vom Apache ausliefern lassen. Dann würde auch HTTP Range unterstützt und mehrere Personen könnten die Datei downloaden. Je nach Anwendungsfall ist also manchmal auch die Nicht-Streaming-Methode besser.
Hier nochmal eine Übersicht der 3 Lösungen:
Lösung 1 | Lösung 2 | Lösung 3 | |
---|---|---|---|
Download beginnt nach | 75 Sekunden | 58 Sekunden | 20 Sekunden |
Download beendet nach (in Sekunden) | 141 Sekunden | 109 Sekunden | 98 Sekunden |
zusätzlicher Festplattenverbrauch | 1400 MB | 700 MB | 0 MB |
memory_get_peak_usage(true) | 500 KB | 500 KB | 1 MB |
Gewinnspiel bei antwerpes
Möchte kurz auf ein nettes Gewinnspiel hinweisen, bei dem man bis Weihnachten 4 PHP Bücher gewinnen kann.
Bin gespannt auf die nächsten Fragen, die erste ist gar nicht so einfach, da muss man kurz recherchieren 😉
Viel Spass!
http://antwerpes.it/weihnachtsgewinnspiel-2010-teil-1/2010/11/
Linkpool Nummer 12
Primzahlen testen mit Regulären Ausdrücken:
http://zmievski.org/2010/08/the-prime-that-wasnt
Infos zu bald kommenden skalaren Type Hints:
http://schlueters.de/blog/archives/148-More-on-scalar-type-hints-in-PHP-trunk.html
http://sebastian-bergmann.de/archives/900-Scalar-Type-Hints-in-PHP-5.3.99.html
Interessante Diskussion über Type Hints: Weak oder Strict:
http://www.zfforum.de/showthread.php?t=6505
Kostenloses PHPUnit Training als PDF:
http://blog.nickbelhomme.com/php/phpunit-training-course-for-free_282
10 Gründe das Zend Framework zu nutzen:
http://blancer.com/tutorials/40338/10-compelling-reasons-to-use-zend-framework/
10 Fehler, die MySQL Nutzer immer wieder machen:
http://blogs.sitepoint.com/2010/11/19/mysql-mistakes-php-developers/
Der Unterschied zwischen fsockopen() und stream_socket_client()
Oft findet man in seinem Code (oder den benutzten Libraries wie beispielsweise Zend Framework) die Funktion fsockopen(). Daran ist auch eigentlich nichts verwerfliches, sie ist seit PHP 4 verfügbar und tut im Prinzip das selbe wie stream_socket_client(): Sie baut eine Socket-Verbindung zu einem entfernten Server auf, beispielsweise zu einem Webserver:
$socket = @fsockopen('ssl://www.example.org' , 443 , $errno , $errstr , 30);
fsockopen() versteht also auch SSL wie man oben sieht. Man kann nun ganz normal fread(), fwrite() und fclose() verwenden.
Wo ist nun also der Vorteil von der seit PHP5 verfügbaren Funktion stream_socket_client()?
Nehmen wir an unser Server besitzt mehrere IP-Adressen. fsockopen() kann man nicht beibringen welche der IP-Adressen es nutzen soll um die ausgehende Verbindung aufzubauen. Mit stream_socket_client() geht das wie folgt:
$socket_options = array('socket' => array('bindto' => '192.0.2.1:0')); $socket_context = stream_context_create($socket_options); $socket = stream_socket_client('ssl://www.example.org:443', $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $socket_context);
Ein weiterer Pluspunkt sind die vielfältigen Optionen bei der SSL Verbindung. Beispielsweise kann man die Überprüfung des Zertifikats aktivieren, selbst signierte Zertifikate erlauben oder auch nicht, den Pfad zum CA-File festlegen und einiges mehr.
$socket_options = array('ssl' => array('verify_peer' => true, 'allow_self_signed' => true));
Es gibt auch eine Menge Optionen für die HTTP(s) Verbindungen. Man kann den User Agent ändern, einen Proxy-Server definieren, als Protokoll-Version entweder 1.0 oder 1.1 verwenden etc.
$socket_options = array('http' => array('user_agent' => 'Mein UA', 'protocol_version' => '1.1'));
Ich suche noch die Nachteile von stream_socket_client, bisher habe ich keine gefunden. Beide beherrschen den non-blocking Modus, mit beiden kann man auch permanente Verbindungen aufbauen, die wiederverwendet werden falls man kurze Zeit später nochmal genau die selbe Verbindung aufbaut, und stream_socket_client() bietet noch viele weitere Stream-Funktionen, die fsockopen() häufig nicht unterstützt.
Wer von euch nutzt bevorzugt fsockopen und warum?
Slides der ZendCon 2010
Vom 1. bis zum 4. November fand dieses Jahr die ZendCon statt, eine der wohl größten und bedeutendsten internationalen Konferenzen. Mit über 70 Sessions wurde von hochkarätigen Speakern (die Namen muss man sich mal alle anschauen, phänomenal) der Tagesplan reich gefüllt und sicher war/ist für jeden etwas dabei.
Da ihr wahrscheinlich, wie ich auch, nicht teilgenommen habt, stelle ich hier die bisher verfügbaren Präsentationen zusammen, zum Nachlesen der aktuell wichtigen Themen aus dem PHP- und Webumfeld. Falls ihr weitere Präsentationen gefunden habt, gebt bitte Bescheid.
Montag – 1. November 2010
Taming the Untestable Beast
Sebastian Bergmann, Stefan Priebsch
http://www.slideshare.net/sebastian_bergmann/taming-the-untestable-beast
Zend PHP Certification Boot Camp
Christian Wenz
Part I
Cloudy with a Chance of PHP
Josh Holmes, Eli White, Travis Swicegood
http://eliw.com/presentations/zendcon2010/zend-Cloudy_with_a_Chance_of_PHP.pdf