Konieczność ograniczania prędkości pobierania plików przez użytkowników raczej nie występuje zbyt często, niemniej jednak warto znać rozwiązanie (które de facto jest proste w implementacji).
W niniejszym wpisie pokażę, w jaki sposób samodzielnie napisać kod rozwiązujący problem ograniczania prędkości pobierania zasobów z serwera przy pomocy PHP oraz alternatywne rozwiązanie dla serwerów Apache.
Limit prędkości pobierania plików z PHP
Poniżej przedstawiam algorytm na rozwiązanie problemu:
1. Zmieniamy czas wykonywania skryptu na nieograniczony
1 | set_time_limit(0); |
Pliki będą przetwarzane przez skrypt przez cały czas pobierania, nie możemy więc pozwolić na wcześniejsze zakończenie wykonywania kodu.
2. Ustawiamy nagłówki HTTP
1 2 3 4 | header('Cache-control: private'); header('Content-Type: application/force-download'); header('Content-Length: jakis-rozmiar'); header('Content-Disposition: attachment; filename=nazwa-pliku'); |
- Cache-control: private
pliki są buforowane i przeznaczone tylko dla jednego użytkownika, czyli nie mogą być przechowywane we współdzielonej pamięci, - Content-Type: application/force-download
informujemy przeglądarkę, że to plik do pobrania (nie może być wyświetlany w oknie przeglądarki), - Content-Length: jakis-rozmiar
wskazujemy rozmiar pliku podawany w bajtach, - Content-Disposition: attachment; filename=nazwa-pliku
dodajemy nazwę i rozszerzenie pliku, w którym będzie możliwy do pobrania.
3. Czyścimy bufor wyjściowy PHP
1 | flush(); |
4. Otwieramy plik do pobrania
1 | $file = fopen('nazwa-pliku.rar', 'r'); |
Plik zostaje otwarty tylko do odczytu, a wskaźnik jest ustawiany na samym początku pliku.
5. Wysyłanie pliku do klienta
1 2 3 4 5 | while (!feof($file)) { echo fread($file, 1024); flush(); sleep(1); } |
W punkcie tym tworzymy pętlę while, której warunkiem zakończenia jest koniec pliku (sprawdzamy to przy pomocy funkcji feof – jeśli wskaźnik pliku jest na końcu to zwracany jest false, w innym przypadku true).
W pętli tej wyświetlamy pewną ilość danych z otwartego pliku (w tym przypadku 1024 bajty) przy pomocy funkcji fread. Następnie wynik przesyłamy do klienta/przeglądarki (flush), po czym zatrzymujemy działanie skryptu na jedną sekundę (sleep).
6. Zamykamy pobrany plik
1 | fclose($file); |
To wszystko, mamy gotowy skrypt spowalniający pobieranie dowolnych plików :) Przykładowy kod może wyglądać następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | $download_file = 'nazwa-pliku.rar'; $download_rate = 10 * 1024; // 10 kb/s if (file_exists($download_file) && is_file($download_file)) { header('Cache-control: private'); header('Content-Type: application/force-download'); header('Content-Length: ' . filesize($download_file)); header('Content-Disposition: attachment; filename=' . basename($download_file)); flush(); $handle = fopen($download_file, 'r'); while (!feof($handle)) { echo fread($handle, $download_rate); flush(); sleep(1); } fclose($handle); } |
Jak widać, problem jest trywialny. Co więcej, w sieci dostępne są już gotowe biblioteki rozwiązujące całą sprawę, jak choćby QoS Bandwidth Throttle stworzona przez Artura Graniszewskiego, której kod jest na dużo wyższym poziomie niż zaprezentowany przeze mnie (miałem na celu pokazanie jednego ze sposobów na rozwiązanie problemu).
Wykorzystując bibliotekę Artura wystarczy następujący kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | require('throttler.php'); $config = new ThrottleConfig(); // enable burst rate for 3 seconds $config->burstTimeout = 3; // set burst transfer rate to 300 kb/second $config->burstLimit = 307200; // set standard transfer rate to 10.000 kb/second (after initial 3 seconds of burst rate) $config->rateLimit = 10240; // enable module (this is a default value) $config->enabled = true; // start throttling $x = new Throttle($config); $download_file = 'nazwa-pliku.rar'; header('Content-type: application/force-download'); header('Content-Disposition: attachment; filename=' . basename($download_file)); header('Content-Length: ' . filesize($download_file)); echo file_get_contents($download_file); |
QoS Bandwidth Throttle ma większe możliwości, niż powyżej zaprezentowany przeze mnie kod. Otóż teraz nasz plik będzie wysyłany do klienta z prędkością 10kb/s, przy czym limitowanie transferu rozpocznie się dopiero po przekroczeniu trzeciej sekundy pobierania lub też wysłaniu 300 kilobajtów (w przypadku szybszego łącza).
Biblioteka ta działa na nieco innej zasadzie niż powyżej opisana – do funkcji buforującej dane wysyłane do klienta (ob_start) dodana została funkcja zwrotna, w której ustawiamy nasze limity i ograniczenia. Jak to jest zrobione można zobaczyć w pliku throttler.php.
Limit prędkości pobierania plików z Apache
W najnowszej wersji Apache (2.4) pojawił się ciekawy moduł, który przypomniał mi o problemie z ograniczaniem prędkości pobierania plików, co przyczyniło się do powstania tego wpisu.
Mowa tutaj o module mod_ratelimit, którego użycie jest banalnie proste, lecz w większości przypadków w zupełności wystarczy.
Przykładowo, aby ustawić limit pobierania wystarczy taki kod:
1 2 3 4 5 6 | <IfModule mod_ratelimit.c> <FilesMatch "\.(rar|zip)$"> SetOutputFilter RATE_LIMIT SetEnv rate-limit 400 </FilesMatch> </IfModule> |
I to wszystko – ustawiliśmy właśnie maksymalną prędkość wysyłania plików *.rar oraz *.zip na 400 kB/s. Jedynym warunkiem działania jest zainstalowany moduł mod_ratelimit na serwerze.
Kiedy używać PHP, a kiedy mod_ratelimit?
Jak widać, oba rozwiązania są proste w implementacji. Jeśli mamy do dyspozycji serwer Apache w wersji 2.4 to śmiało możemy użyć mod_ratelimit, przy czym należy pamiętać, że kiedyś możemy zechcieć zmienić Apache na serwer nginx (lub inny), czego skutkiem będzie konieczność modyfikacji kodu PHP (to samo w przypadku przeniesienia strony na Apache w starszej wersji).
Z drugiej strony, jak często przenosisz strony między różnymi serwerami? Swoje strony od dobrych kilku lat trzymam na tych samych serwerach i mają się dobrze (może raz czy dwa musiałem wykupić nowy hosting dla kolejnych projektów).
Podsumowując, dla prostych i szybkich projektów instalowanych na uaktualnionych serwerach Apache możemy używać modułu mod_ratelimit, natomiast dla bardziej przyszłościowych i rozwojowych stron radziłbym postawić na większą uniwersalność, czyli zaimplementować bibliotekę QoS Bandwidth Throttle lub stworzyć coś własnego.
Twój kod ma jedną poważną wadę: kompletnie ignoruje nagłówki range, co w praktyce uniemożliwia pobranie większych plików osobom z rwącym łączem. Każde zerwanie połączenia oznaczać będzie doda nich konieczność pobierania pliku od nowa.
@Asgraf: brak nagłówku Range nie nazwałbym poważną wadą, a jedynie małym brakiem wsparcia dla przerywanych łącz :)
Przy zwykłych stronach to raczej nie problem, bo większość osób nie przemieszcza się w miejscu i ma stałe łącze, natomiast korzystając z mobilnego Internetu nie ściągamy dużych plików (limity + duże opłaty). Niemniej jednak dziękuję za zwrócenie uwagi na sprawę, dobrze wiedzieć :)
Wg mnie jest to wada – brak Range.
Stałe łącze oznacza tylko to że idzie kablem, a nie to że jest „niezrywalne”.
Co do implementacji nagłówka RANGE w php, polecam link: http://w-shadow.com/blog/2007/08/12/how-to-force-file-download-with-php/
Natomiast jeżeli nie spieszy nam sie z upgradem Apache, istnieje polski moduł który między innymi potrafi regulować prędkość: http://sysdesign.pl/mod_cband/
@dev-null-dweller: dzięki za link, właśnie trudno mi było znaleźć praktyczny przykład użycia Range :)
Dla Apache jest jeszcze mod_bandwidth. Opisałem jednak tylko mod_ratelimit z tego względu, że oficjalnie został włączony do Apache (co ma swoje plusy).
Pewnie na hostingach współdzielonych ludki nie mają dostępu do takich modułów, więc skrypt w PHP to dla nich wybawienie. Chociaż z drugiej strony po co ktoś na współdzielonym miałby ograniczać prędkość pobierania plików.
Dzięki za opisanie procedury krok po kroku. Chyba muszę coś takiego zaimplementować na jednym swoim serwisie :/
@Paweł , aby podzielić łacze serwera na kilku użytkowników, sam hostuje pliki i przyda mi sie prosty kod w php aby podzielić łaczę.
Całkiem prosty, ale przydatny tip. Czasem warto skorzystać, kiedy potrzebujemy trochę przyciąć transfer ;)
Super, dzięki. Rozwiązałeś mój problem, który męczył mnie już od dłuższego czasu. Muszę przyznać, że nie wpadłem na tą opcję, która okazała się całkiem prosta.
Hm… wysyłanie dużych plików porpzez php jest raczej kiepskim rozwiązaniem – jeżeli nie chcemy zdradzać patha do plików to xsendfile nam to załatwia, jest na apaczu, litespedzie…
@Przemek: wpis dotyczył ograniczania prędkości pobierania. Czy mod_xsendfile posiada taką możliwość?