数年前から Advent Calendar に憧れがあったんですが,今年はライバル?が少なくて初投稿できてひゃっほーい,な @dayflower です。
クライアントの性能テスト用に,単純な応答をゆっくり返すモックサーバをささっと書く必要に駆られたことはありませんか。わたしはあります。
(単純な) レスポンスを,だらだらと返したい!でも大量にリクエストをさばきたい!できれば単純に書きたい!
AnyEvent
ベースの PSGI compliant なウェブサーバの Twiggy
を使えば,応答を非同期に返すことができるので,低コスト低リソースでそんなことができます。
おことわり
- おそくておそくて connection すら張れないよ,という状況は模倣できません。
-
Twiggy
(というかAnyEvent::Socket
) が全力で accept するので。 - ふつうに event model でないサーバアプリたててください。
- 503 を模倣するとかなら (もちろん) できます。
-
- 1バイト送るのに1秒かかるの!,という状況は模倣していません。
- 実のところ
Twiggy
(&AnyEvent
) の得意分野なので,やってできないことはありません。responder にとりあえず empty body 返して,チマチマ writer で書き込めばいいのです。
- 実のところ
まずはシンプルに
単純なレスポンスを,リクエストからちょい遅れてから返すサーバをつくります。
とりあえず, cpanfile
から。
requires "Twiggy";
requires "AnyEvent";
AnyEvent
は Twiggy
が依存しているので書かなくてもいいです。なんとなく書きました。
次にアプリケーション本体です。
use strict;
use warnings;
use AnyEvent;
use Plack::Loader;
my $app = sub {
my $env = shift;
if ($env->{REQUEST_URI} =~ m{(?: ^ | / ) favicon.ico $}x) {
return res_404();
}
warn "request: " . $env->{REQUEST_URI};
return sub {
my $responder = shift;
my $w; $w = AE::timer(5, 0, sub {
my $content = '200 OK';
my %headers = (
'Content-Type' => 'text/plain',
'Content-Length' => length $content,
);
$responder->([ 200, [ %headers ], [ $content ] ]);
undef $w;
});
};
};
sub res_404 {
return [ 404, [ 'Content-Type', 'text/plain' ], [ '404 Not Found' ] ];
}
Plack::Loader->load('Twiggy', host => '0.0.0.0', port => 20080)->run($app);
そのまま carton exec server.pl
などとすると動きます。
とても単純だったので拍子抜けしたのではないでしょうか。
いくつかポイントがあります。
- ふつうの PSGI App のように,リクエストに対してささっと返すこともできます。
- favicon への応答に関するところです (サブルーチン化する必要はないのですが)
- favicon のログがでるとうざかったりするので,除外しています。
- favicon への応答に関するところです (サブルーチン化する必要はないのですが)
- 非同期に応答するには,ふつうの PSGI handler の戻り値 (配列) ではなく,サブルーチンリファレンスを返します。そのサブルーチンに第一引数として,応答用のサブルーチンリファレンス (上記サンプルでの
$responder
) が渡されるので,それを使ってレスポンスを返します。- 上記サンプルでは,さらに
AE::timer
(AnyEvent->timer
) を利用して,5秒間応答を遅延させています。
- 上記サンプルでは,さらに
- 本当はサーバが非同期に応答できるかどうか,
psgi.streaming
環境変数を検査して判定する必要があります。- ですが今回は
Twiggy
決め打ちなので,省略しています。
- ですが今回は
- という,いままで述べたあたりのことは PSGI Spec の Delayed Response and Streaming Body 項 に全部書いてありますので,そちらを読むことをおすすめします。
- (
Twiggy
決め打ちなので)Plack::Loader
使っていますが,$app
をそのまま返せば,ふつうの PSGI ファイルになります。- ただし
psgi.streaming
をサポートした非同期応答に対応した PSGI サーバでないと動きません。
- ただし
応答時間をランダムに変えたり,相手によって変えたり,もっとまともなレスポンスを返したり,夢がひろがりますね。
応用 (1) カラフルに出力する
クライアントの送信する内容を覗いてみたくなったことはありませんか。
use strict;
use warnings;
use Term::ANSIColor qw( colored );
use AnyEvent;
use Plack::Request;
use Plack::Loader;
my $app = sub {
my $env = shift;
my $req = Plack::Request->new($env);
if ($req->path_info =~ m{(?: ^ | / ) favicon.ico $}x) {
return res_404();
}
print "\n";
# Request Method & URI
my $meth_uri = sprintf '%s %s', $req->method, $req->uri->as_string;
print colored([qw( cyan reverse )], $meth_uri), "\n";
# Headers
foreach my $field ($req->headers->header_field_names) {
my $vals = $req->header($field);
print colored([qw( yellow )], "$field: $vals"), "\n";
}
# Query Parameters
my $query_params = $req->query_parameters;
foreach my $field (sort keys %$query_params) {
my $vals = join q{, }, $query_params->get_all($field);
print colored([qw( green )], "$field: $vals"), "\n";
}
# TODO for Body Parameters!
# Start to respond
my $delay = 1 + int(rand(3));
return sub {
my $responder = shift;
my $w; $w = AE::timer($delay, 0, sub {
my $content = '200 OK';
my %headers = (
'Content-Type' => 'text/plain',
'Content-Length' => length $content,
);
$responder->([ 200, [ %headers ], [ $content ] ]);
undef $w;
});
};
};
sub res_404 {
return [ 404, [ 'Content-Type', 'text/plain' ], [ '404 Not Found' ] ];
}
Plack::Loader->load('Twiggy', host => '0.0.0.0', port => 20080)->run($app);
これで
こんな感じで表示されるようになります。
(おまけで,遅延時間を乱数で決めるようにしています)
Request body の表示は実装していません。JSON のリクエストだったらパースして整形して表示したり,とかやってみてもいいですね。
応用 (2) イライラする Proxy を作る
せっかく非同期に応答できるので, AnyEvent::HTTP
を使って,なんちゃってプロキシをつくってみようと思います。
use strict;
use warnings;
use AnyEvent;
use AnyEvent::HTTP;
use Plack::Loader;
my $app = sub {
my $env = shift;
warn $env->{REQUEST_URI};
return sub {
my $responder = shift;
my $w; $w = AE::timer(
3, 0,
sub {
http_request(
$env->{REQUEST_METHOD},
$env->{REQUEST_URI},
sub {
my ($body, $headers) = @_;
$responder->([ $headers->{Status}, [ %$headers ], [ $body ] ]);
undef $w;
}
);
}
);
};
};
Plack::Loader->load('Twiggy', host => '0.0.0.0', port => 8080)->run($app);
多段 callback になってしまって,見づらいですがご容赦を (どうすればいいんでしょう)。
本当はリクエストされた HTTP ヘッダの内容をプロキシ先に投げたりとかしないとちゃんと完全には動かないんですが,そのへんは宿題にしましょう。
ダイヤルアップ時代の気分をそこそこ感じられることうけあいです。
参考文献
- Twiggy 添付の websocket を利用したサンプルの chat.psgi
- PSGI Spec の Delayed Response and Streaming Body 項
-
lestrrat さんによる Perl Hackers Hub の AnyEvent の記事 in WEB+DB PRESS
- Perl 徹底攻略にも収録されています。他にも Perl hacker にはお役立ちな記事も多くて,超おすすめです。
おわりに
Twiggy
, イベント駆動まわりのベースが AnyEvent
なので,ほかの AnyEvent
を使ったライブラリ (AnyEvent::HTTP
とか AnyEvent::IRC
とか) と組み合わせて使うにはもってこいだったりします。
さて,明日は lestrrat さんの『普通のデーモンを 1) Server::Starterでホットデプロイ+ 2) slow-restart対応にする』です!……おや,書く人がまだ決まってない!書きたい人はチャンスです!