ペライチPSGIアプリケーションの概念と実証

  • 15
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

この記事は 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で書いてみたという経緯があります。

その時の実際のコードはこちらになります。

https://github.com/mackee/kuiperbelt/blob/master/_example/app.psgi

このアプリケーションは/は静的なHTMLを返すだけで他のパスはJSONのAPIやkuiperbeltと通信するためのエンドポイントになっています。HTMLは埋め込んでいないので前述の定義からすると「広義のペライチPSGIアプリケーション」ですが、狭義のほうにすることも可能かと思われます。

ペライチPSGIアプリケーションのメリット・デメリット

書いてみた時に感じたメリット・デメリットを上げてみます。

メリット

  • フレームワークに頼らずに書くので自分の好きなモジュールを組み合わせることができる
    • プラモデル感があって良い
    • ウェブアプリ/フレームワークはどういう部品で構成されているか勉強になる
  • フレームワークに頼らずに書くのでフレームワークのお作法を知らなくてもPSGIのお作法だけおさえていれば読むことができる
    • ただそこまでPSGIを生で書く人が少ない気はするのでメリットになりうるか?

デメリット

  • 自分で書き方を決めなければならない
    • レールは敷かれていないので自分で敷くしかない
  • フレームワークが用意しているセキュリティ的な機能は自前でやる必要がある
    • 例えばAmon2はCSRFトークンを検証する機能がBasicフレーバーで埋め込まれますが、ちゃんとCSRF対策をやる場合は自分で埋め込まないといけない

ペライチPSGIアプリケーションを書くための部品とtips

Plack::Request

https://metacpan.org/pod/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

https://metacpan.org/pod/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を吐くだけなら結構簡単です。

https://metacpan.org/pod/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を使ってみます。

https://metacpan.org/pod/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 さんです! 楽しみ!

この投稿は Perl 5 Advent Calendar 201510日目の記事です。