Często dochodzi do konieczności pobrania zewnętrznych zasobów, zwłaszcza w przypadku korzystania z różnych usług sieciowych (tj. REST, XML-RPC). Czasem nawet musimy pobrać kod całej strony, np. Google, by wyszukać na której pozycji znajduje się nasza strona.
Do operacji tej lepiej jest użyć cURL, file_get_contents, fopen czy może fsockopen? Jeśli też interesuje Cię problem wydajności każdego rozwiązania to zapraszam do niniejszego artykułu, w którym prezentuję wyniki swoich testów.
Sposób przeprowadzania testu
Do testu wykorzystałem Benchmark z pakietu PEAR. Jest to sprawdzony i przyjemny w obsłudze benchmark, więc oszczędziłem sobie przy okazji trochę czasu.
Edit: test został wykonany z użyciem prostego benchmarku:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | function Benchmark($function, $iterations=1000) { set_time_limit(0); if (is_callable($function) === true) { $result = microtime(true); for ($i = 1; $i <= $iterations; $i++) { call_user_func_array($function, null); } return round(microtime(true) - $result, 8); } return false; } |
Testy zostały wykonane na: Windows XP, Apache 2.2.6, PHP 5.2.5.
file_get_contents
Najczęściej i najchętniej chyba wykorzystywana metoda. Popularność jak zwykle wynika z lenistwa programistów, czyli prostoty wykorzystania. Bowiem wystarczy pojedyncze wywołanie funkcji, by uzyskać kod pobieranego zasobu.
Kod pojedynczego wywołania wygląda następująco:
1 | $page = file_get_contents('http://localhost/blog.kamilbrenk.pl/'); |
Prostota wykorzystania funkcji file_get_contents jest zarówno zaletą, jak i wadą – otóż nie mamy zbyt wielu możliwości formatowania żądania. Mimo iż można pobawić się trochę w nagłówkach, niewiele to raczej nam daje:
1 2 3 4 5 6 7 8 9 10 | $http = array( 'method' => 'POST', 'header' => 'Content-Type: application/x-www-form-urlencoded' ); $page = file_get_contents( 'http://localhost/blog.kamilbrenk.pl/', false, stream_context_create(array('http' => $http)) ); |
fopen
Sposób podobny do file_get_contents, lecz wymaga włożenia trochę większego trudu do pobrania zawartości danej strony. Z tego powodu przez wielu nielubiana, a przynajmniej przeze mnie :-)
Oto wykorzystany kod testowy:
1 2 3 4 5 6 7 8 | $handle = fopen("http://localhost/blog.kamilbrenk.pl/", "r"); $data = ''; while (!feof($handle)) { $data .= fread($handle, 4096); } fclose($handle); |
Aby wykorzystywać funkcję fopen do otwierania zewnętrznych zasobów to w ustawieniach konfiguracyjnych PHP musimy ustawić allow_url_fopen na On oraz wyłączyć tryb bezpieczny (safe mode).
Również i tutaj możemy konfigurować własne nagłówki i kilka innych rzeczy, tj.:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | $http = array( 'method' => 'GET', 'header' => 'Accept-language: pl\r\n' . 'Cookie: foo=bar\r\n' ); $fp = fopen( 'http://www.example.com', 'r', false, stream_context_create(array('http' => $http)) ); fpassthru($fp); fclose($fp); |
fsockopen
Dająca multum możliwości funkcja, lecz niestety również dość skomplikowana i niechętnie wykorzystywana – przynajmniej przez początkujących.
Niemniej jednak pozwala nawiązywać połączenia chyba każdego rodzaju (SSL, TLS dla TCP/IP).
Zasada jest prosta: łączymy się z podanym adresem (na wybranym porcie). Następnie przy pomocy funkcji fwrite dołączamy do otwartego połączenia wybrane przez nas nagłówki (odpowiednio sformatowane), po czym przy użyciu pętli odbieramy przesyłkę.
W praktyce może wyglądać to następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | $fp = fsockopen("localhost", 80, $errno, $errstr, 30); if(!$fp) { echo $errstr; } else { fwrite($fp, "GET /blog.kamilbrenk.pl/ HTTP/1.0\r\n" . "Host: www.php.net\r\n" . "Connection: Close\r\n\r\n"); $data = ''; while (!feof($fp)) { $data .= fread($fp, 4096); if (substr($data, -9)=="\r\n\r\n0\r\n\r\n") { exit; } } } |
Jak widać, nie jest to zbyt piękne i przejrzyste rozwiązanie. Mimo wszystko będziesz miał często do czynienia z powyższym kodem, zwłaszcza przy korzystaniu z usług sieciowych.
Na potrzeby testów zrobiłem mały eksperyment – pobieram zawartość zarówno z wykorzystaniem protokołu HTTP w wersji 1.0, jak i HTTP 1.1.
Oto zmieniony kod dla wersji HTTP 1.1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | $fp = fsockopen("localhost", 80, $errno, $errstr, 30); if(!$fp) { echo $errstr; } else { fwrite($fp, "GET /blog.kamilbrenk.pl/ HTTP/1.1\r\n" . "Host: www.php.net\r\n" . "Connection: Close\r\n\r\n"); $data = ''; while (!feof($fp)) { $data .= fread($fp, 4096); if (substr($data, -9)=="\r\n\r\n0\r\n\r\n") { exit; } } } |
cURL
I przejdźmy do ostatniego rozwiązania – biblioteki cURL. Jest to najbardziej uniwersalne i przydatne narzędzie, którego znajomość przyda się nam niejednokrotnie.
Z wykorzystaniem biblioteki możemy się logować na stronie, przechodzić między podstronami, otwierać wiele adresów na raz, modyfikować nagłówki – możemy zrobić niemal wszystko!
Oczywiście kosztem tylu możliwości jest trudniejsza konfiguracja, lecz tylko na początku, ponieważ obsługi biblioteki dość łatwo się nauczyć.
Na potrzeby naszego przykładu wykorzystamy taki oto prosty kod:
1 2 3 4 5 6 7 8 9 10 | $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "http://localhost/blog.kamilbrenk.pl/"); curl_setopt($ch, CURLOPT_FAILONERROR, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)'); $output = curl_exec($ch); curl_close($ch); |
Analiza powyższego kodu nie powinna nikomu sprawić trudności (przynajmniej jeśli potrafi język angielski w stopniu podstawowym).
Jedyną wadą cURL, którą teraz dostrzegam, to konieczność instalacji dodatkowej biblioteki (niestety nie wszystkie jeszcze serwery mają ją wbudowaną).
Które rozwiązanie będzie najwydajniejsze?
Aby wyniki były bardziej wiarygodne, przeprowadziłem kilka prób tego testu (czas podawany w sekundach).
Metoda / requesty | 1 | 10 | 100 | 1000 | 10000 |
file_get_contents | 0.2235 (0.2235) |
0.2078 (2.0785) |
0.2029 (20.287) |
0.1980 (198.052) |
0.2555 (2554.93) |
fopen | 0.2018 (0.2018) |
0.1930 (1.9296) |
0.1965 (19.6463) |
0.1903 (190.343) |
0.2866 (2865.57) |
fsockopen – HTTP 1.0 | 0.2267 (0.2267) |
0.1883 (1.8832) |
0.1961 (19.6064) |
0.2133 (213.30) |
0.2460 (2460.17) |
fsockopen – HTTP 1.1 | 0.1962 (0.1962) |
0.1933 (1.9327) |
0.2046 (20.4571) |
0.2263 (226.324) |
0.2294 (2293.68) |
cURL | 0.1895 (0.1895) |
0.1814 (1.8137) |
0.1842 (18.4159) |
0.2273 (227.325) |
0.2314 (2314.05) |
Konkluzje
Jak widać, najlepiej uplasował się tutaj cURL. Choć przy większej ilości wywołań jest już na gorszej pozycji, to posiada możliwość nawiązywania kilku połączeń (interfejs curl_multi), co jeszcze bardziej skraca czas połączeń (dzięki stosowaniu gniazd nieblokujących).
Pozostałe rozwiązania są czasem wolniejsze, czasem szybsze – nie bardzo rozumiem takiego wyniku i nie wiem z czego to wynika, trudno. Benchmark udostępniam w załączniku na dole strony, także każdy może indywidualnie przetestować poszczególne rozwiązania u siebie.
Najgorzej natomiast uplasowały się file_get_contents oraz fopen, który ma dodatkowo kilka ograniczeń.
Dla mnie najlepszym wyborem jest cURL i od teraz zamierzam posługiwać się głównie tą biblioteką. Choć nie widać wielkiej różnicy między tymi funkcjami w powyższym porównaniu, w praktyce stanowi to dużą różnicę (zwłaszcza przy konieczności częstego odwoływania się do zewnętrznych zasobów).
IMHO takie porównywanie nie jest nie do końca miarodajne. Rozumiem, gdybyś odwoływaj się wszędzie do localhosta, a tak, to pozostaje kwestia obciążeń Sieci, którą ciężko tu zmierzyć… ;)
Porównanie na wyrost – trzeba by tak z 10000 razy zapytać localhosta…
—
Sam osobiście preferuję cURL’a. Jeszcze nie trafiłem na serwer gdzie nie był zainstalowany. Za to często trafiałem na serwery gdzie allow_url_fopen był false.
Macie racje Panowie. Tak myślałem, że odwoływanie się do stron w sieci nie jest zbyt miarodajne, jednak widziałem podobne benchmarki działające na podobnych zasadach i też tak zrobiłem – teraz już wiem, że to był błąd :-)
Testy wykonałem ponownie, zwiększająca liczbę prób do 10.000 i odwołując się do localhosta. Mimo to jakieś dziwne wyniki powychodziły :D
Podziel uzyskane czasy, przez ilość requestów – łatwiej będzie porównywać.
Dodałem też czasy pojedynczych requestów :-) I tak mam trochę zmieszane uczucia do wyżej uzyskanych wyników, coś mi tam nie gra :D
Przy tak niskich rzędach czasów nie jesteś w stanie przeprowadzić miarodajnych testów na własnej maszynie. Już zwykły hosting ze stałym gwarantowanym obciążeniem procesora będzie wystarczający. W tym przypadku odpadnie Ci również kwestia optymalizacji Linux’a i Apache’a.
Poza tym nie sprawdzasz pamięci, a może być tak, że któraś metoda jest bardziej pamięciożerna niż procożerna i nie może rozwinąć skrzydeł przy zadanym limicie.
Dobrym pomysłem przy pobieraniu wielu stron z jednego serwera jest pipelining. Trudno mi powiedzieć, czy cURL go wspiera.
@Piotrek: dziękuję za wartościowe uwagi :) wiem, że niezbyt przemyślałem swoje pomiary, to fakt.
Jak wyseparować zawartość zassaną przez file_get_content?