行き帰りの満員電車で痴漢に間違われるのが怖くて手をあげていたら、 ウェストポーチを開けられそのまま小銭入れをスリに盗まれ泣いていた尾崎です。 結婚して苗字が変わりましたが、幸田敦、と言えば知っている方もいるかもしれません。 ご無沙汰の皆さん、お元気ですか?
さて 今日はapacheとmod_perlの組み合わせでおもろいことをやってみようと思いました。
apacheはもう説明の必要がないくらい、httpサーバのデファクトスタンダードとして あちこちに使用されているソフトウェアです。 mod_perlはperlのインタプリタをapacheに組み込み、apacheのモジュールを perlで作れるようにしたすばらしいモジュールです。
通常は、mod_perlはCGIの高速化ツールとして使用されることが多いです。 例えば、
MpTest.pm
package MpTest;
use strict;
use warnings;
use Apache2::RequestRec();
use Apache2::RequestIO();
use Apache2::Const -compile => qw(OK);
sub handler{
my $r = shift;
$r->content_type('text/html');
$r->print('<H1>Mod_Perl2 Test!!!</H1>');
$r->print('Hello, OZAKING');
return Apache2::Const::OK;
}
1;
というperlスクリプト(モジュール)を作成して、 httpd.confに
SetHandler modperl
PerlResponseHandler MpTest
と設定してあげると、超速のwebアプリケーションが作成可能です。 これはもうかなりあちこちで採用されている手法で、 mixiなどもこの手の手法(といってももっともっと複雑かつ高度でしょうが)を 使用してperlのCGIを高速化しています。
ここで、httpd.confに PerlResponseHandler という設定項目があるのが分かります。 これがhttpdに組み込まれたperlのインタプリタを呼び出す設定で、 Responseを返すときにperlのMpTestというクラス(厳密に言うと違うけど)を呼び出す、 という意味になります。
ここで、httpdの動作の内容を軽くおさらいしておきます。
さっきささっと書いた図なので、決して綺麗じゃないですが httpdのライフサイクルの図です。 wait状態からリクエストを受けると順に処理を行い、一周してまたwaitに戻る、 というものを図にしたものです。 mod_perlはこのあらゆるフェーズに介入し、httpdの動作を変えたり、機能を 追加することが可能です。 また、httpdの起動時に全てメモリに読み込まれれ、動作準備の整った状態 となるため極めて高速です。 (さっき軽くab、jmeterで計測してみましたが、 同じコンテンツを返すstaticcontentsと変わらない、もしくは 条件によってはstatic contentsよりも速いレスポンスを得られました。 そのかわり、メモリはぎょうさん必要になります) 先ほどの例は、Responseフェーズにperlのインタプリタが介入することにより クライアントにコンテンツを返していたんです。
さて、あらゆるフェーズに介入できるということは、connectionの段階でも mod_perlで介入することが可能ということです。 ということは。 httpプロトコル以外のプロトコルも実装によっては話すことが可能、ということです。
長い前フリでしたが、今回はここに目をつけ、 へんてこなサーバを一つapache+mod_perlで作成してみようと思います。
CpanモジュールにはAcme::*という空間があり、へんてこなモジュールが大量に 登録されています。 ここで今回僕が目をつけたのが Acme::Playmate。 年月を与えるとplayboy.comにアクセスして 当該年月の"Playmate of the Month"のモデルのデータを 取得するモジュールです。
そこで 年月を与えたら、当該年月のplaymate of the monthのデータを TCP経由で取得するサーバを構築してみましょう。
まず、httpd.confです。
<VirtualHost 127.0.0.1:80>
PerlModule MpTest2
PerlProcessConnectionHandler MpTest2
</VirtualHost>
PerlProcessConnectionHandlerでProcessConnectionフェーズで よびだすPerlのクラスをMpTest2と指定します。
そして、MpTest2.pm
package MpTest2;
use strict;
use warnings;
use APR::Bucket();
use APR::Brigade();
use APR::Status();
use Apache2::Connection();
use Apache2::Filter();
use APR::Const -compile=>qw(SUCCESS);
use Apache2::Const -compile=>qw(OK MODE_GETLINE);
use Acme::Playmate;
sub handler{
my $c = shift;
my $input = APR::Brigade->new($c->pool, $c->bucket_alloc);
my $output = APR::Brigade->new($c->pool,$c->bucket_alloc);
my $last =0;
while (1){
my $rc = $c->input_filters->get_brigade(
$input, Apache2::Const::MODE_GETLINE);
last if APR::Status::is_EOF($rc);
while (!$input->is_empty){
my $b = $input->first;
$b->remove;
if ($b->is_eos) {
$output->insert_tail($b);
last;
}
if ($b->read(my $data)) {
if ($data=~ /^[\r\n]+$/){
$last++;
}else{
my @ym=split " ",$data;
my $bunny = Acme::Playmate->new($ym[0],$ym[1]);
my $out = "Details for playmate " . $bunny->name() . "\n";
$out .= "Bust: " . $bunny->bust() . "\n";
$out .= "Waist: " . $bunny->waist() . "\n";
$out .= "Hips: " . $bunny->hips() . "\n";
$out .= "Height: " . $bunny->height() . "\n";
$out .= "Weight: " . $bunny->weight() . "\n";
$out .= $bunny->link(),"\n";
$b = APR::Bucket->new($output->bucket_alloc, $out);
}
}
$output->insert_tail($b);
}
my $fb = APR::Bucket::flush_create($c->bucket_alloc);
$output->insert_tail($fb);
$c->output_filters->pass_brigade($output);
last if $last;
}
$output->destroy;
$input->destroy;
Apache2::Const::OK;
}
1;
これがサーバの本体部分のスクリプトです。 handlerというメソッドでhttpdから処理を引き取り、 $cにProcessConnectionにアクセスするためのインターフェイスを設定します。 $input、$outputは、クライアントからの入出力を取り持つクラスですが、 Bucket brigadeというメカニズムを使用しています。 Bucket brigadeとは、Apache 2でstream IOを抽象化したモデルのことです。 まず、ストリームデータを流れ放題にするのではなく、 一定の塊ごとに分割します。この塊のことを"Bucket"といい、 そのBucketの集まりのことをBrigadeといいます。 この機構により、apacheはいつでもどこでも、何度でもストリームのデータを 読み込む事が可能になっています。
無限ループの中で、 クライアントからの入力を受け取り、 その入力からplaymateのデータを取得し、 クライアントに返す処理が実装されてます。
input_filters->get_brigade($input, Apache2::Const::MODE_GETLINE);
my $rc = $c->input_filters->get_brigade(
$input, Apache2::Const::MODE_GETLINE);
last if APR::Status::is_EOF($rc);
while (!$input->is_empty){
my $b = $input->first;
$b->remove;
last;
}
実際にクライアントの入力を読み込む場所です。 get_brigadeで$inputにbrigadeを読み込み brigadeが空になるまで(!$input->is_empty) brigadeの先頭bucketを取得して、消去することを繰り返します。
my @ym=split " ",$data;
my $bunny = Acme::Playmate->new($ym[0],$ym[1]);
my $out = "Details for playmate " . $bunny->name() . "\n";
$out .= "Bust: " . $bunny->bust() . "\n";
$out .= "Waist: " . $bunny->waist() . "\n";
$out .= "Hips: " . $bunny->hips() . "\n";
$out .= "Height: " . $bunny->height() . "\n";
$out .= "Weight: " . $bunny->weight() . "\n";
$out .= $bunny->link(),"\n";
$b = APR::Bucket->new($output->bucket_alloc, $out);
で、処理を行い、bucketを作成し、
$output->insert_tail($b);
でbrigadeのお尻にbucketを追加します。
my $fb = APR::Bucket::flush_create($c->bucket_alloc);
$output->insert_tail($fb);
ここで出力をflushするbucketを作成してbrigadeの尻尾に追加し、
$c->output_filters->pass_brigade($output);
ここで集めたbucketの集まりのbrigadeを出力します。
さて、動作確認です。
# telnet 127.0.0.1 80
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
2006 12
Details for playmate Kia Drayton
Bust: 34" C
Waist: 24
Hips: 34
Height: 5' 9"
Weight: 118 lbs
http://www.playboy.com/girls/playmates/directory/200612.html
Connection closed by foreign host.
めでたく取得できていますね。
プログラムを書くのは本来とっても苦手なため、決して綺麗なプログラムではないし、 エラー処理もまったくされていないため、 実用性はほとんどないですが、こんなこともできますよ、という紹介です。
今回はおふざけなtcpのサーバを一つ作りましたが、 この機構を使用してapacheでsmtpサーバを動作させることもできますね。 apache::qbsmtpdというツールがあります。 apacheとmod_perlの機能を利用したsmtpサーバで メールを受けて処理を行う、ということが簡単に行うことができます。
それではごきげんよう。
プログラムも含めて今回のblog作成時間3時間半。長すぎやろwww