PHPでHTTPの並行ダウンロードを実現する(Guzzle編)
PHPで最近注目のHTTPクライアントライブラリにGuzzleがあります。日本での知名度はまだまだという印象ですが、かなり高機能かつ真面目にメンテナンスされている印象で、今後のデファクトスタンダードになりうるライブラリと言えるでしょう。
本稿ではこのGuzzleを使ってWebサーバから並行にダウンロードする方法を紹介します。Webブラウザのように同時に複数コネクションを管理しながらKeep-Aliveでコネクションを使い回しますので、下手なコードで実現するより接続先Webサーバにも優しいはずです。
Guzzleの特徴
まずは、Guzzleについて僕が特徴的だと思う点を紹介します。
- パッと見でわかりやすいインターフェース
- cURLは必須ではないがデフォルトでcURLを使う
- イベントフック(リクエストの前後などでフック可能)の導入
- Composer対応
- クッキー、OAuth、キャッシュ、ロギングなど多くのプラグインを提供
欠点は巨大すぎることと、マニュアルの日本語訳がまだ無いことくらいでしょう。
Guzzleのバージョン
Guzzleを使う上で、Guzzleのバージョンには要注意です。GuzzleにはGuzzle 3とGuzzle 4と2つのバージョンがあります。同じ機能でもGuzzle 3とGuzzle 4とでメソッド名が変わっていたりするので、検索で見つけた記事を読むような場合は注意してください。
Guzzle 3とGuzzle 4は異なるネームスペースを利用していますので、どちらを使っているかは一目瞭然です。Guzzle 3の名前空間は「\Guzzle\」から始まり、Guzzle 4は「\GuzzleHttp\」から始まります。
また、Packagistでのパッケージ名もGuzzle 3は「guzzle/guzzle」、Guzzle 4+は「guzzlehttp/guzzle」となっていますので、composer.jsonに書く際も注意が必要です。
Guzzle 4は今年の3月にリリースされたばかりですが、今から使うならこちらを使った方が良いでしょう。本稿でもGuzzle 4を利用しています。
並行ダウンロードするサンプルコード
Guzzleの一般的な使い方は他のページを見て頂くとして、早速並行ダウンロードのサンプルコードを紹介します。このコードはジェネレータを使いたかったのでPHP 5.5以降でしか動作しませんが、Guzzle 4はPHP 5.4以降で動くはずです。
<?php use GuzzleHttp\Client; use GuzzleHttp\Event\CompleteEvent; $client = new Client(); $client->sendAll(requests_generator($client), [ 'parallel' => 4, 'complete' => function (CompleteEvent $event) { echo 'Completed request to ' . $event->getRequest()->getUrl() . "\n"; echo 'Response: ' . $event->getResponse()->getBody() . "\n\n"; } ]); function requests_generator($client) { // IDの若いGitHubユーザー20人 $loginIds = [ 'mojombo', 'defunkt', 'pjhyett', 'wycats', 'ezmobius', 'ivey', 'evanphx', 'vanpelt', 'wayneeseguin', 'brynary', 'kevinclark', 'technoweenie', 'macournoyer', 'takeo', 'Caged', 'topfunky', 'anotherjesse', 'roland', 'lukas', 'fanvsfan', ]; foreach ($loginIds as $loginId) { yield $client->createRequest('GET', 'https://github.com/'.$loginId.'.keys'); } }
これは4並列でgithub.comの20URLへHTTPSアクセスする例です。ここで利用しているsendAllメソッドが今回のキモで、並列にHTTPリクエストを投げるメソッドになります。第一引数でリクエストの配列またはイテレータを渡し、第二引数の連想配列で並列数とリクエスト終了毎に呼ばれるコールバック関数を指定するようなインターフェースになっています。
これを実際に動かすと、4コネクションを作った上で、作ったコネクションをKeep-Aliveで使い回しながら並列にリクエストを投げてくれます。HTTPSの場合はコネクションの確立に時間がかかるので、こうしたテクニックが特に有効です。
これを実行してみるとジェネレータの順番と異なる順序で結果が返ってくることがあります。4並列の非同期I/O処理になっており、早く返ってきたレスポンスから順に処理されるためです。サンプルコードもPHPというよりNode.jsの方が近いくらいの印象で、非同期I/Oっぽいコードと言えるかもしれません。
ちなみに、PHPは他の言語のように非同期I/Oをうまく扱うような機構はありませんが、cURLエクステンションを使えばHTTP・HTTPSについての非同期I/O処理はギリギリ実現できます。Guzzleでも内部的にはcurl_multi_init関数やcurl_multi_exec関数、curl_multi_select関数などを利用しています。また、cURL無しの環境では上のサンプルコードは直列に動作します。
ベンチマークテスト
Keep-Aliveとコネクションプーリングが有効であることを確認する意味で、簡易的なベンチマークテストを取ってみました。
実行時間(sec) | |
---|---|
(1) 4並列 | 2.2 |
(2) 1並列 | 4.7 |
(3) 1並列、Keep-Alive無し | 22.7 |
(1)が上記のサンプルコードで、(2)は下記のようにcurlコマンド一発で20URLを取得したものです。
$ curl https://github.com/{mojombo,defunkt,pjhyett,wycats,ezmobius,ivey,evanphx,vanpelt,wayneeseguin,brynary,kevinclark,technoweenie,macournoyer,takeo,Caged,topfunky,anotherjesse,roland,lukas,fanvsfan}.keys
(2)は1接続をKeep-Aliveで使い回していると考えられますが、Guzzleで4並列にした方が倍以上速いという結果になりました。
また、(3)は下記のようにfor文でcurlコマンドを20回呼び出したものです。
$ (for i in https://github.com/{mojombo,defunkt,pjhyett,wycats,ezmobius,ivey,evanphx,vanpelt,wayneeseguin,brynary,kevinclark,technoweenie,macournoyer,takeo,Caged,topfunky,anotherjesse,roland,lukas,fanvsfan}.keys ; do curl $i; done)
実装者の経験不足などで(3)のように20回接続しては接続断するコードを書いた場合Guzzleで書くより10倍遅くなる可能性さえありそうです。怖いですね。
ちなみに、Guzzleで更に並列数を上げると多少は速くなりますが、たまに極端に遅くなったりしていたので、最大でも6並列くらいにとどめておいた方が良さそうです。
参考記事
- http://guzzlephp.org/ 本家ドキュメント
- Guzzle 4.0 - mtdowling Guzzle 4.0リリース時のブログ記事
- PHP - curl_multiでHTTP並列リクエストを行うサンプル - Qiita