この記事は Perl5 Advent Calendar 2015 の10日目の記事です。
昨日の記事は @shogo82148 さんの Perl の DateTime 利用上の注意点 でした。いやほんとね、うるう秒がビルド時に入っているのを見た時にはどうしようかと思いました。
さてはじめましょうか。
ペライチPSGIアプリケーションとは
ンなものは誰も今まで言ったことがないと思うので定義してみます
- 1枚の.psgiファイルのみで構成されている
- 狭義の「ペライチPSGIアプリケーション」はPerlコードはもちろん各種リソースすべてを1枚の.psgiに収めている
- 広義の「ペライチPSGIアプリケーション」はPerlコードのみを.psgiに収めている
この記事では何を語るか
- PSGIについての基本のおさらい
- ペライチPSGIアプリケーションの実例の紹介
- ペライチPSGIアプリケーションのメリット・デメリット
- ペライチPSGIアプリケーションを書くための部品とtips
PSGIについての基本のおさらい
Perl5でウェブアプリケーションを書く場合、CGIやFastCGI、mod_perlなどが従来は使われてきていましたが、現在ではPerlで書かれたHTTPを直接しゃべるサーバをnginxなどのリバースプロキシを介して利用することが多いかと思われます。
この「Perlで書かれたHTTPを直接しゃべるサーバ」を作るための規格化されたインターフェイスがPSGIです。そしてPSGIに沿った形でPerlのコードが記述されたファイルは一般的には.psgi
という拡張子を取ります。
超シンプルな.psgiファイルを以下に示します。前後はなくこれだけです。
use strict;
use warnings;
use utf8;
sub {
return ["200", ["Content-Type" => "text/plain"], ["OK"]];
};
説明すると3つの配列のリファレンスを返す関数のリファレンスを最後に返すプログラムを定義するだけです。
3つの配列はHTTPのレスポンスを示していて、1つ目がHTTPのステータスコード(文字列)、2つ目がレスポンスヘッダーに入れるキーと値(ヘッダーは同じキーを複数入れることができるのでハッシュリファレンスではなく配列リファレンスです)、3つ目がレスポンスボディの内容を配列リファレンスで囲ったものです。
これをapp.psgi
で保存し、cpanm Plack
した上で以下のコマンドで立ち上げます。
$ plackup app.psgi
すると5000番ポートでHTTPサーバが立ち上がります。ブラウザやcurlなどでこのサーバを叩くと OK
という文字列が返ってくることがわかると思います。
また、立ち上げたい時にこのプログラムに引数を渡したいと思って素直にやろうとするとハマると思います。そのときは.plで書いてplackup
コマンドを用いずにPlack::Loader
を自分で呼ぶみたいなのもあり、テストもできるのでこっちのほうがいいのではという説が最近ではあります。
PSGI/Plackアプリケーションの起動方法いろいろと本番環境アレコレ
.psgiからの卒業
とまあそんな感じですがここではわかりやすさとホビー感を出すために.psgiで書くことに話を進めていきます。
ペライチPSGIアプリケーションの実例の紹介、というか動機
kuiperbelt というWebSocketを中継してくれるミドルウェアを書いているのですが、このミドルウェアの目的は普通のイベント駆動ではないWebアプリケーションでもWebSocketを扱えるようにする、というものでした。
ある程度動いてコマンドラインなどで確認できたところで、じゃあ本当に動くのかという実証をしようとしたところ、やはりミドルウェア本体の言語であるGoで書くよりはPerl/Ruby/Python/PHPなどの実際に使われるであろう言語で書いたほうがよいだろうということで、簡単なチャットアプリケーションをPSGIで書いてみたという経緯があります。
その時の実際のコードはこちらになります。
このアプリケーションは/
は静的なHTMLを返すだけで他のパスはJSONのAPIやkuiperbeltと通信するためのエンドポイントになっています。HTMLは埋め込んでいないので前述の定義からすると「広義のペライチPSGIアプリケーション」ですが、狭義のほうにすることも可能かと思われます。
ペライチPSGIアプリケーションのメリット・デメリット
書いてみた時に感じたメリット・デメリットを上げてみます。
メリット
- フレームワークに頼らずに書くので自分の好きなモジュールを組み合わせることができる
- プラモデル感があって良い
- ウェブアプリ/フレームワークはどういう部品で構成されているか勉強になる
- フレームワークに頼らずに書くのでフレームワークのお作法を知らなくてもPSGIのお作法だけおさえていれば読むことができる
- ただそこまでPSGIを生で書く人が少ない気はするのでメリットになりうるか?
デメリット
- 自分で書き方を決めなければならない
- レールは敷かれていないので自分で敷くしかない
- フレームワークが用意しているセキュリティ的な機能は自前でやる必要がある
- 例えばAmon2はCSRFトークンを検証する機能がBasicフレーバーで埋め込まれますが、ちゃんとCSRF対策をやる場合は自分で埋め込まないといけない
ペライチPSGIアプリケーションを書くための部品とtips
Plack::Request
上に書いたPSGIに沿った関数リファレンスは実は引数をもらっているのですが、これはCGI時代からのキーが付いたハッシュリファレンスなのでちょっと使いづらいと思うかもしれません。例えばリクエストのパスは以下のようにして取ります。
use strict;
use warnings;
use utf8;
sub {
my $env = shift;
my $path = $env->{PATH_INFO};
return ["200", ["Content-Type" => "text/plain"], ["This path is $path"]];
};
ですが僕は使いにくいなーと思うことが多いので、便利なメソッドがいっぱい生えたPlack::Request
をいつも使っています。
Plack::Request
を使った場合の同様のコードは以下の様な感じです。
use strict;
use warnings;
use utf8;
use Plack::Request;
sub {
my $env = shift;
my $req = Plack::Request->new($env);
my $path = $req->path_info;
return ["200", ["Content-Type" => "text/plain"], ["This path is $path"]];
};
これだけだとあまりメリットを感じませんが、例えばquery string
をハッシュの形式で取れるようになったりします。
use strict;
use warnings;
use utf8;
use Plack::Request;
sub {
my $env = shift;
my $req = Plack::Request->new($env);
my $hoge = $req->parameters->{hoge};
return ["200", ["Content-Type" => "text/plain"], ["hoge parameter is $hoge"]];
};
Router::Boom
Plack::Request
によってパスやquery stringなどが取れるようになりましたが、パスによって動作を変えるRESTfulなアプリケーションを書く場合、if文を列挙してやるのはやっぱりつらいのでパスをうまいことよしなにパースして特定の関数にdispatchするみたいな仕組みを使いたくなります。
まあ以下の様な感じでしょうか。
use strict;
use warnings;
use utf8;
use Plack::Request;
use Router::Boom;
my $router = Router::Boom->new;
$router->add("/", sub {
return ["200", ["Content-Type" => "text/plain"], ["This is root."]];
});
$router->add("/hoge", sub {
return ["200", ["Content-Type" => "text/plain"], ["This is hoge."]];
});
sub {
my $env = shift;
my $req = Plack::Request->new($env);
my ($code) = $router->match($env->{PATH_INFO});
return ["404", ["Content-Type" => "text/plain"], ["Not Found."]] unless $code;
return $code->($req);
};
フレームワークっぽくなってきましたね。
JSONとかテンプレートとか
JSONを吐くだけなら結構簡単です。
use strict;
use warnings;
use utf8;
use Plack::Request;
use Router::Boom;
use JSON qw/encode_json/;
my $router = Router::Boom->new;
$router->add("/", sub {
return ["200", ["Content-Type" => "application/json"], [encode_json({ "this" => "json" })]];
});
$router->add("/hoge", sub {
return ["200", ["Content-Type" => "application/json"], [encode_json({ "this" => "hoge" })]];
});
sub {
my $env = shift;
my $req = Plack::Request->new($env);
my ($code) = $router->match($env->{PATH_INFO});
return ["404", ["Content-Type" => "text/plain"], ["Not Found."]] unless $code;
return $code->($req);
};
Perlでテンプレートエンジンといえば日本ではText::Xslateですが、ここではText::MicroTemplateを使ってみます。
さらに、狭義のペライチPSGIアプリケーションを実現するためにテンプレートを.psgiに埋め込んでみましょう。変数として文字列で埋め込んでみてもいいですが、Data::Section::Simple
を使ってみます。
use 5.16.1;
use warnings;
use utf8;
use Plack::Request;
use Router::Boom;
use Text::MicroTemplate qw/build_mt/;
use Data::Section::Simple qw/get_data_section/;
my $router = Router::Boom->new;
$router->add("/", sub {
my $req = shift;
my $name = $req->parameters->{name};
state $mt = build_mt(get_data_section("index.mt"));
my $res = $mt->($name)->as_string;
return ["200", ["Content-Type" => "text/html"], [$res]];
});
sub {
my $env = shift;
my $req = Plack::Request->new($env);
my ($code) = $router->match($env->{PATH_INFO});
return ["404", ["Content-Type" => "text/plain"], ["Not Found."]] unless $code;
return $code->($req);
};
__DATA__
@@ index.mt
<html>
<meta charset="UTF-8">
<body>hello <?= $_[0] // "world" ?></body>
</html>
これでquery stringのname
に受け取った文字列をHTMLにレンダリングするよくある感じのhello worldが書けましたね。
圧縮とかかけてみる
ペライチということはコードを難読化したり圧縮することがし易いのかなと思ったので遊んでみましょう。
まず手始めにコードの本体を__DATA__
に突っ込んでみます
use 5.16.1;
use warnings;
use utf8;
use Data::Section::Simple qw/get_data_section/;
my $app = get_data_section("app.psgi");
eval $app;
__DATA__
@@ app.psgi
use Plack::Request;
use Router::Boom;
use Text::MicroTemplate qw/build_mt/;
my $router = Router::Boom->new;
$router->add("/", sub {
my $req = shift;
my $name = $req->parameters->{name};
state $mt = build_mt(get_data_section("index.mt"));
my $res = $mt->($name)->as_string;
return ["200", ["Content-Type" => "text/html"], [$res]];
});
sub {
my $env = shift;
my $req = Plack::Request->new($env);
my ($code) = $router->match($env->{PATH_INFO});
return ["404", ["Content-Type" => "text/plain"], ["Not Found."]] unless $code;
return $code->($req);
};
@@ index.mt
<html>
<meta charset="UTF-8">
<body>hello <?= $_[0] // "world" ?></body>
</html>
これで動くんですね。ゆるふわ。
さらにapp.psgiのコードをgzipしてbase64したものを__DATA__
に突っ込んでみます。
use 5.16.1;
use warnings;
use utf8;
use MIME::Base64;
use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
use Data::Section::Simple qw/get_data_section/;
my $app = decode_base64(get_data_section("app.psgi"));
gunzip \$app, \my $uncompressed or die "$GunzipError";
eval $uncompressed;
__DATA__
@@ app.psgi
H4sIAF3naFYAA31RwWrCQBC95yuWxUMCjZHiKcFAW5D2UCuSWwlhTaYamt3V7GxVxH/vZKPQtNC9
7cyb9968sQbYshHlZxyvYG/BYOJZqq20RWjj+FFr2VcyOGIcv9ZlqzOQu0YgsP0hWtu6qQqJUeJ5
8sRGrRtkswFDmCo4EODaDVNRVT6P+B0zds3OHqPnhmFPk2Zbf5CNW1EJCVTtmmG6Ey19icOE6bnr
XHqgwc7PSCIhb5b8DWBRCRSFgRJrrXxeqwqOY4k8CJIfqqbjlximvlMLyKApDLa12vSwFtC2ir3z
+8mEXL/zJ60QFIbZaQeczVLGkfKJtigbnhOgI83zxLuQjjdcEtTX3yX7zYeXcKn5Hb4juSL9Uakr
CFwg1zClwHLrcJTJ8iF7Ll4W87dL8Mv6dDL9zzpdtFbOO19oZHNtVTXmec6sasAY5nQHlK7SZUbm
SYwu4XnfFa1C3FACAAA=
@@ index.mt
<html>
<meta charset="UTF-8">
<body>hello <?= $_[0] // "world" ?></body>
</html>
だいぶ意味がわからなくなってきましたね。ウケる。これでplackupしてhtmlをちゃんと返すのが面白いです。
ちなみに圧縮の効果ですが、
$ ls -lh app*.psgi
-rw-r--r-- 1 mackee staff 890B 12 10 11:48 app.psgi
-rw-r--r-- 1 mackee staff 782B 12 10 11:55 app_uncompressed.psgi
うん、圧縮前のほうが小さいですね。他にもppencodeで予約語だけのプログラムに変換してやるとかなり難読化できる気がするのですがまあ面白いだけですね。
追記:
さらに過激な例があったので記載させていただきます。
初めてのPlack
なるほど
まとめ
これらのコードはオモチャみたいなものなのでお仕事に使う事はできないとは思いますが、仕組みを知るだとか簡単なWebサーバを組むのとかには使えるのかなあと思いました。
そんな感じで、明日は @zoncoen さんです! 楽しみ!