Perl の HTTP/2 事情

Posted on | 1488 words | ~3mins
perl HTTP/2

この記事は Perl Advent Calendar 2015 の 11 日目の記事です。

昨日の記事は mackee_w さんの「ペライチ PSGI アプリケーションの概念と実証」でした。

今年 2015 年は、HTTP/2 の RFC が出ましたね。というわけで HTTP/2 の話をします。以前 Gotanda.pm #4 にて「Perl の HTTP/2 事情」というタイトルで発表したのですが、それとだいたい一緒です(記事書いてなかったので…)。

HTTP/2 の各言語実装は https://github.com/http2/http2-spec/wiki/Implementations にまとまっているのですが、ここを見ると Perl には Protocol::HTTP2 / http2-perl という実装があるようです。ただ http2-perl は h2-04 準拠なので Older Implementations に入れられており、実質 Protocol::HTTP2 一択です。以前発表した時は Protocol::HTTP2 は draft-17 だったのですが、現在は h2 になっているのできちんと開発は続いているようですね。

How to Use Protocol::HTTP2

というわけで、Protocol::HTTP2 を実際に使ってみましょう。今回はクライアントを Protocol::HTTP2 を使って Perl で書き、nghttp2 のサーバーに繋いでみます。コードは GitHub にあげてあります。

https://github.com/zoncoen-sample/p5-protocol-http2-nghttp2

まずはこんな感じでクライアントを作ります。 on_change_stateon_error にコールバックを登録しておきます。

my $client = Protocol::HTTP2::Client->new(
    on_change_state => sub {
        my ( $stream*id, $previous_state, $current_state ) = @*;
printf "Stream %i changed state from %s to %s\n",
$stream_id, const_name( "states", $previous_state ),
const_name( "states", $current_state );
    },
    on_error => sub {
        my $error = shift;
printf "Error occured: %s\n", const_name( "errors", \$error );
},
);

次にクライアントにリクエストを登録します。ここではリクエストの内容と、成功した時のコールバックを on_done として渡してあげます。まだ実際のリクエストは行われません。

$client->request(
    ':scheme'    => "http",
    ':authority' => $host . ":" . $port,
    ':path'      => "/assets/hello.txt",
    ':method'    => "GET",
    headers      => [
        'accept'     => '*/*',
        'user-agent' => 'perl-Protocol-HTTP2/0.01',
    ],
    on_done => sub {
        my ( $headers, $data ) = @_;
        printf "Get headers. Count: %i\n", scalar(@$headers) / 2;
printf "Get data. Length: %i\n", length($data);
        print $data;
},
);

ここまでできたら、AnyEvent::Sockettcp_connect を使って TCP コネクションをはり、$client->feed() でクライアントに流れてくるデータを渡していきます。クライアントはリクエストが完了すると、リクエストを登録したときの on_done を実行します。

my \$w = AnyEvent->condvar;

tcp*connect $host, $port, sub {
my (\$fh) = @* or die "connection failed: $!";
    my $handle;
$handle = AnyEvent::Handle->new(
        fh       => $fh,
autocork => 1,
on*error => sub {
\$*[0]->destroy;
print "connection error\n";
$w->send;
        },
        on_eof => sub {
            $handle->destroy;
\$w->send;
}
);

    # First write preface to peer
    while ( my $frame = $client->next_frame ) {
        $handle->push_write($frame);
    }

    $handle->on_read(
        sub {
            my $handle = shift;

            $client->feed( $handle->{rbuf} );

            $handle->{rbuf} = undef;
            while ( my $frame = $client->next_frame ) {
                $handle->push_write($frame);
            }
            $handle->push_shutdown if $client->shutdown;
        }
    );

};

\$w->recv;

簡単ですね(?)

\$ carton exec -- perl client-simple.pl
Stream 1 changed state from IDLE to HALF_CLOSED
Stream 1 changed state from HALF_CLOSED to CLOSED
Get headers. Count: 6
Get data. Length: 14
Hello HTTP/2!

リクエストの多重化

HTTP/2 は 1 つの TCP コネクション上で複数のストリームをつかってリクエストの多重化を行うことができます。Protocol::HTTP2 でももちろんリクエストの多重化ができます。

複数のリクエストを同時になげるには、以下のように request() をつなげていきます。この例ではサイズの大きい /assets/largefile と、サイズの小さい /assets/hello.txt の GET を行います。

$client->request(
    ':scheme'    => "http",
    ':authority' => $host . ":" . $port,
    ':path'      => "/assets/largefile",
    ':method'    => "GET",
    headers      => [
        'accept'     => '*/*',
        'user-agent' => 'perl-Protocol-HTTP2/0.01',
    ],
    on_done => sub {
        my ( $headers, $data ) = @_;
        printf "Get headers. Count: %i\n", scalar(@$headers) / 2;
printf "Get data. Length: %i\n", length($data);
        print "Finish getting largefile.\n"
    },
)->request(
    ':scheme'    => "http",
    ':authority' => $host . ":" . $port,
    ':path'      => "/assets/hello.txt",
    ':method'    => "GET",
    headers      => [
        'accept'     => '*/*',
        'user-agent' => 'perl-Protocol-HTTP2/0.01',
    ],
    on_done => sub {
        my ( $headers, $data ) = @_;
        printf "Get headers. Count: %i\n", scalar(@$headers) / 2;
printf "Get data. Length: %i\n", length($data);
        print "$data\n";
},
);

これを実行すると以下のような結果が得られます。まず Stream 1 (largefile) のリクエストが実行され、次に Stream 3 (hello.txt) が実行されますが、さきに実行されたファイルサイズの大きい Stream 1 のリクエストにブロッキングされることなく、Stream 3 のリクエストが先に完了していることが分かります。

\$ carton exec -- perl client-multi-streams.pl
Stream 1 changed state from IDLE to HALF_CLOSED
Stream 3 changed state from IDLE to HALF_CLOSED
Stream 3 changed state from HALF_CLOSED to CLOSED
Get headers. Count: 6
Get data. Length: 14
Hello HTTP/2!

Stream 1 changed state from HALF_CLOSED to CLOSED
Get headers. Count: 6
Get data. Length: 100000000
Finish getting largefile.

HTTP/2 便利

所感

というわけで Perl でも HTTP/2 は使えるよ、という話でした。ただ AnyEvent でやるの割とめんどくさいですね…(コールバックばっかで一昔前の JavaScript っぽい)。 Enjoy!

明日は yusukebe さんです。楽しみですね!(すでに「先走って」書かれているようですが w)

P.S.

これの一個前の記事が去年の Advent Calendar の記事とかいうヤバイ状態でした。書くネタはあったのにブログ書くのサボりすぎ…来年はきちんと書いていきたい。