パート12: 詩の変換サーバー

もうひとつのサーバ

Twisted を使ったサーバをひとつ実装してみましたので、もうひとつ作ってみましょう。 その後で Deferreds についてもっと詳しく学習することにしましょう。

パート9: Deferred 再入門”と”パート10: 変換された詩”で、詩の変換エンジンを導入しました。 ここまで実装してきた cummingsifier はとても単純で、失敗をシミュレートするために随意な例外を追加しました。 しかし、変換エンジンが他のサーバで稼動していてネットワーク越しに「詩の変換サービス」を提供するものだと、失敗ははるかに現実的な問題となります。 たとえば、変換サーバがダウンしている状態です。

パート12では、詩の変換サーバの実装を続けていきます。 次のパートでは、詩のクライアントが外部の変換サービスを使うようにして、その過程で Deferreds に関して新しいことをいくつか学んでいきましょう。

プロトコルの設計

ここまでは、クライアントとサーバのやり取りは厳密に片方向でした。 サーバはクライアントに詩を送りますが、クライアントはサーバに何も送りません。 しかし、変換サービスは双方向です。 クライアントが詩をサーバに送ると、サーバは変換した詩を送り返します。 よって、そのやり取りを処理するプロトコルを利用あるいは発明する必要があるでしょう。

このタイミングで、サーバに複数種類の変換をサポートさせて、クライアントにどれを使うか選んでもらうようにしましょう。 クライアントからは、変換を特定する名前と完全な詩のテキストという二種類の情報を送るようにします。 サーバは、変換された詩のテキストと呼ぶことにする、単一の情報を返します。 これで非常に単純な一種の遠隔手続き呼び出し (Remote Procedure Call) ができました。

Twisted は、この問題を解決するために利用できる複数のプロトコルをサポートしています。 XML-RPCPerspective Broker 、それから AMP です。

しかし、このように完全な機能を持ったプロトコルのいずれかでも導入すると、非常に遠回りになってしまいます。 そこで代わりに、自分たちでちっぽけなプロトコルを使えるようにしてみましょう。 クライアントに次の形式の文字列を送るようにさせます。かぎ括弧はありません。

<変換名>.<詩の文字列>

変換の名前とその後にピリオド、それから詩の全文を続けただけです。 そうしたら netstring 形式で全体をエンコードしましょう。 サーバから変換された詩を送り返すのも netstrings とします。 netstrings は長さに基づく符号化 (length-encoding) ですので、サーバが詩の完全な結果を送り返すのに失敗した場合をクライアントは検知できます (おそらく操作の途中にクラッシュした場合です)。 もしも覚えているなら、元々の詩のプロトコルは詩を送受信するときに中断されたことを検知する部分に問題がありましたよね。

プロトコルの設計はこの辺にしましょう。何らかの賞を狙ったものでもありませんから。とはいえ、私たちの目的にはこれで十分です。

コード

それでは変換サーバのコードに目を通してみましょう。 twisted-server-1/tranformedpoetry.py にあります。 最初は TransformService を定義します。

class TransformService(object):

    def cummingsify(self, poem):
        return poem.lower()

今のところ変換サービスは cummingsify という変換を同名のメソッドで実装しているだけです。 さらなるメソッドを追加することでアルゴリズムを追加できるでしょう。 ここで注意しておくことがあります。 変換サービスは、私たちが以前に定義したプロトコルの詳細からは完全に独立しています。 サービスの実装からプロトコルの実装を分離することは Twisted を使うとよくあるパターンです。 こうすることで、コードが重複せずに複数のプロトコルを使って同じサービスを提供することが簡単になります。、

それでは protocol factory を見ていきましょう (プロトコル自体はその後で)。

class TransformFactory(ServerFactory):

    protocol = TransformProtocol

    def __init__(self, service):
        self.service = service

    def transform(self, xform_name, poem):
        thunk = getattr(self, 'xform_%s' % (xform_name,), None)

        if thunk is None: # no such transform
            return None

        try:
            return thunk(poem)
        except:
            return None # transform failed

    def xform_cummingsify(self, poem):
        return self.service.cummingsify(poem)

このファクトリでは、接続元のクライアントに代わってプロトコルインスタンスが詩の変換を要求するために利用できる transform メソッドを提供します。 もしも要求する変換が存在しない場合、あるいは変換に失敗した場合は None を返します。 TransformService のように、プロトコルファクトリは接続レベルのプロトコルから独立していて、その詳細はプロトコルクラス自身に委譲されます。

押さえておくべきことのひとつは、 xform_ というプリフィックスの付いたメソッドを介さないとサービスにアクセスできないように制限している方法です。 プリフィックス名は異なり、たいていはファクトリではないオブジェクトに存在しますが、Twisted のソースコード内で見かけるパターンです。 クライアントコードがサービスオブジェクトの特定メソッドを実行するのを防ぐひとつの方法です。クライアントはあらゆる変換名を送れるからです。 また、サービスオブジェクトによって提供される API へプロトコル特有の適合を実行する場所でもあります。

それでは プロトコルの実装 をみてみましょう。

class TransformProtocol(NetstringReceiver):

    def stringReceived(self, request):
        if '.' not in request: # bad request
            self.transport.loseConnection()
            return

        xform_name, poem = request.split('.', 1)

        self.xformRequestReceived(xform_name, poem)

    def xformRequestReceived(self, xform_name, poem):
        new_poem = self.factory.transform(xform_name, poem)

        if new_poem is not None:
            self.sendString(new_poem)

        self.transport.loseConnection()

プロトコルの実装においては、 NetstringReceiver プロトコルで Twisted が netstrings をサポートしている利点を活用します。 基底クラスが netstrings のデコード (エンコードも) を処理してくれますので、私たちは stringReceived メソッドを実装するだけです。 言い換えると、 stringReceived にはクライアントから送られてきた netstrings の content 部分を渡されます。 netstrings エンコーディングによって付け足された余分なバイトは含みません。 また、基底クラスは、完全な文字列をデコードするのに十分になるまで入力バイトをバッファリングしてくれます。

もしもすべてが順調なら (そして、私たちが接続を閉じたばかりでなければ)、 NetstringReceiver が提供する sendString メソッド (最終的には transport.write() を呼び出します) を使って変換済みの詩をクライアントに送り返します。 これが起こっていることの全てです。 前回見たものと似ていますので、 main 関数を示して退屈させたりはしません。

xformRequestReceived メソッドを定義することによって、入力バイトストリームを抽象度の高い形式に変換する Twisted のパターンをつないでいる方法に注意してください。 このメソッドには変換名と詩が別々の引数として渡されています。

簡単なクライアント

次のパートでは変換サービスのために Twisted のクライアントを実装しましょう。 今のところは twisted-server-1/transform-test にある簡単なスクリプトを作るだけにしておきます。 サーバに詩を送るために netcat プログラムを使い、レスポンスを表示する (netstrings でエンコードされているでしょう) だけです。 次のようにして変換サーバを 11000 番ポートで動かしましょう。

python twisted-server-1/tranformedpoetry.py --port 11000

このようにするとサーバに対してテストスクリプトを実行できます。

./twisted-server-1/transform-test 11000

次のような出力が見えるでしょうか。

15:here is my poem,

netstring でエンコードされた詩になっていますね (元の詩は全て大文字でした)。

議論

このパートでいくつかの考え方を紹介しました。

  1. 双方向の通信。

  2. Twisted によって提供される既存のプロトコル実装の上に構築していくこと。

  3. 機能のロジックとプロトコルのロジックを分離するためにサービスオブジェクトを使うこと。

双方向通信の基本的な機構は単純です。 ひとつ前のクライアントとサーバでデータを読み書きするときに同じテクニックを使っています。 唯一の違いは両方ともで一緒に使ったことです。 もちろん、もっと複雑なプロトコルではバイトストリームを処理して送信メッセージを整形するためにもっと複雑なコードになります。 そしてこれこそが、先ほどやったように、既存のプロトコルの実装を使う大きな理由です。

基本的なプロトコルを記述することがカンタンになってきたら、Twisted が提供する他のプロトコルの実装にも目を通してみるのは良い考えですね。 twisted.protocols.basic モジュールから始めるのが良いかもしれません。 簡単なプロトコルを記述することは、Twisted のプログラミングスタイルに慣れる素晴らしい方法です。 しかし現実のプログラムでは、プロトコルに使いたいと思うものがひとつはあると仮定して、今すぐ使える実装を使う方がよくあるでしょう。

ここで紹介する最後の新しいアイデアは、機能とプロトコルの詳細を分離するためにサービスオブジェクトを使うことで、Twisted プログラミングでは本当に重要なデザインパターンです。 このパートで作ったサービスオブジェクトはちっぽけなものですが、もっと現実的なネットワークサービスだと非常に複雑になると想像できますよね。 そして、サービスをプロトコル層の詳細から独立させることにより、コードが重複することなく新しいプロトコル上で同じサービスを素早く提供できます。

図27では、ふたつの異なるプロトコルを介して詩を変換する変換サーバを示します (上で示したサーバのバージョンではひとつのプロトコルだけです)。

_images/p12_server-21.png

図27:ふたつのプロトコルを持つ変換サーバ

図27ではふたつの別々のプロトコルファクトリを必要としましたが、 protocol クラス属性が異なるだけでも、単に識別可能なだけでも構いません。 ファクトリは同じサービスオブジェクトを共有し、 Protocol 自体が異なる実装を要求するようにできます。 これでコードが再利用可能になりましたね。

次は

変換サーバに関してはこの辺で十分でしょう。 “パート13: Deferred と行こう” では、クライアント自身で変換を実装する代わりに変換サーバを使うように詩のクライアントを更新しましょう。

おすすめの練習問題

  1. NetstringReceiver クラスのソースコードを読みましょう。 クライアントが不正な netstring を送信すると何が起こるでしょうか? クライアントが巨大な netstring を送信しようとしたときは?

  2. 違う変換アルゴリズムを考えて、変換サービスとプロトコルファクトリに追加しましょう。 netcat クライアントを修正してテストしてみてください。

  3. 詩の変換を要求するプロトコルをもうひとつ考えて、サーバが両方のプロトコルを処理できるように修正しましょう (ふたつの異なるポート上です)。 それぞれに対して TransformService の同じインスタンスを使います。

  4. TransformService のメソッドが非同期だったとすれば (つまり遅延オブジェクトを返します)、どのようにコードを修正しましょうか?

  5. 変換サーバ用の同期クライアントを実装しましょう。

  6. 詩を送信するときは netstrings を使うように元のクライアントとサーバを更新しましょう。