October 29, 2003

Apache::XMLRPC::Lite を使った mod_perl ハンドラによる XML-RPC サーバ

[ Perl ]

以前に 'Weblogs.Com changes.xml を吐いたりする Ping サーバの Perl 実装' で Perl による XML-RPC サーバの実装について記述しました。CPANSOAP::Lite ディストリビューションに含まれる XMLRPC::Transport::HTTP の CGI 実装 (XMLRPC::Transport::HTTP::CGI) を使ったものでした。

手軽に XML-RPC サーバを作成するという意味も含めて CGI 実装には色々と利点がありますが、その一方でやはり、CGI プログラムの起動や Perl 起動時のオーバーヘッドによるパフォーマンス劣化が気になります。そこで、XMLRPC::Transport::HTTP のもう一つのサブクラスである XMLRPC::Transport::Daemon でデーモンとして実装するという手があります。あるいは mod_perl を使って、CGI 実装したものを Apache::Registry で高速化するという手も考えられます。

更にもう一手として、XML-RPC サーバを mod_perl ハンドラとして実装するという手段があります。mod_perl ハンドラであれば、スタックハンドラなどのテクニックを使って、XML-RPC の応答前後に任意の処理を挟んだりということが容易に可能なので、より柔軟なカスタマイズができると思います。XML-RPC サーバを mod_perl で、そんな要求に応えてくれるのが CPAN の Apache::XMLRPC::Lite です。Apache::XMLRPC::Lite も SOAP::Lite ディストリビューションに含まれています。

Apache::XMLRPC::Lite 自体の実装はとても単純で、

package Apache::XMLRPC::Lite;
 
use strict;
use vars qw(@ISA $VERSION);
use XMLRPC::Transport::HTTP;
 
@ISA = qw(XMLRPC::Transport::HTTP::Apache);
$VERSION = sprintf("%d.%s", map {s/_//g; $_} 
             q$Name: release-0_55-public $ =~ /-(\d+)_([\d_]+)/);
 
my $server = __PACKAGE__->new;
 
sub handler {
  $server->configure(@_);
  $server->SUPER::handler(@_);
}
 
1;

と数行のスクリプトになっています。XMLRPC::Transport::HTTP::Apache を継承し、ハンドラルーチンが定義されています。では、そのスーパークラスに当たる XMLRPC::Transport::HTTP::Apache はどんな実装かといいますと、このクラスは XMLRPC/Transport/HTTP.pm の中に記述されており、

package XMLRPC::Transport::HTTP::Apache;
 
@XMLRPC::Transport::HTTP::Apache::ISA = qw(SOAP::Transport::HTTP::Apache);
 
sub initialize; *initialize = \&XMLRPC::Server::initialize;
sub make_fault; *make_fault = \&XMLRPC::Transport::HTTP::CGI::make_fault;
sub make_response; *make_response = \&XMLRPC::Transport::HTTP::CGI::make_response;

といったこれまた数行です。SOAP::Transport::HTTP::Apache を継承して、幾つかのメソッドを XMLRPC::* のメソッドで置き換えています。スーパークラスにあたる SOAP::Transport::HTTP::Apache にはハンドラが定義されています。

それでは、Apache::XMLRPC::Lite による Ping サーバの実装を考えてみます。

package Ping::Handler::XMLRPC;
 
use strict;
use warnings;
 
package weblogUpdates;
 
sub ping {
    my $self = shift;
    my ($name, $url) = @_;
 
    return { flerror => XMLRPC::Data->type ('boolean', 0),
             message => 'Thanks for the ping' };
}
 
1;

たったのこれだけだったりします。リクエストを受け取って、特に何もせずに応答だけ返していますが、DB にパラメータを保存する処理などを入れたとしても、Class::DBI などを使えばそれもものの数行です。パッケージの記述を見ると Ping::Handler::XMLPRC では何もせずに weblogUpdates を定義してしまっていますが、そのカラクリの説明はちょっと後に回します。これを

/usr/local/pingserv/app/lib/Ping/Handler/XMLRPC.pm

として保存しておきます。

さて、Apache::XMLRPC::Lite は前述のように mod_perl ハンドラとして実装されているものなので、この Ping サーバを動作させるために mod_perl を組み込んだ Apache のコンフィギュレーションに数行設定を加えます。(本当は dispatch_to は "weblogUpdates::ping" としたかったのですが、そう記述すると ping メソッドがなぜか二回呼ばれてしまい、以下の記述で落ち着いてます。原因不明)

PerlModule Ping::Handler::XMLRPC
PerlModule Apache::XMLRPC::Lite
<Location /XMLRPC>
  SetHandler perl-script
  PerlHandler Apache::XMLRPC::Lite
  PerlSetVar dispatch_to "weblogUpdates"
  PerlSetVar options "compress_threshold => 100000"
  Order allow,deny
  Allow from all
</Location>

これにより、http://localhost/XMLRPC が Ping サーバの URL になります。ping.○○.com とかドメインを取ればそれらしいですね。:)

コンフィギュレーションを書き換えたら Apache 再起動と行きたいところですが、PerlModule の行で Perl のライブラリサーチパス (@INC) に含まれていないパスに置かれた先ほどのモジュールをロードしていますので、

$ export PERL5LIB=/usr/local/pingserv/app/lib

と環境変数を設定してから、再起動をかけます。(環境変数以外でなんとかする方法ってあるんでしょうか。PerlSetEnv ディレクティブだと、Ping::Handler::XMLRPC から利用している自作モジュールを use するところでエラーになってしまったんですが。)

あとは適当にクライアントを記述するなり、ウェブログツールから Ping を飛ばすなりして動作させてみれば良いでしょう。素の CGI プログラムで実装したものに比べると、かなりの速度向上が期待できるはずです。

Apache::XMLRPC::Lite は PerlSetVar dispatch_to で指定した変数に基づいてモジュールをロードしようとします。先ほどの Ping サーバを素直に実装する (パッケージを weblogUpdates だけにする) と、@INC/weblogUpdates.pm をロードしようとします。ここではモジュールを任意のパッケージ階層に置いて、ファイル名も任意の物にしたかったので、先のような実装にしておき、PerlModule で Ping/Handler/XMLRPC.pm のロードと一緒にweblogUpdates パッケージを名前空間にインポートしておくという方法を取りました。(ping メソッドは weblogUpdates.ping とパッケージが予め決められているので、weblogUpdates 以外のパッケージにそれを実装しても Ping は受け取れません...で合ってるかな?)

さて、この Ping サーバ、実は一点問題点があります。XML-RPC リクエストにマルチバイト文字列が混ざっている場合です。具体的には、ウェブログの名前が日本語だったりするケース。リクエストが UTF-8 で送られてくる、あるいは XML 宣言に正しく "<?xml version="1.0" encoding="EUC-JP" ?>" と encoding が記述されている場合はいいのですが、EUC-JP にエンコードされた文字列がまざっているにも関わらず、encoding が省略されていたりすると、XML-RPC リクエストのデシリアライズに失敗して、処理が中断されてしまいます。

その原因は、SOAP::Lite ディストリビューションは XML のパーシングに XML::Parser を用いており、XML::Parser は XML 宣言の encoding に従って処理をするようになっているためです。(これは正しい実装)

encoding が省略されるとそれは UTF-8 あるいは UTF-16 であると解釈されるのが XML の仕様です。なので、EUC-JP などの場合、必ず encoding が指定されていなければなりません。故に、本来には Ping サーバの問題ではなく、Ping クライアントの実装が正しくないのが問題なのですが、例えば Movable Type の Ping クライアント部分の実装 (MT::XMLRPC) を見ると、

my $text = <<XML;
<?xml version="1.0"?>
<methodCall>
    <methodName>$method</methodName>
    <params>
    <param><value>$blog_name</value></param>
    <param><value>$blog_url</value></param>

と encoding が指定されていないし(EUC-JP パッチを当てた場合にはここも書き換わるべきですね。)、Perl の XML-RPC バインディングの実装を見ても、encoding を指定できるものは少ないようです。(可能なものはあるのかな?)

クライアントの実装が原因なんだから放っておくのもアリだとは思いますが、サーバ側の実装で解決できるのであれば、解決しておくのがベターだとも思います。そこで、Apache::XMLRPC::Lite にリクエストが渡る前に本文の文字コードを調べて XML 宣言を正しく書き直す、あるいは UTF-8 に変換するなどの方法が考えられます。Apache::XMLRPC::Lite の処理の前にそれらを割り込ませる方法として、前半で軽く触れたスタックハンドラを使えば可能なんじゃないかと考えました。

mod_perl のスタックハンドラというのは、一回のリクエストに複数のハンドラを起動して処理することを言います。(Apache拡張ガイド〈上〉サーバサイドプログラミング に詳しく記述されていました) 最も簡単なのは、コンフィギュレーションの PerlHandler の記述に

PerlHandler My::EncodingFilter Apache::XMLRPC::Lite

として複数のハンドラを並べるというものです。My::EncodingFilter で XML-RPC リクエストをハンドリングし先のエンコーディングの問題に対処した上で、Apache::XMLRPC::Lite に渡す、というもの。ちょっと試してみたのですが、この辺の扱いには不慣れなものでまだうまくいってません。(POST で送られてきたリクエストを書き換えて Apache インスタンスに戻す、ってできるんだろうか?)

あるいは PerlFixupHandler とかに登録したハンドラでやるのが正しいのかな? もう少し調べてみようと思います。

Posted by naoya at October 29, 2003 03:15 AM | トラックバック (2)  b_entry.gif
トラックバック [2件]
TrackBack URL: http://mt.bloghackers.net/mt/suck-tbspams.cgi/578
日本語化パッチ更新しました
Excerpt: 日本語化パッチを更新しました。(ダウンロードはこちら) NDO::Weblogのnaoyaさんに指摘を受けた箇所を主に修正しました。 1. lib/MT/XMLRPCServer.pm を書き換えるようにした。 EUC版のみ修正です。naoyaさんのパッチをそのまま適用させていただきました。これで、moblo...
Weblog: Milano::Monolog
Tracked: November 23, 2003 01:49 PM
郵便番号変換でXMLRPCサーバのベンチ
Excerpt: XML-RPCを使って、郵便番号から住所引っ張ってくるサービスを作る際に、perlとPHPどちらの方がパフォーマンスがでるのか気になったので、ちょっと実験してみ...
Weblog: hori-uchi.com
Tracked: April 26, 2005 10:49 AM
コメント [2件]

httpd.conf での lib 問題は、mod_perl を EVERYTHING=1 でビルドしておけば Perl コンテナがつかえるので


use lib "/path/to/lib";
use Module;

とかやっちゃうのがはやいです。

そうでなかったら PerlRequire /path/to/startup.pl しておいて startup.pl で use lib して use Module する、かな。

あと MT 日本語パッチの問題はアレゲなので milano 氏に報告した方がいいかもしれませんな。

[1] Posted by: miyagawa at October 29, 2003 03:45 AM [返信]

お、いけました。素晴らしい。
ていうか基本が抜け落ちてるなあ、自分。w

みらのさんに報告しまっさ。

[2] Posted by: naoya at October 29, 2003 10:52 PM [返信]