Perl Hackers Hub

第2回 AnyEventでイベント駆動プログラミング (3)

この記事を読むのに必要な時間:およそ 4 分

AnyEventベースのモジュール

前回まで紹介したのはAnyEventの基本APIのみです。CPANにはこれらをベースにさらに複雑な機能を実装した様々なモジュールがあり,それらをうまく組み合わせて簡単に高機能なイベント駆動プログラムを構築することができます。代表的なものとしては,以下のようなモジュールが登録されています。

  • 非同期でHTTP通信を行うAnyEvent::HTTP
  • 低レイヤのI/Oを簡単にするAnyEvent::Handle
  • ソケット接続を行うAnyEvent::Socket
  • データベース処理を非同期で行うAnyEvent::DBI
  • CouchDBと非同期に通信を行うAnyEvent::CouchDB
  • PSGI/Plackを非同期エンジンで動かすTwiggy

これらを組み合わせて使うことにより,ブラウザと非同期に通信しつつ裏ではデータベースおよびイントラネットのほかのREST APIとも非同期で通信しながら処理を進めるWebサーバ,なんていうものも簡単に作れてしまうわけです。

AnyEvent::Socketを使った簡易HTTPクライアント

リスト11は,任意のホストに接続し,GET / HTTP/1.0というHTTPリクエストは発行したあと,読み込めるだけデータを読み込むサンプルです。AnyEvent::Socketを使ってソケット接続を行います。

リスト11 簡易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リクエストを送信しなくてはならないので,(1)でI/Oウォッチャーにpoll=> "w"を指定して書き込み可能になるまで待ちます。リクエストを送信したあと(2)で再度I/Oウォッチャーを設定し,今度は書き込み可能になるまで待ったあと,読み込みを行います。この読み込み用のI/Oウォッチャーは読み込みが不可能になるまで読み込み続けたいため,sysread( )が成功している間は$rを解放しないようにします。読み込みに失敗したときに$rを解放し,$cv->sendを呼び出します。

高速に複数サーバとHTTP接続する

AnyEvent::HTTP

前項では簡易HTTP通信を行うスクリプトをスクラッチで実装しましたが,それでは対応できないパターンも当然ありますので,実際にAnyEventでHTTP通信する場合はAnyEvent::HTTPを使用するべきです。AnyEvent::HTTPならリスト12のように書くだけで任意のURLの内容をGETで取得できます。POSTなどのリクエストも同様に簡単に書けます。

リスト12 AnyEvent….::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;

ただ,これだけでは非同期I/Oの良いところがまったく使いこなせていません。最初に説明したとおり,協力式マルチタスキングを実装するためには「待ち」の時間をうまく利用する必要があるのですが,この例では1個のURLしか取得しにいっていないため待ちを活かせていません。待ちを活かすには,複数のURLをなるたけ速く取得したいような状況が必要です。

複数URLを並行して取得する

それでは普通の書き方とAnyEventを用いた書き方の違いをはっきり見るために,WebサイトのHTMLをダウンロードするスクリプトをそれぞれの書き方で実装してみましょう。標準入力から1行ずつURLを受け取り,それらをダウンロードするスクリプトhttp.plを実装します。

http.plを次のように実行すると入力待ち状態になるので,URLを入力するとHTMLを取得します。

> perl http.pl
http://gihyo.jp # 入力
+ http://gihyo.jp -> 200

上記のように標準入力からURLを受け取るようにしておけば,複数のURLをファイルに記入して次のように標準入力から渡すこともできます。

> cat urls.txt | perl http.pl
普通の書き方の場合

従来の方式で実装すると,リスト13のようになります。標準入力を1行ずつ読み込みながら,LWP::UserAgentを使用して指定されたURLを1個ずつダウンロードしにいきます。

リスト13 従来方式の実装

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";
    }
}

このスクリプトが抱える潜在的な問題は,取得すべきURLリストの中にレスポンスの遅いサイトが存在した場合に顕在化します。そのサイトに当たってしまった場合,データが返ってくるのを待ってから次の処理を行うことになります。サーバには接続できるもののレスポンスが返ってこないURLを最初に指定してしまうと,リクエストがタイムアウトするまで待ってからほかのURLを取得しにいくことになり,ほかのサイトのレスポンスがどんなに速くても最初に大幅に時間をロスしてしまいます。遅いURLが複数個存在した場合はさらに遅延は深刻化します。

著者プロフィール

牧大輔(まきだいすけ)

オープンソース技術を使ったシステム開発をメインに,講師,執筆活動などを行う。2011年,Perlコミュニティに多大な貢献をもたらした人物に贈られるWhite Camel Awardを受賞。本業の傍ら2009年からのほぼ全てのYAPC::Asia Tokyoで主催をつとめ,その中で現運営母体のJapan Perl Associationも設立。LINE等を経て現在HDE, Inc.勤務。

コメント

コメントの記入