AnyEventベースのモジュール
前回まで紹介したのはAnyEventの基本APIのみです。CPANにはこれらをベースにさらに複雑な機能を実装した様々なモジュールがあり、
- 非同期でHTTP通信を行うAnyEvent::HTTP
- 低レイヤのI/
Oを簡単にするAnyEvent::Handle - ソケット接続を行うAnyEvent::Socket
- データベース処理を非同期で行うAnyEvent::DBI
- CouchDBと非同期に通信を行うAnyEvent::CouchDB
- PSGI/
Plackを非同期エンジンで動かすTwiggy
これらを組み合わせて使うことにより、
AnyEvent::Socketを使った簡易HTTPクライアント
リスト11は、GET / HTTP/
というHTTPリクエストは発行したあと、
use strict;
use AnyEvent;
use AnyEvent::Socket;
my $cv = AnyEvent->condvar;
my $guard; $guard = tcp_connect 'your.host.name', 80, sub {
my ($fh) = @_;
undef $guard;
my $w; $w = AnyEvent->io( ……(1)
fh => $fh,
poll => "w",
cb => sub {
undef $w;
my $buf = "GET / HTTP 1.0\r\n\r\n";
my $length = syswrite( $fh, $buf, length($buf) );
if ($length != length($buf)) {
warn "failed to write";
$cv->send;
}
my $r; $r = AnyEvent->io( ……(2)
fh => $fh,
poll => "r",
cb => sub {
my $length = sysread( $fh, my $buf, 8192 );
if ($length > 0) {
print $buf, "\n";
} else {
undef $r;
$cv->send;
}
}
);
}
);
};
$cv->recv;
ソケットがつながったあとHTTPリクエストを送信しなくてはならないので、poll=> "w"
を指定して書き込み可能になるまで待ちます。リクエストを送信したあと
高速に複数サーバとHTTP接続する
AnyEvent::HTTP
前項では簡易HTTP通信を行うスクリプトをスクラッチで実装しましたが、
use strict;
use AnyEvent;
use AnyEvent::HTTP;
my $cv = AnyEvent->condvar;
my $guard; $guard = http_get 'http://gihyo.jp' => sub {
my ($body, $headers) = @_;
undef $guard;
print $body;
$cv->send;
};
$cv->recv;
ただ、
複数URLを並行して取得する
それでは普通の書き方とAnyEventを用いた書き方の違いをはっきり見るために、
http.
> perl http.pl
http://gihyo.jp # 入力
+ http://gihyo.jp -> 200
上記のように標準入力からURLを受け取るようにしておけば、
> cat urls.txt | perl http.pl
普通の書き方の場合
従来の方式で実装すると、
use strict;
use LWP::UserAgent;
main() unless caller();
sub main {
my $ua = LWP::UserAgent->new();
while (my $url = <STDIN>) {
chomp $url;
my $res = $ua->get($url);
print " + $url -> ", $res->code, "\n";
}
}
このスクリプトが抱える潜在的な問題は、
イベント駆動で非同期I/Oを実現した場合
AnyEventを使用し非同期で複数のURLを同時に見るようにすれば、
use strict;
use AnyEvent;
use AnyEvent::Handle;
use AnyEvent::HTTP;
main() unless caller();
sub main {
my $cv = AnyEvent->condvar;
# 標準入力からURLを1行ずつもらうので、STDINを監視する
# AnyEvent::Handleオブジェクトを作成する
my $handle = AnyEvent::Handle->new(
fh => \*STDIN,
# エラー、もしくはEOF時に$cvに終了通知を送る
on_eof => sub { $cv->end },
on_error => sub { $cv->end },
);
# 最後の待ちを有効にするために1回beginを呼んでおく
$cv->begin();
my $w;
my $read_stdin; $read_stdin = sub {
$handle->push_read( line => sub {
my ($handle, $url) = @_;
chomp $url;
if ($url) {
$cv->begin(); # このURLを処理している間は
# $cvの条件を満たさないように
# フラグを立てておく
# AnyEvent::HTTPを使ってリクエストを送る
http_get $url, sub {
my ($body, $headers) = @_;
print " + $headers->{URL} -> " .
"$headers->{Status}\n";
# URLを取得したのでフラグを落とす
$cv->end();
};
}
# 1行読んだので、もう1行読むために$read_stdinを
# イベントキューにいれてもらう
$w = AnyEvent->timer(after => 0, cb => $read_stdin);
} );
};
$w = AnyEvent->timer(after => 0, cb => $read_stdin);
# $cv->end()がbegin()の回数分呼ばれるまで待つ
$cv->recv();
}
普通の書き方と比べてコードは大分複雑になってしまいましたが、
リスト14では標準入力を監視するためにAnyEvent::Handleというモジュールを使っています。このモジュールはpush_
と指定しておくことによって1行分データが溜まると任意のコールバックを呼んでくれるので、
実際のHTTP処理はAnyEvent::HTTPに実装されているhttp_
このコードで重要なのは$cvの扱いです。最終行で$cv->recvとしていますが、
このスクリプトは基本的には標準入力が閉じられたら終了という前提ですので、
また、
> cat urls.txt | perl http.pl
のようなURLのリストを受け取ったあとすぐに標準入力が閉じられてしまうような状況でも正しく動作するようにしています。
比較のため、
以上見てきたように、
まとめ
今回はAnyEventの基本APIとAnyEventを利用したモジュールの使い方を紹介しましたが、
AnyEventの基本APIに関してはそのシンプルさにだまされてはいけません。基本APIだけでも非常に複雑なイベント駆動なプログラムを書くことができますし、
たとえば本連載の前回で紹介したPlackの非同期エンジンTwiggyの中で、
みなさんもこれを機会に非同期プログラミングに挑戦してみてはいかがでしょうか。
次回は村瀬大輔さんでテーマはDBIx::Classです!