HTTP Range-Request Header in PHP parsen
Hört sich eigentlich nach einer einfachen Aufgabe an: HTTP-Clients können beim Download nur Teile einer Datei anfragen, beispielsweise die ersten 500 Bytes eines Videos. Der Server announced die Unterstützung dieses Partial-Downloads mit dem Response-Header:
Accept-Ranges: bytes
Ein HTTP-Client, der partielle Downloads unterstützt kann nun mit dem folgenden Header die ersten 500 Bytes anfordern:
Range: bytes=0-500
Ein Webserver, der direkt die Datei ausliefert, tut wie ihm befohlen. Wird die angefragte Datei jedoch von einem PHP-Script ausgeliefert, muss im PHP-Script dieser Header geparst werden, damit man in PHP weiß wie viele Bytes man ausliefern soll.
Durchsucht man das Internet nach einer Antwort, findet man sehr häufig folgende Lösungen. Achtung, nicht dem RFC https://tools.ietf.org/html/draft-ietf-http-range-retrieval-00 entsprechend, nicht unbedingt nutzen:
http://stackoverflow.com/questions/2209204/parsing-http-range-header-in-php
function getRanges() { return preg_match('/^bytes=((\d*-\d*,? ?)+)$/', @$_SERVER['HTTP_RANGE'], $matches) ? $matches[1] : array(); }
https://gist.github.com/codler/3906826
preg_match('/bytes=(d+)-(d+)/', $_SERVER['HTTP_RANGE'], $matches)
https://licson.net/post/stream-videos-php/
// Parse the range header to get the byte offset $ranges = array_map( 'intval', // Parse the parts into integer explode( '-', // The range separator substr($_SERVER['HTTP_RANGE'], 6) // Skip the `bytes=` part of the header ) ); // If the last range param is empty, it means the EOF (End of File) if(!$ranges[1]){ $ranges[1] = $size - 1; }
http://stackoverflow.com/questions/2209204/parsing-http-range-header-in-php
if (isset($_SERVER['HTTP_RANGE'])) { if (!preg_match('^bytes=\d*-\d*(,\d*-\d*)*$', $_SERVER['HTTP_RANGE'])) { header('HTTP/1.1 416 Requested Range Not Satisfiable'); header('Content-Range: bytes */' . filelength); // Required in 416. exit; } $ranges = explode(',', substr($_SERVER['HTTP_RANGE'], 6)); foreach ($ranges as $range) { $parts = explode('-', $range); $start = $parts[0]; // If this is empty, this should be 0. $end = $parts[1]; // If this is empty or greater than than filelength - 1, this should be filelength - 1. if ($start > $end) { header('HTTP/1.1 416 Requested Range Not Satisfiable'); header('Content-Range: bytes */' . filelength); // Required in 416. exit; } // ... } }
All diese Beispiele unterstützen jedoch nicht die volle Funktionalität, die der Range-Header bietet. Folgende Header sind beispielsweise möglich:
Range: bytes=0-500 // Die ersten 500 Bytes
Range: bytes=-500 // Die letzten 500 Bytes (nicht 0-500!)
Range: bytes=500- // Ab Byte 500 bis zum Ende
Range: bytes=0-500,1000-1499,-200 // Die ersten 500 Bytes, von Byte 1000 bis 1499, und die letzten 200 Bytes
Die meisten Code-Snippets, die man im Internet findet, unterstützen nur Beispiel 1, bei dem ein StartByte und ein EndByte explizit hingeschrieben wird.
Manche unterstützen eine fehlende Angabe, wie in Beispiel 2, und setzen es dann auf 0, was aber falsch ist!
Fast kein Code-Snippet unterstützt die kommaseparierte Angabe von mehreren Ranges. Man fragt sich natürlich auch welche Clients so etwas brauchen und machen, aber theoretisch geht es, und die Applikation sollte im Idealfall damit umgehen können.
Ich will hier nur auf die Problematik hinweisen, eine vollständige Funktion zu erstellen, die alle 4 möglichen Varianten unterstützt, bleibt dem geneigten Leser überlassen 🙂
Wer sich mit dem Thema beschäftigt, sollte auch die Beobachtungen von Steve Souders lesen, iOS macht ganz komische Requests bei Videos…
Ich hatte mich mit dem Thema auch mal beschäftigen dürfen aber in einem Java Servlet. Da hatte alles auch erst prima mit dem Desktop Browser funktioniert und Probleme gab es damals auf nem ipad.
Zuerst wurde immer das erste Byte angefordert, was vermutlich ein Check sein sollte, ob ranges überhaupt vom Server unterstützt sind. Und dann wurden Bytes zufällig über das Video verteilt geladen. Ich denke das sollte wohl helfen schneller zu einem anderen Punkt zu springen ohne lange Ladezeit.
War auf jeden Fall mal interessant das zu untersuchen und noch spannender zu sehen, dass es sich seit einigen Jahren nicht geändert hat 😉
Norbert
19 Okt 17 at 14:00
Wo wäre denn einen Einsatz vom HTTP Range sinnvoll? Du schreibst zwar um ein Video zum Teil herunterladen, wo liegt da aber genau den Sinn oder für welchen Zweck würde man das verwenden. Heute hat doch so gut wie jeder schnelles Internet, daher denke ich nicht das man so auf die Geschwindigkeit achten müsste
Paul
31 Jul 18 at 05:47
@Paul Das einfachste Beispiel sind Videos. Stell dir vor, in deinem Videoplayer spulst du vor an die Stelle 80%. Dann willst du die Datei wenns geht ab der Stelle 80% runterladen, und nicht den ganzen Quatsch vorher. Dafür braucht(e) man Range-Requests.
Oder aber du willst kleine Vorschaubildchen erstellen alle 10 Sekunden, damit du beim hin- und herspulen schon Vorschaubildchen angezeigt bekommst. Dazu kann man mit Hilfe der Range-Requests kleine Bereiche runterladen, in denen hoffentlich ein kompletter Video_Frame enthalten ist, und daraus dann die Vorschaubilder extrahieren und anzeigen. Dazu musst du nicht die ganze Datei runterladen (mit deinem trafficbeschränkten Mobilfunktarif), was ja auch bei einem mehrstündigen Video einige Minuten dauern kann…
Heutzutage gibt es als Alternative zu HTTP-Range-Requests für Videos + Audio das Protokoll HLS, das im Prinzip was sehr ähnliches macht, nur besser (man kann damit an die Stelle „Minute 04:56“ vorspringen, ohne die Byte-Position davon erraten zu müssen beispielsweise…)
https://en.wikipedia.org/wiki/HTTP_Live_Streaming
Ein weiteres Beispiel wären Download-Manager. Die nutzen häufig auch Range-Requests. Statt mit einem TCP-Stream die ganze Datei runterzuladen (der vielleicht auf 1MB/s beschränkt ist auf dem Weg oder dem Server), lädt man die Datei in 10 gleichzeitigen Streams runter. Wenn jeder Stream davon auf 1MB/s beschränkt ist, hat man schon 10MB/s Downloadspeed, man kann die Leitung besser auslasten.
Beispiel: Du willst eine Linux-Distribution runterladen. Diese liegt auf 50 Uni-Servern bereit. Anstatt nun von einem davon die Datei runterzuladen (mit 1MB/s), könntest du die Datei in 50 Teilen anfordern, von jeder Uni ein Teil. Damit wirst du deine 400 MBit/s zuhause ziemlich sicher voll auslasten, auch wenn die einzelnen Uni-Server auf 10MBit/s beschränkt sein würden (was sie meistens nicht sind).
Michael Kliewe
1 Aug 18 at 12:23
Michael, vielen Dank für die ausführliche Erklärung, man lernt nie aus! Du hast einen Super Blog.
Paul
1 Aug 18 at 13:48
Dein Artikel hat mir sehr geholfen für meine REST-API einen Interpreter für Downloads zu schreiben, der folgende Patterns berücksichtigt:
^bytes=(?\d+)-(?\d+)$
^bytes=(?-\d+)$
^bytes=(?\d+)-$
Da ich Dir mit diesem Artikel Dein Ranking sehr gerne gönne, und aufgrund meiner Faulheit, folgene Frage:
Kann ich ein negatives Offset (vom Ende des Files / Streams) angeben mit einer Länge? Das wäre folgenes Pattern
^bytes=(?-\d+)-(?\d+)$
codekandis
4 Jan 19 at 04:04