2011年7月4日月曜日

C# からProtoRPCを呼び出すサンプル

といっても、単にJSONオブジェクトをhttpで送ってるだけです。
JSONのシリアライズには、DynamicJsonを使っています。
これは簡単だ

GettingStartGuideのはじめの方にある、
helloサービスに接続するサンプルです。

var obj = new {
my_name = "3sons"
};
var jsonStringFromObj = Codeplex.Data.DynamicJson.Serialize(obj);
byte[] postDataBytes = System.Text.Encoding.ASCII.GetBytes(jsonStringFromObj);

System.Text.Encoding enc = System.Text.Encoding.GetEncoding("UTF-8");
System.Net.WebRequest req = System.Net.WebRequest.Create("http://localhost:8080/hello.hello");
req.Method = "POST";
req.ContentType = "application/json";
//POST送信するデータの長さを指定
req.ContentLength = postDataBytes.Length; 

//データをPOST送信するためのStreamを取得 
System.IO.Stream reqStream = req.GetRequestStream();
//送信するデータを書き込む 
reqStream.Write(postDataBytes, 0, postDataBytes.Length);
reqStream.Close();

//サーバーからの応答を受信するためのWebResponseを取得 
System.Net.WebResponse res = req.GetResponse();
//応答データを受信するためのStreamを取得 
System.IO.Stream resStream = res.GetResponseStream();
//受信して表示
System.IO.StreamReader sr = new System.IO.StreamReader(resStream, enc);
string ret = sr.ReadToEnd();
//閉じる 
sr.Close();
MessageBox.Show(ret);

C#は.Net2.0のころ触って以来ですが、いろいろ便利になってますね。varとか
現在、これを使ってGAEをバックエンドに使うWindowsアプリを開発中です。

ローカルDBにSQLiteを使い、定期的/必要に応じて、
ProtoRPCを使ってGAEのデータストアとレプリケーションを行います。
同じことはAndroidでもiPhone/iPadでもできるはずですので、
端末のフル機能を使用するクラウドシステムが構築できます。

また、クラウドの弱点である、ネットワークの混雑による遅延や、
ネットワークトラブルによりシステムが使えない、
といった問題を防ぐことができます。

2011年7月3日日曜日

19.Generate postservice module from the server

サーバー(ゲストブックアプリケーション)で作成したpostserviceモジュールのコードは、
クライアントに対しては秘密であり、クライアントがその中身を知る必要はないはずです。
また、サーバーのコードはクライアントには必要のないライブラリに依存している場合もあります。
(つまり、クライアントアプリケーションを書く人が、サーバーのpostserviceモジュールを入手し、利用することは通常、ありえません。また、その必要もありません。)

サーバーは、RegistryServiceを公開していますので、
クライアントプログラムを書く際に必要になるモジュールを自動生成することができます。

これを行うために、ProtoRPCでは、「get_protorpc.py」というコマンドラインツールを用意しています。
コマンドシェルで、クライアントアプリケーションのディレクトリに移動し、
サーバーを指定してこのコマンドを実行することで、モジュールのコードを自動生成できます。
以下のコマンドを実行してみてください。

gen_protorpc.py registry localhost:8080 /postservice

クライアントアプリケーションのディレクトリを覗いてみると、
postservice.pyという名前のファイルがあるはずです。

そのファイルの中身は、サーバーで書いたpostservice.pyとよく似ていると思います。
しかし、クライアント側はPostServiceのメソッドは空で、何も書かれていないはずです。


Service stubs

サーバーとの通信は、クライアント側の持つスタブクラスによって行います。
個々のサービスは、スタブサブクラスを持ち、スタブはすべてのメソッドを含んでいます。

from protorpc import transport
import postservice

service = postservice.PostService.Stub(
    transport.HttpTransport(
        'http://localhost:8080/postservice'))


Invoke remote method

スタブを用いて、サーバーのget_notesメソッドを呼び出すリクエストハンドラを書いてみましょう。
(6行目に出てくる「service」は、さきほど定義したスタブです。
ですので、実行する際には、class MainHandlerの宣言の前に、上のservice=のコードを書いてください。)

from google.appengine.ext import webapp
from google.appengine.ext.webapp import util

class MainHandler(webapp.RequestHandler):
    def get(self):
        notes = service.get_notes().notes
        if notes is None:
            notes = []

        note_count = len(notes)
        note_lengths = [len(note.text) for note in notes]
        total_characters = sum(note_lengths)
        if note_count == 0:
            average_characters = 0
        else:
            average_characters = total_characters / note_count
        self.response.out.write('<br>Notes retrieved: %d\n' %
                                note_count)
        self.response.out.write('<br>Total characters: %d\n' %
                                total_characters)
        self.response.out.write('<br>Average characters: %d\n' %
                                average_characters)


def main():
    application = webapp.WSGIApplication([('/', MainHandler)],
                                         debug=True)
    util.run_wsgi_app(application)


if __name__ == '__main__':
    main()


dev_appserverで、ゲストブックアプリケーションとは異なるポート番号を指定して、
クライアントアプリケーションを立ち上げてください。
クライアントアプリケーションにアクセスすると、
ゲストブックアプリケーションの投稿に関する統計情報が表示されるはずです。


Sending Parameters

上記では、クライアントアプリケーションは、get_notesを引数無しで呼び出しました。
しかし、サーバーはリクエストとしてGetNotesRequest型を受け取ることを知っていますよね
当然、クライアント側では、GetNotesRequest型のフィールドで指定されたパラメータをサーバーに送信することができます。
例えば、max_notesパラメータを送信することで、サーバーが返すノートの件数の上限を指定できます。

実は、スタブメソッドは内部でGetNotesRequestのインスタンスを生成し、それをサーバーに送っています。
get_notesメソッドのキーワード引数を指定することで、
サーバーに送信するGetNotesRequestインスタンスのフィールドを設定することができます。
以下のように、get_notesの呼び出し部分を変更します。

max_notes = int(self.request.params.get('max_notes', '10'))
notes = service.get_notes(limit=max_notes).notes


また、キーワード引数ではなく、GetNotesRequestのインスタンスそのものを渡すこともできます。

request = postservice.GetNotesRequest(limit=max_notes)
notes = service.get_notes(request).notes

18.Creating a client program

さあ、いよいよ、リモートサービスを「利用する」アプリケーションを作成します。

ゲストブックアプリケーションのサービスを利用するこのアプリケーションを「gueststats」と呼ぶことにします。
このアプリケーションは、最近の投稿の情報を収集し、ユーザーに示します。


まず、新規のApp Engineアプリケーションを生成し、
サーバー(ゲストブックアプリケーション)で行ったのと同様に、
protorpcのライブラリファイルをコピーしてください。
(または、eclipseであれば外部参照に追加)

17.Viewing a method form

PostService.post_noteメソッドをフォーム上で見てみましょう。

先ほどのwebページで、PostService.post_noteのリンクをクリックしてみてください。


こんなページが表示されると思います。

フォームには、Noteクラスに定義したフィールドが表示されているはずです。
whenフィールドは必須ではありませんから、デフォルトでは送信しない、と表示されています。

whenのチェックボックスをチェックすることで、whenフィールドに値を設定することができるのですが、今はそれをせずに、
textフィールドだけ何か入力して、「Send Request」をクリックしてください。

リクエストが成功すれば、レスポンスが表示されます。
curlを用いた場合と同様、レスポンスは空のJSONオブジェクトのはずです。


次のテストに進む前に、ゲストブックにいくつかのノートを追加しておいてください。
それができたら、ページ上部の「Back to method selection」をクリックしてください。

戻ったページで、PostService.get_notesメソッドをクリックします。


こんなページが表示されます。
パラーメータを設定せずに「Send Request」ボタンをクリックすると、
先ほどのPostService.post_noteよりは面白いレスポンスが表示されます。


「Show Form」ボタンをクリックすると、リクエストの送信ページに戻りますので、
orderや、limitといったパラーメータを変更して結果が変わるかどうか試してみてください。

16.The forms interface

webappフレームワークを用いて、ProtoRPCのリモートメソッドをテストする方法があります。

「service_mapping」ファンクションが、実際にリモートサービスを登録して公開するのですが、
その際に、「フォームインターフェース」も自動登録し、使えるようにします。

「フォームインターフェース」とは、登録したリモートサービスと、そのメソッドを閲覧できるwebページで、
さらに、そのページを使って、パラメータを与えてメソッドを呼び出すことができます。

この便利なフォームインターフェースは、app.yamlに、以下のような行を追加するだけで使用できます。
- url: /protorpc.*
   script: services.py

ブラウザで以下のURLにアクセスすることで、フォームインターフェースを使えます。

http://localhost:8080/protorpc/form


こんな感じのページが表示されます。
あれ、サービスが2つありますね。
(RegistryServiceと、PostService)

PostServiceは、以前の章で作成したものですが、RegistryServiceとは何でしょう?

The RegistryService

「service_mapping」ファンクションは、特に指定しなくても、RegistryServiceという組み込みサービスを自動登録します。

RegistryServiceは、ユーザーが登録したサービスに関する情報で構成され、サーバーがどんなサービスを公開しているのかを調べるのに役立ちます。
また、フォームインターフェース自体も、RegistryServiceを使用しています。

他にもいろいろあるのですが、とりあえずここでの説明はこれぐらいにしておきます。

2011年7月1日金曜日

15.Implement get_notes

PostServiceクラスにget_notesメソッドを定義してみましょう。


@remote.method(GetNotesRequest, Notes)
def get_notes(self, request):
    query = guestbook.Greeting.all().order('-date')
    if request.on_or_before:
        when = datetime.datetime.utcfromtimestamp(request.on_or_before)
        query.filter('date <=', when)

    notes = []
    for note_model in query.fetch(request.limit):
    if note_model.date:
        when = int(time.mktime(note_model.date.utctimetuple()))
    else:
        when = None
    note = Note(text=note_model.content, when=when)
    notes.append(note)

    if request.order == GetNotesRequest.Order.TEXT:
        notes.sort(key=lambda note: note.text)

    return Notes(notes=notes)

モジュールの先頭でtimeをインポートするのを忘れずに。
import time

14.More complex messages

では、get_notesのレスポンスメッセージを定義してみましょう。

get_notesのレスポンスは、複数の投稿を返すため、
Noteメッセージのコレクションとなる必要があります。
これは以下のように定義します。

class Notes(messages.Message):
  notes = messages.MessageField(Note, 1, repeated=True)



Message Fields


メッセージは、他のメッセージを含むことができます。
上記のNotes.notesフィールドでNoteを最初のパラメータとしたように、
最初のパラメータとして指定します。



Repeated Fields


上記のNotes.notesフィールドはまた、repeatedキーワードが指定されています。
repeatedフィールドの値は、指定した型のリストでなければなりません。
上記の例では、Notes.notesフィールドの値は、Note型のインスタンスのリストでなければなりません。

例として、Notesインスタンスの作成方法を示してみましょう。

response = Notes(notes=[Note(text='This is note 1'),
                        Note(text='This is note 2')])
print 'First Note is:', response.notes[0].text
print 'Second Note is:', response.notes[1].text





13.Enum types

orderフィールドは、enumフィールドを使用します。

enumは、フィールドの値を特定の値だけ許可し、それ以外の値を禁止したい場合に使用します。
この場合、enumは、サーバーがレスポンスを生成する際のデータの並び順を指定します。

enumを定義したい場合は、Enumクラスのサブクラスを作成します。
それぞれのenumのメンバには、enum型ごとに一意の数値が割り当てられます。
その数値は、enum型のインスタンスになるほか、クラス変数としてクラスからアクセスすることも可能です。

print 'Enum value Order.%s has number %d' % (Order.WHEN.name,
                                             Order.WHEN.number)

enum型のインスタンスには特別の性質があり、
上記のように「name」や「number」というメンバを使わなくても、自動的に文字列や数値に変換されます。
print 'Enum value Order.%s has number %d' % (Order.WHEN,
                                             Order.WHEN)
メッセージにenum型のフィールドを定義する場合、enum型を、最初のパラメータに指定してください。
enum型のフィールドも、デフォルト値を持つことができます。
order = messages.EnumField(Order, 3, default=Order.WHEN)

12.Default values

リクエストを送る際、日時を指定し、その日時以前に投稿されたノートを取得するようにし、
(on_or_beforeフィールド。タイムスタンプを整数型で受け取り、日時として使用します。)

また、取得するノートの順序を指定できるようにします。(orderフィールド)

limitフィールドは、取得するノートの最大件数を指定します。
もしlimitフィールドが指定されていない場合、defaultキーワードで指定された10が最大件数になります。

11.More fields

postserviceに新しいノートを投稿できるようになりました。
次は、投稿されたノートを取得する、get_notesメソッドを定義してみましょう。

まず、リクエストメッセージを、postservice.pyファイルの、サービスクラス定義より上に書きます。

class GetNotesRequest(messages.Message):
  limit = messages.IntegerField(1, default=10)
  on_or_before = messages.IntegerField(2)
  class Order(messages.Enum):
    WHEN = 1
    TEXT = 2
  order = messages.EnumField(Order, 3, default=Order.WHEN)

10.Testing the service from the command line

これで、サービスのテストが可能になりました。
curlなどのツールでアクセスしてみましょう。

% curl -H \
     'content-type: application/json' \
    -d {"text": "Hello guestbook!"}'\
    http://localhost:8080/postservice.post_note

サーバーが空のJSONレスポンスを返せば、成功です。ノートは投稿されています。
ブラウザでゲストブックアプリケーションを開けば、投稿されたノートを見ることができるでしょう。

http://localhost:8080/

09.Registering the service

ここまでの説明でサービス、メッセージを定義できましたので、
App Engineのwebappフレームワークを利用して、サービスを公開し、
外部からサービスにアクセスできるようにします。

service.pyというファイルをアプリケーションディレクトリに作成してください。
そして、以下のコードを追加します。

from google.appengine.ext import webapp
from google.appengine.ext.webapp import util
from protorpc.webapp import service_handlers
import postservice
# Register mapping with application.
application = webapp.WSGIApplication(
  service_handlers.service_mapping(
      [('/postservice', postservice.PostService)]),
  debug=True)

def main():
  util.run_wsgi_app(application)

if __name__ == '__main__':
  main()


そして、以下のURLハンドラをapp.yamlに追加してください。

- url: /postservice.*
  script: services.py

08.Defining a service

「サービス」は、「Service」(protorpc.remote.Service)を継承したクラスによって定義します。

ゲストブックアプリケーションのチュートリアルで、
ゲストブックの挨拶文は、guestbook.Greetingクラスによって、データストアに保存しました。

Postサービスも、投稿のデータストアへの保存にGreetingクラスを使用することにします。

import datetime
from protorpc import message_types
from protorpc import remote
import guestbook
class PostService(remote.Service):
  @remote.method(Note, message_types.VoidMessage)
  def post_note(self, request):
    if request.when:
      when = datetime.datetime.utcfromtimestamp(request.when)
    else:
      when = datetime.datetime.now()
    note = guestbook.Greeting(content=request.text, date=when)
    note.put()
    return message_types.VoidMessage()

「method」デコレータ(@remote.method)は、2つのパラメータを取ります。
最初のパラメータは、リクエストのデータ型で、
2つ目のパラメータは、レスポンスのデータ型です。

ノートの投稿を受け付けるために、上記の「post_note」メソッドはNoteクラス(メッセージ)のインスタンスをリクエストとして受け取ります。

レスポンスには、「VoidMessage」(protorpc.message_types.VoidMessage)を指定していますが、
これはProtoRPCの組み込み型で、フィールドを持たないメッセージを意味します。

つまり、「post_note」メソッドは、呼び出し側に意味のあるデータを返しません。
エラーが発生しない限り、呼び出し側では、投稿は成功した、と見なします。

Note.whenは、必須フィールドではないので、呼び出し側がセットしているとは限りません。
セットしていない場合、whenにはNoneがセットされています。

「post_note」メソッドは、Noteにタイムスタンプが設定されていない場合、現在時刻を指定したものと見なします。

レスポンスメッセージは、リモートメソッドによってインスタンス化され、戻り値として使われます。

07.Working with messages

「メッセージ」は、ProtoRPCで、クライアントアプリケーションと、
サーバーがやりとりをするためのデータ型です。

「メッセージ」は、「Message」(protorpc.messages.Message)を継承したクラスとして定義します。
データフィールドはクラスの属性に相当します。

例として、ゲストブックアプリケーションでは、ユーザーはノートの投稿ができます。
そのノートを表現するメッセージを作ってみましょう。


from protorpc import messages
class Note(messages.Message):
  text = messages.StringField(1, required=True)
  when = messages.IntegerField(2)

このノートメッセージは、「text」と「when」という、2つのフィールドを持ちます。
それぞれのフィールドは型を持ちます。
textフィールドは文字列(ユニコード文字列)であり、ユーザーがゲストブックに投稿した文章を表します。
whenフィールドは整数であり、ノートが投稿された際のタイムスタンプを表します。

それぞれのフィールドは、一意の数値を持ちます。
(フィールド定義の最初の引数のことです。ノートメッセージの場合、「text」が1で、「when」が2です。)
この数値は、データをネットワークに流す際に、フィールドの識別子として利用されます。
(名前は識別子として使用しません。)

フィールドの値は、Noteクラスのコンストラクタでセットできます。
import time
note_instance = Note(text=u’Hello guestbook!’,
                     when=int(time.time())


これらのフィールドは、通常のPythonクラスの属性と同様に読み書きできます。
例えば、

print note_instance.text
note_instance.text = u‘Good-bye guestbook!’
print note_instance.text

とした場合、この出力は以下のようになります。

Hello guestbook!
Good-bye guestbook!

フィールドは、値の設定を必須にすることも、しないこともできます。
デフォルトは必須ではありませんが、フィールド定義に「required=True」を追加することで、必須になります。

メッセージのインスタンスが初期化済みである、と見なされるためには、必須フィールドが設定されている必要があります。
そして、ProtoRPCサービスは、初期化済みであるとみなされたメッセージのみ、受け取ることができます。

06.The postservice

postserviceモジュールは、ゲストブックアプリケーションのデータにアクセスするための、
以下の2つのリモートメソッドを定義するモジュールです。

post_note:App Engineのデータストアにノートを投稿します。
get_note:投稿されたノートを取得します。

とりあえず、アプリケーションディレクトリに、postservice.pyファイルを作成してください。
ファイルの中身は、順次説明していきます。

2011年6月30日木曜日

05.What a service is made of

ProtoRPCの一つのサービスは、複数のリモートメソッドを定義した、一つのPythonクラスによって定義します。

それぞれのリモートメソッドは、リクエストを受け取り、レスポンスを返します。
リクエストとレスポンスはともに、messages.Messageを継承した、ユーザー定義のクラスです。

04.Defining a service

Google App Engineの、GettingStartedGuideで、ゲストブックアプリケーションを作成しました。

ゲストブックアプリケーションは、典型的なwebアプリケーションで、
サイト訪問者は、メッセージを記入することができ、
また、他の訪問者が記入したメッセージの閲覧ができます。

ゲストブックアプリケーションは、
アプリケーションとユーザーが直接対話する、という前提で書かれています。

このため、ゲストブックアプリケーションは、他のwebアプリケーションが、簡単にゲストブックアプリケーションの情報にアクセスして、それを利用できるようなインターフェイスを公開していません。

例えば、ゲストブックアプリケーションに投稿されたメッセージの日時から、
1日あたりの投稿数のグラフを出力するようなアプリケーションを作成したい場合などです。

03.Installation

ProtoRPCは、まだ開発の初期段階です。
現在、ProtoRPCを使ってみる、一番手っ取り早い方法は、Google App Engineを使うことです。

<protorpc-source-dir>/python/protorpc を、アプリケーションディレクトリにコピーするか、
eclipseを使っている場合は外部参照のライブラリに追加してください。

ただし、SDKのバージョンは1.5.1以上を使ってください。
それより古いと、sdkのディレクトリに、ProtoRPC関係のファイルはありません。

02.Helloサンプル

次のコードは、ProtoRPCで定義された、簡単なRPCサービスの例です。

from google.appengine.ext import webapp
from google.appengine.ext.webapp import util
from protorpc import messages
from protorpc import remote
from protorpc.webapp import service_handlers
class HelloRequest(messages.Message):
  my_name = messages.StringField(1, required=True)
class HelloResponse(messages.Message):
  hello = messages.StringField(1, required=True)
class HelloService(remote.Service):
  
  @remote.method(HelloRequest, HelloResponse)
  def hello(self, request):
    return HelloResponse(hello='Hello there, %s!' %
                         request.my_name)
service_mappings = service_handlers.service_mapping(
    [('/hello', HelloService),
    ])
application = webapp.WSGIApplication(service_mappings)
def main():
  util.run_wsgi_app(application)
if __name__ == '__main__':
  main()
 この簡単なサービスは、クライアントから、ユーザーの名前を受け取ります。(HelloRequest.my_name)
そして、そのユーザーに対する挨拶メッセージを返します。
(HelloResponse.hello)

注目して欲しいのは、これがサーバー側で書かなければならないコードのすべてであり、
PythonからJSONに変換したり、HTTPリクエストハンドラを書く必要はない、ということです。

次に、JQueryを使って、javascriptからこのサービスを使う方法を示します。


  $.ajax({url: ‘/hello.hello’,
          type: 'POST',
          contentType: 'application/json',
          data: ‘{ my_name: Bob }’,
          dataType: 'json',
          success: function(response) {
            // The response is { hello: “Hello there, Bob!” }
            alert(response.hello);
          }
         });
このコードは、サーバーにリクエストをJSONで送り、サーバーはレスポンスをJSONで返します。
ProtoRPCのメッセージ型は、よく使われるデータ型に対応していますので、
JQueryのsuccessハンドラを使うことで、簡単にjavascriptオブジェクトに変換できます。

01.導入

ProtoRPCは、webベースのRPC(Remote Procedure Call)サービスを簡潔に定義する方法です。

RPCサービスとは、リモート呼び出し可能なメソッドと、メソッドが受け取ったり返したりするメッセージ型の集まりであり、
それらを用いることにより、クライアントアプリケーションがwebアプリケーションと通信することを可能にします。

さて、実際に自分でこのようなwebベースのRPCを、
クライアント:javascript
サーバー:Python
という環境で定義する場面を想定してみてください。

サーバー側では、各メソッドに相当するURLを定義し、リクエストを受け取った際に、パラメータのチェックを行い、Pythonのオブジェクトに変換する必要があります。
クライアント側では、ユーザーの入力チェックを行い、入力データをHTTPリクエストのパラメータに変換します。
そしてサーバーからの返答をjavascriptオブジェクトに戻す必要があります。

このような変換まわりのコードは、毎回同じようなコードになる上に、
くだらない間違いにイライラさせられることが多いです。

ProtoRPCは、この種の変換を、異なるプログラム言語間であっても簡単に行えるようにしたライブラリです。
ProtoRPCは、サーバー側で、リモートメソッドやメッセージ型をPythonオブジェクトとして定義できるようにしました。

Pythonだけを用いて、RPCの定義ができますので、
独自のRPCの作成が容易になるはずです。