SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Adobe Developer Connection(AD)

Flex + LL連携(Ruby、Python、PHP)

  • このエントリーをはてなブックマークに追加

 Flexが登場してから、Webの様相は大きく変わりました。本稿では、初めてFlexを触ってみる人や、軽量スクリプト言語の経験があるWeb系エンジニアにもわかりやすいように、Flexとサーバサイドの通信方法について説明したいと思います。

  • このエントリーをはてなブックマークに追加

はじめに

必要なソフトウェアとファイル

サンプルコード

 Sample_code.zip(zip形式、26KB)

この記事で使用する言語

  • Ruby 1.8.4以降
  • Python 2.3以降
  • PHP 5.2以降

事前知識

 Flex又はFlashの基礎的な知識。上記の(サーバサイドの)言語の何れかの基礎的な知識。

サーバサイド?

 Flexが登場してから、Webの様相は大きく変わりました。FlexはFlashの表現力を持ちながら、データベースに代表されるサーバサイドのリソースとの融和性に優れ、まだこの世に登場してから数年と経っていませんが、Flexを使った画期的なWebサイトやサービスが登場してきています。

 現在、既にFlexを絡めた本格的なWebサイトの構築の経験がある方もかなり増えてきたと思いますが、サーバサイドと連携しないコンテンツのみを開発している方もまた少なくないことでしょう。「サーバサイドって難しそう/面倒そう...」と尻込みされている方もいるかもしれません。

 本稿では、初めてFlexを触ってみる人や、軽量スクリプト言語の経験があるWeb系エンジニアにもわかりやすいように、Flexとサーバサイドの通信方法について説明したいと思います。

少しだけ歴史を振り返る

 従来、Flex・Flashのサーバサイドは、ColdFusionやJavaで構築するのが一般的でした。Flashがサーバサイドと本格的に連携出来るようになったのは、2002年にまで遡ります。Macromedia(当時)から発売された製品、「Macromedia Flash Remoting MX」が連携を可能にする鍵となりました。この製品により、サーバサイドのサービスとフロント側のFlashを容易に接続し、データを自在にやり取り出来るようになったのです。サーバ側の技術としてはColdFusion、Javaや.NETが利用できました。

 時は流れ現在では、PHPやRubyに代表されるいわゆるLL(軽量スクリプト言語)でもサーバ側サービスを構築することが可能になっています。もちろん、今まで通りColdFusionやJavaでも構築できますが、これらの環境は、一般的なホスティングサービスでは利用できないか、利用できても環境構築が大がかりになる場合が多いと思われます。

 大抵のホスティングサービスでは、スクリプト言語がデフォルト利用できるようになっている(PerlやPHP、時々RubyやPython)ため、これらの言語でサーバサイドのサービスの構築が出来れば手軽です。小規模な案件では、軽量スクリプト言語のフットワークの良さやスピード感の方が好ましい場面もあるかも知れません。

AMF仕様のオープン化

 サーバ側とFlex・Flashがやりとりする際に使われるフォーマットは「AMF(Action Message Format)」と呼ばれます。これはAdobeのプロプライエタリなフォーマットで、XMLやJSONに見られるようなテキスト形式ではなく、バイナリ形式なのが特徴です。仕様は非公開でしたが、2008年始めに仕様が正式に公開されました。既存のオープンソースのAMF実装は、仕様が明らかでない頃から、フォーマットをリバースエンジニアリングにより解析・実装してきたものが多くあります。仕様の公開に伴い、より正確・厳密にAMFフォーマットを扱えるようになり、信頼性が増すことが期待できます。

 このことと直接は関係しないものの、最近のFlex・Flashの広がりに呼応して、オープンソースのAMFの実装が増えました。一つの言語に複数のAMF実装があることも少なくありません。いろいろな(得意な)言語で、AMFを話すサーバサイドのサービスが簡単に実装できる環境が整ってきています。

サーバと通信してみる

 Flex・Flashがサーバ側と通信する方法は、幾つかあります。

 一番原始的な方法は、Webサーバに対し、GET/POSTメソッドを用いて、データを送受信することです。これは、普通のHTMLのフォームで「送信」ボタンを押したときに行われるのと同じ処理を、Flex・Flashでも行うやり方です。flash.netパッケージのAPIであるURLLoaderクラス、URLRequestクラスを使います。簡単なサンプルを見てみましょう。

 Flexで、ホームページで利用するための単純なゲストブックを作るとしましょう。入力出来るのは、名前とEメールアドレス、感想の3項目です。まずはFlex側のUIを作ります。項目も少ないし、簡単ですね。「送信」ボタンが押されたら、入力されたデータをサーバに送信するようにしてみましょう。その際、入力された名前を「name」、Eメールアドレスは「email」、感想は「comment」として送信します。

 URLRequestが、要求を表現するクラスです。要求を送信する宛先URLや渡すパラメータを設定します。URLLoaderが実際の要求を実行するクラスです。要求が成功した時のハンドラ、失敗した時のハンドラなどを登録しましょう。URLLoaderは、デフォルトではGETメソッドを使って要求を送信しますが、今回はPOSTメソッドを明示的に指定します。

画面のイメージ
画面のイメージ

Guestbook.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" viewSourceURL="srcview/index.html">
         <mx:Script><![CDATA[
                   private function buttonClickHandler(e:Event):void
                   {
                            // サーバ側の処理スクリプトのURL
                            const kDestUrl:String = "http://localhost/guestbook/guestbook.php";
 
                            var request:URLRequest = new URLRequest(kDestUrl);
                            request.method = URLRequestMethod.POST;
                            var vars:URLVariables = new URLVariables();
                            vars.name = tiName.text;
                            vars.email = tiEmail.text;
                            vars.comment = tiComment.text;
                            request.data = vars;
                            lblServerResult.text = "";
                            var loader:URLLoader = new URLLoader();
                            try {
                                     loader.load(request);
                            }
                            catch (error:SecurityError){
                                     trace("リクエスト時にセキュリティエラーが発生しました.");
                            }
                            loader.addEventListener(IOErrorEvent.IO_ERROR, onIOError, false, 0, true);
                            loader.addEventListener(Event.COMPLETE, onLoadComplete, false, 0, true);
                   }
 
                   private function onIOError(event:IOErrorEvent):void
                   {
                            trace("IOエラー");
                   }
 
                   private function onSecurityError(event:SecurityErrorEvent):void
                   {
                            trace("セキュリティエラー");
                   }
 
                   private function onLoadComplete(e:Event):void
                   {
                            var result:String = URLLoader(e.target).data as String; 
                            if (result)
                            {
                                     lblServerResult.text = result;
                                     trace("サーバからのデータのロードに成功しました。受信データ: " + result);
                            }
                   }
         ]]></mx:Script>
 
         <mx:Form width="100%">
                   <mx:FormItem label="お名前">
                            <mx:TextInput id="tiName" />
                   </mx:FormItem>
                   
                   <mx:FormItem label="Eメールアドレス">
                            <mx:TextInput id="tiEmail" />
                   </mx:FormItem>
         
                   <mx:FormItem label="ひとこと">
                            <mx:TextArea id="tiComment"/>
                   </mx:FormItem>
         </mx:Form>
 
         <mx:HBox width="100%">
                   <mx:Button label="送信" left="0" click="buttonClickHandler(event)" />
 
                   <mx:Label text="サーバからの応答:"/> <mx:Label id="lblServerResult" />
         </mx:HBox>
</mx:Application>

 サーバサイドのスクリプトは以下のようになります。PHPで記述しました。サーバサイドでは、送られてきたパラメータを受け取った後、必要な処理を行います。標準出力に書き出した内容が、結果としてクライアントサイドに渡されます。

<?php
if (isset($_POST["name"]) && isset($_POST["email"]) && isset($_POST["comment"]))
{
  $name = $_POST["name"];
  $email = $_POST["email"];
  $comment = $_POST["comment"];
  print "name=$name/email=$email/comment=$comment";
}
else 
{
  print "error";
}

もっと良い方法

 HTTPのGET/POSTメソッドでデータをやり取りする例を見てきました。簡単な処理ですが、それなりの手間は掛かりますね。

 実はFlex・Flashでは、サーバサイドと通信するのに、もっと良い方法があります。それは、サーバサイドで"RPCサービス"を作成して、Flex・Flashから、そのサービスのメソッドを呼ぶやり方です。

 「RPC」とは”Remote Procedure Call”の略で、名前の通り、遠隔にあるプロシージャ(=メソッド)を呼び出しを可能にする技術のことを指します。要は、サーバサイドでサービスを書いて登録すると、クライアントは、ローカルのメソッドを呼ぶのと同じように、サーバサイドのサービスのメソッドを呼ぶことができます。

 RPCで何が嬉しいかというと、まず直感的に使えることがあります。ローカルのメソッドを呼ぶような感覚で、サーバサイドのメソッドが呼べるのは便利です。しかし、更に重要なのは効率です。最初に紹介したGET/POSTを使う方法や、XML/JSONといったテキスト表現でデータを送受信する方法では、サーバ・クライアント間で流れるデータは全てが文字列になってしまいます。これは効率が良くありません。例えば、int型を受け渡すことを考えてみましょう。クライアントサイドからint型の値を送る際、一旦文字列型に変換されます。

int -> 文字列 -> 文字列 -> 文字列 ->int
(クライアント)(サーバ)(クライアント)

 という流れになりますが、文字列型とint型との相互変換が発生するのが無駄です。Flex・FlashのRPC呼び出しで使われるAMFフォーマットは、バイナリ形式のため冗長性が低く、そもそも効率的なデータのやりとりが可能ということもありますが、それだけではなく、Flash Playerがネイティブに扱えるフォーマットというのが重要なポイントです。

 前述の例で言えば、RPC呼び出し時には

int -> int ->int
(クライアント)(サーバ)(クライアント)

 となり、余計な中間オブジェクトが生成されません。クライアントサイドではint型はActionScriptの型からAMFに直接シリアライズされますし、サーバサイドでもAMFからサーバサイドの言語の対応する型にデシリアライズされます。上記の例では軽微な差で、重要性があまり感じられないかもしれませんが、非常に頻繁にデータをやりとりするシステムではこの差が大きく影響してきます。例えば、株価情報を毎秒チェックし続けるクライアントを考えてみてください。

 他にも、高解像度画像など非常に大きなデータを扱うシステムでも、この差が全体のパフォーマンスに大きな影響を与えることでしょう。もしサーバから送られてきた画像がBase64エンコードの文字列なら、文字列を画像に変換するのに多くのメモリを使用しなければなりません。サーバサイドで画像を適切な型(ByteArray)にエンコードしさえすれば、変換の際に必要になるメモリはそもそも不要です。

クライアントサイドの実装

 作成するのは、「HelloWorld」です。クライアントから、"Hello"を送信すると、サーバから"World!"を返すというだけの、ごく単純なサンプルです。

 一口にRPC呼び出しと言っても幾つか実装の選択肢があり、例えばFlexでは、「RemoteObject」というクラスを使うこともできますが、今回の記事では省略します。RPC呼び出しの際に最も基礎となる、「NetConnection」クラス(flash.net.NetConnection)を使って、RPC呼び出しを実行してみましょう。RemoteObjectと違い、NetConnectionは、Flex・Flashの両方にあるクラスですので、このクラスの使い方を把握すれば、Flashでもほぼ同様の手順でRPC呼び出しを実行できます。

画面のイメージ
画面のイメージ

処理の説明

 一番上のラジオボタンで、サーバサイドのサービスのうち、どの言語で実装されたサービスを呼ぶかを選択します。「Send a request」ボタンを押すと、左側の入力エリアに入力された文字列を引数として、サーバの「hello」メソッドを送信します。

 サーバサイドでエラーが発生した場合は下のテキストエリアに、正常に処理された場合は呼び出した結果が上のテキストエリアに表示されます。

NetConnectionを使う

 サーバサイドのサービスを呼び出すにはNetConnectionクラスを使用します。まず、「gatewayアドレス」を第一引数に、connectメソッドを呼び出し、接続します。

connect(command:String, ... arguments):void

 「gatewayアドレス」は、サーバサイドのAMF実装に依存する、固定のアドレスです。接続に成功したら、callメソッドを使って実際のサービスを呼び出します。

call(command:String, responder:Responder, ... arguments):void

 第一引数には、サービス名を指定します。第二引数には、Responderインスタンスを指定します。Responderは、呼び出しが成功したときのハンドラと、呼び出しが失敗したときのハンドラを指定して生成します。第三引数以降には、引数を指定します。引数が必要ない場合には省略します。

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="100%">
         <mx:Script><![CDATA[
                   private static const PERL:String   = "Perl";
                   private static const PHP:String    = "PHP";
                   private static const PYTHON:String = "Python";
                   private static const NET:String    = "NET";
                   private static const RUBY:String   = "Ruby";
         
                   private function sayHello():void
                   {
                            var client:IServiceClient;
                            
                            switch (lang.selectedValue)
                            {
                                     case RUBY:
                                     client = new RubyClient(this);
                                     break;
                                     case PHP:
                                     client = new PHPClient(this);
                                     break;
                                     case PYTHON:
                                     client = new PythonClient(this);
                                     break;
                                     case PERL:
                                     client = new PerlClient(this);
                                     break;
                                     case NET:
                                     client = new NETClient(this);
                                     break;
                                     default:
                                     throw new Error("Invalid selection.");
                            }
 
                            if (client != null)
                            {
                                     const useAMF0:Boolean = lang.selectedValue == PERL;
                                     client.callRemote(useAMF0);
                            }
                   }
         ]]></mx:Script>
 
         <mx:Form width="100%" height="100%">
                   <mx:FormItem label="Remote service type:">
                            <mx:HBox>
                                     <mx:RadioButtonGroup id="lang"/>
                                     <mx:RadioButton groupName="lang" label="{RUBY}" selected="true"/>
                                     <mx:RadioButton groupName="lang" label="{PHP}"/>                                    
                                     <mx:RadioButton groupName="lang" label="{PYTHON}"/>
                                     <mx:RadioButton groupName="lang" label="{PERL}"/>
                            </mx:HBox>
                   </mx:FormItem>
 
                   <mx:FormItem label="Send to server">
                            <mx:HBox horizontalAlign="left" width="100%">
                                     <mx:TextInput id="tiMessage" text="Hello" />
                                     <mx:Spacer width="100%" />
                                     <mx:Button label="Send a request" click="sayHello()" />
                            </mx:HBox>
                   </mx:FormItem>
 
                   <mx:FormItem label="Result from server" width="100%" height="100%">
                            <mx:TextArea id="taResult" width="100%" height="100%"/>                    
                   </mx:FormItem>
 
                  <mx:FormItem label="Error from server" width="100%" height="100%">
                            <mx:TextArea id="taFault" width="100%" height="100%"/>
                   </mx:FormItem>
         </mx:Form>
 
</mx:Application>

 サーバサイドの処理結果が成功か失敗かに応じて、Responderの生成時に指定した対応するハンドラに渡されます。

 実際のコード例は以下のようになります。

var responder:Responder = new Responder(onSuccess, onFault);
try{
gateway.connect(gatewayUrl);                            
}
catch(e:ArgumentError){
        trace("invalid gateway address...");
}

PHPを使う

 PHPには幾つかのAMF実装があります。

 今回はamfphpを取り上げます。AMFPHPは、非常に古くからあるPHPによるオープンソースのAMF実装です。現時点での最新版は、2008年1月20日にリリースされた「1.9 beta2」です。安定版の「1.2.4」は、AMF0は扱えるものの、Flash Player 9から登場したAMF3フォーマットを扱えません。AMF3フォーマットをサポートした版が、1.9 betaとして開発が続けられています。

 AMFPHPの特徴は、とても使いやすいことです。インストールは簡単ですし、「サービスブラウザ」という、サーバ側のサービスの一覧を閲覧するためのGUIが用意されており、より直観的に使うことができます。

AMFPHPのインストール:

 「amfphp-1.9.beta.20080120.zip」を、http://sourceforge.net/project/showfiles.php?group_id=72483#filesからダウンロードします。zipファイルを展開し、Webサーバのルートに移動させます。

※注

 展開先はルートでなくても構いませんが、説明を簡略化するためにルートに展開することとします。

 動作確認のため、http://localhost/amfphp/gateway.phpにアクセスしてみましょう。正常に動作していれば、以下のようなメッセージが表示されます。

AMFPHPでのサービスの実装:

 サービス用のクラスを作成し、メソッドを記述します。publicなメソッドは、自動的にサービスとして登録されます。privateなメソッドは、クライアント側から呼ぶことはできません。publicなメソッドでも、メソッド名が「_」で始まるものはprivateなメソッドと見なされ、同様に呼ぶことができないので注意して下さい。

HelloWorldService.php
HelloWorldService.php
<?php
class HelloWorldService
{
    public function hello($message)
    {
        return "World! [message from client: " . strtoupper($message) . "]";
    }
}
?>

 引数は1つで、ここには呼び出し側から任意の文字列を渡します。これを、サービス側で大文字に変換してから、呼び出し側に戻します。

AMFPHPでのサービスのデプロイ:

 サービスは、/amfphp/servicesディレクトリに配置します。Unixサーバにアップする場合は、ファイルの許可権を適切に設定して下さい。ディレクトリ構造は以下のようになります。

 AMFPHPには「サービスブラウザ」があり、servicesディレクトリ以下に配置したサービスの詳細をグラフィカルに確認することができます。先ほどの/amfphp/gateway.phpにアクセスし、「Load the service browser」をクリックして、「サービスブラウザ」を起動してみましょう。今配置した「HelloWorldService」のメソッド名や引数などが表示されるはずです。

 実際のコード例は以下のようになります。

AMFPHPのサービスの呼び出し:

 「クラス名.サービス名」をサービス名として指定し、呼び出します。今回の例では、「HelloWorldService.hello」です。

Pythonを使う

 Pythonでは、オープンソースのAMF実装である「PyAMF」を使うことにより、サーバサイドとFlex・Flashの連携が可能になります。PyAMFの現時点での最新版は、2008年5月4日にリリースされた0.3.1です。PyAMFは、PylonsやTurboGears等の代表的なWebアプリケーションフレームワークと連携させることも出来ますが、今回は単独で起動するサービスを実装してみます。

PyAMFのインストール:

 まずOS上にPythonをインストールした後、PyAMFをインストールします。http://download.pyamf.org/releases/から、PyAMF-0.3.1.zipを取得し、任意のディレクトリに展開した後、PyAMF-0.3.1ディレクトリに移動して以下のコマンドを実行します。

C:\PyAMF-0.3.1>python setup.py install

PyAMFでのサービスの実装:

 サービスの実装は、普通に処理を記述するだけです。注意する点として、PyAMFではサービス名と、それを実装したメソッドとを関連付ける「services」プロパティを定義する必要があります。今回、サービス名は「hello」、実装したメソッドは「hello_service」ですので、

services = {
    'hello': hello_service,
}

 と定義します。

 完全なソースコードは以下のようになります。

"""
Helloworld example server.
"""
 
import string
 
def hello(message):
    """
    A simple helloworld client
    """
    return "World! [message from client: " + string.upper(message) + "]"
 
services = {
    'hello': hello_service,
}
 
if __name__ == '__main__':
    from pyamf.remoting.gateway.wsgi import WSGIGateway
    from wsgiref import simple_server
 
    gw = WSGIGateway(services)
 
    httpd = simple_server.WSGIServer(
        ('localhost', 3001),
        simple_server.WSGIRequestHandler,
    )
 
    httpd.set_app(gw)
 
    print "Running Helloworld AMF gateway on http://localhost:3001"
 
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass

PyAMFでのサービスの起動:

 コマンドプロンプトを開き、

C:\PyAMF-0.3.1> HelloWorldService.py

 と入力して、サービスを起動します。実行時のイメージは以下のようになります。

Rubyを使う

 Rubyには、幾つかのオープンソースのAMF実装があります。

  • MidnightCoders WebORB for Ruby
  • rubyamf

 今回はrubyamfを取り上げます。現時点でのRubyAMFの最新版は、1.6.3です。

 RubyAMFの特徴は、WebアプリケーションフレームワークであるRuby on Railsとの連携が容易なことです。

RubyAMFのインストール:

 Ruby on Railsをインストールし、railsコマンドでプロジェクトを作成します。

C:\devel\rails>gem install rails
C:\devel\rails>rails helloservice
C:\devel\rails>cd helloservice

 プロジェクトディレクトリに移動し、以下のコマンドを実行すると、RubyAMFがインストールされます。

C:\devel\rails>ruby script/plugin install http://rubyamf.googlecode.com/svn/tags/current/rubyamf

 無事にインストール出来たかを確認するために、Webブラウザでgatewayアドレスにアクセスしてみましょう。次のコマンドでWebサーバを起動します。

C:\devel\rails>ruby script/server

 RubyAMFのゲートウェイアドレスは、

http://localhost:3000/rubyamf/gateway

 です。正常にインストールされていれば、RubyAMFのロゴ画像が表示されます。

RubyAMFでのサービスの実装:

 「プロジェクトディレクトリ/app/controllers」以下に、「hello_world_controller.rb」を作成します。クラス名、ファイル名は、Railsの命名規則に従います。

hello_world_controller.rb
class HelloWorldController < ApplicationController
  def hello
    render :amf => "Ruby World! [message from client: " + params[0].to_s.upcase + "]"
  end
end

 クライアントから渡された引数には、params配列でアクセスできます。

 RubyAMFの細かい挙動は、「プロジェクトディレクトリ/config/rubyamf_config.rb」で設定できます。一度目を通しておくことをお勧めします。

RubyAMFのサービスの呼び出し:

 「クラス名.サービス名」をサービス名として指定し、呼び出します。今回の例では、「HelloWorldController.hello」です。

デバッグのためのツール

 クライアント~サーバ間通信は、問題が発生した際にデバッグが難しい個所です。例えば、サーバから受け取ったデータ型を、クライアント側の対応する型に正しくキャスト出来ないケースを考えましょう。

  • サーバから返されたデータが期待した内容で届いているのか
  • サーバから返されたデータが指定した型になっているのか
  • そもそもデータがサーバから届いているのか

 このような時に便利なのが、実際に通信されているAMFデータの内容を確認できるモニタリングソフトウェアです。

 などが代表的です。

 下の画面はCharlesのものですが、送受信するAMFの内容を、型情報と共に表示してくれるため、問題の切り分けに効果的です。AMFの生データのHexダンプなど、追加の情報も確認することができます。

終わりに

 サーバサイドのサービスをスクリプト言語で記述し、Flexクライアントと通信する簡単な例を紹介してきましたが、いかがでしたでしょうか。意外と簡単にクライアント・サーバ連携が実現できることが伝われば幸いです。

 最後に、サーバサイドとの通信を容易にするためのオープンソースのライブラリを幾つか紹介したいと思います。

 今回のサンプルでは、生のNetConnectionクラスを使って処理を行いましたが、イベントリスナの登録など、定型の処理を記述する必要がありました。これらのライブラリを使うことにより、定型の処理の記述を省くなど、より簡単に通信処理を記述することが出来ます(もちろん、他にもいろいろな機能が提供されています)。

 クラスメソッド社では、主にFlex/AIR技術を使ったユーザーインターフェース開発を行っています。また、Flexと連携する様々なサーバーサイドテクノロジーを利用し、多くの業務システム開発を行っています。Flex/AIRを用いたシステム開発を検討されている企業様はお気軽にお問い合わせください。

「FlexではじめるRIA開発」特集、絶賛公開中!

この記事は参考になりましたか?

  • このエントリーをはてなブックマークに追加

【AD】本記事の内容は記事掲載開始時点のものです 企画・制作 株式会社翔泳社

この記事は参考になりましたか?

この記事をシェア

  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/4541 2009/11/04 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング