実装(RabbitMQ編)
まずはサーバ側のコード「src/rpc_server.py」について解説します。以下はコードの抜粋になります。
10 class TestEndpoint(object):
11 target = oslo_messaging.Target(namespace='foo', version='1.2')
12
13 def hoge(self, ctxt, arg):
14 print("[TestEndpoint] hoge(%s, %d) is called" % (ctxt, arg))
15 return arg * 2
16
17 def start_server(tgt_topic = DEFAULT_TOPIC, tgt_server = DEFAULT_SERVER, url=''):
18 transport = oslo_messaging.get_transport(cfg.CONF, url=url)
19 target = oslo_messaging.Target(topic=tgt_topic, server=tgt_server)
20 endpoints = [
21 TestEndpoint(),
22 ]
23
24 server = oslo_messaging.get_rpc_server(transport, target, endpoints)
10 行目のTestEndpointがRPCリクエストを処理するメソッド(hoge)を実装したエンドポイントです。ここではエンドポイントの定義を1つだけしか指定していませんが、章の冒頭で説明したとおり複数のエンドポイントを指定することもできます。
24行目のget_rpc_serverメソッドの呼び出しでServerオブジェクトを生成します。Serverオブジェクトは18行目で生成されたTransportオブジェクト(各種MQを抽象化したオブジェクト)、および後述するTargetオブジェクト、そして10行目で定義されたエンドポイントのオブジェクトにひも付きます。
TargetはAPIのnamespaceとversion情報を保持したデータ構造で、エンドポイントとServerにひも付きます。Targetは、接続するRPCクライアント、および受け取るRPCリクエストを取捨選択できるようにするためのクラスで、サンプルでは11行目でエンドポイントのTargetオブジェクトを生成しており、19行目でServerのTargetオブジェクトを生成しています。
EndpointオブジェクトにおいてTargetの設定を省略した場合には、グローバルなnamespace(=None)とversion(=1.0)が暗黙的に設定されます。ただしServerオブジェクトではTargetオブジェクトをtopicおよびserverパラメータ付きで指定しなければなりません。
Targetオブジェクトのtopicパラメータは、サーバが提供するAPIを識別するための項目でAMQPドライバでは名前付きキューの名前に対応します。クライアントはこの値だけを知っていれば、サーバの場所(IPアドレス/ホスト名)を意識せずにRPCリクエストをサーバに送ることができます。
また、Targetオブジェクトのserverパラメータは、サーバを一意に識別できる任意の文字列になります(サーバのホスト名やIPアドレスと一致していなくても問題ありません)。これはクライアントがtopicパラメータで指定したAPIを持つサーバ群のうち、特定のサーバに対してメッセージを送りたい場合に利用されます。AMQPドライバではtopic名で指定した名前付きキューに加えて、「topic.server」の名前付きキューを生成します。
最後に、Endpointのコールバックメソッドはctxtとargの2つのパラメータを取ります。サンプルの実装では、ctxtに任意のdict型の値を受け取り、argの値を2倍してクライアントに結果を返すという処理を行います。
次にクライアントのコード「src/rpc_client.py」について見ていきます。同じようにコードの抜粋を以下に示します。
9 class TestClient(object): 10 def __init__(self, transport, tgt_topic, tgt_server): 11 target = oslo_messaging.Target(topic=tgt_topic, server=tgt_server) 12 self.client = oslo_messaging.RPCClient(transport, target) 13 14 def hoge(self, ctxt, arg): 15 cctxt= self.client.prepare(namespace='foo', version='1.1') 16 return cctxt.call(ctxt, 'hoge', arg = arg) 17 18 def send_request(ctx, arg, topic=DEFAULT_TOPIC, server=DEFAULT_SERVER, url=''): 19 transport = oslo_messaging.get_transport(cfg.CONF, url=url) 20 client = TestClient(transport, topic, server) 21 22 return client.hoge(ctx, arg)
18行目のsend_requestメソッドの呼び出しでサーバへRPCリクエストを送信し、サーバで実行された処理の結果を返します。内部ではRPCクライアント処理を行うoslo.messagingのクラスRPCClientのオブジェクトを生成し、サーバ処理と同様にTransportオブジェクトとTargetオブジェクトをひも付けてRPCリクエストを送ります。
9行目で定義しているTestClientはRPCClientのラッパーで20行目でオブジェクト化しています。引数にTransportオブジェクトを取り、内部でRPCClientオブジェクトを生成しTargetオブジェクトとのひも付けを行っています。
そして22行目のメソッドhogeの呼び出しで、サーバに対してRPCリクエストを送ります。内部では15行目のprepare()メソッドを呼び出し、RPCClientオブジェクトが内部に持つTargetオブジェクトのnamespaceとversionパラメータを設定します。
11行目と15行目で2回Targetオブジェクトの設定をしていることを疑問に思うかもしれません。先ほどのサーバ側の処理でServerとエンドポイントの2つに別々のTargetをひも付けていたことを思い出してください。11行目のTargetオブジェクトはRPCリクエストを送るサーバを識別するために設定されたもので、15行目で内部的に設定されるTargetはRPCリクエストを処理するエンドポイントを識別するための設定です。
また15行目で設定されたversionパラメータ値1.1がサーバのエンドポイントTestEndpointで設定した値1.2と一致していないことに気づいたかもしれません。Server内部で実施されるバージョンネゴシエーション処理では、クライアントからの要求versionのメジャーバージョン(上の位)が一致しており、かつマイナーバージョン(下の位)がサーバで設定した値以下であれば、互換性がある要求としてRPCリクエストにマッチするメソッドを実行します。このため、クライアントが送信したRPC要求は、サーバで定義したエンドポイントTestEndpointと互換性があると判断され、エンドポイントのコールバックメソッドhogeが実行されます。
それでは、実際にこれらのスクリプトを実行します。次のように2つのターミナルを開き、一つでサーバスクリプト「src/server.py」を実行し、もう一つでクライアントスクリプト「src/client.py」を実行してみてください。
クライアント側からの呼び出しで、サーバ側で定義したエンドポイントのコールバックメソッドが呼び出され、クライアント側で実行結果が受け取れていることが確認できました。
