SimpleXMLRPCServer – XML-RPC サーバを実装する

目的:XML-RPC サーバを実装する
利用できるバージョン:2.2 以上

SimpleXMLRPCServer モジュールは XML-RPC プロトコルを使用してクロスプラットホーム、言語非依存なサーバを作成するためのクラスを提供します。多くのクライアントライブラリが多言語で存在するので RPC スタイルのサービスを構築するために XML-RPC を選択することは簡単な方法です。

Note

本稿で紹介する全てのサンプルはデモサーバとやり取りするクライアントモジュールを含めて提供します。もし、そのコードをダウンロードしてサンプルプログラムを実行したいなら、2つのシェルウィンドウを起動して、1つはサーバ、もう1つはクライアントにすると良いでしょう。

シンプルサーバ

このシンプルサーバのサンプルはディレクトリ名を受け取ってコンテンツを返す1つの関数を提供します。先ず SimpleXMLRPCServer インスタンスを作成して、入力となるリクエスト(このケースでは ‘localhost’ の9000番ポート)を受信するサーバ/ポートを指定します。それから、そのサービスの一部となる関数を定義して、サーバへその関数の呼び出し方法を知らせるために登録を行います。最後にリクエストを受信するためにそのサーバを無限ループで実行してレスポンスを返します。

from SimpleXMLRPCServer import SimpleXMLRPCServer
import logging
import os

# ロギングをセットアップ
logging.basicConfig(level=logging.DEBUG)

server = SimpleXMLRPCServer(('localhost', 9000), logRequests=True)

# 関数を公開する
def list_contents(dir_name):
    logging.debug('list_contents(%s)', dir_name)
    return os.listdir(dir_name)
server.register_function(list_contents)

try:
    print 'Use Control-C to exit'
    server.serve_forever()
except KeyboardInterrupt:
    print 'Exiting'

サーバは xmlrpclib を使用して http://localhost:9000 の URL にアクセスされます。このクライアントコードは Python から list_contents() サービスを呼び出す方法を紹介します。

import xmlrpclib

proxy = xmlrpclib.ServerProxy('http://localhost:9000')
print proxy.list_contents('/tmp')

そのベース URL を使用してサーバに対する ServerProxy へ単純に接続して、プロキシで直接メソッドを呼び出していることに注意してください。プロキシで実行される各メソッドはサーバへのリクエストに変換されます。その引数は XML でフォーマットされてサーバへ POST されます。サーバはその XML をアンパックして、クライアントから実行されたメソッド名からどの関数を呼び出すかを見つけ出します。その引数は関数へ渡されて、その返り値は XML に変換されてクライアントへ返されます。

サーバを起動します。

$ python SimpleXMLRPCServer_function.py
Use Control-C to exit

別のウィンドウでクライアントを実行すると /tmp ディレクトリの中身を表示します。

$ python SimpleXMLRPCServer_function_client.py
['.s.PGSQL.5432', '.s.PGSQL.5432.lock', '.X0-lock', '.X11-unix', 'ccc_exclude.1mkahl',
'ccc_exclude.BKG3gb', 'ccc_exclude.M5jrgo', 'ccc_exclude.SPecwL', 'com.hp.launchport', 'emacs527',
'hsperfdata_dhellmann', 'launch-8hGHUp', 'launch-RQnlcc', 'launch-trsdly', 'launchd-242.T5UzTy',
'var_backups']

そのリクエストに対する処理が完了した後でサーバのウィンドウにログが出力されます。

$ python SimpleXMLRPCServer_function.py
Use Control-C to exit
DEBUG:root:list_contents(/tmp)
localhost - - [29/Jun/2008 09:32:07] "POST /RPC2 HTTP/1.0" 200 -

ログ出力の最初の行は list_contents() の内部で logging.debug() を呼び出して出力されます。2番目の行は logRequestsTrue なのでサーバのログが出力されます。

関数の別名

あなたのモジュール又はライブラリの内部で使用している関数名を外部 API で使用したくないときがあります。例えば、設定ファイルから動的にそのサービス API を構築して、プラットホームに特化した実装を読み込んだり、実際の関数をテスト用のスタブに置き換えたりする必要がある場合もあります。もし別の名前で関数を登録したいなら、次のように register_function() の2番目の引数にその名前を渡してください。

from SimpleXMLRPCServer import SimpleXMLRPCServer
import os

server = SimpleXMLRPCServer(('localhost', 9000))

# 別名で関数を公開する
def list_contents(dir_name):
    return os.listdir(dir_name)
server.register_function(list_contents, 'dir')

try:
    print 'Use Control-C to exit'
    server.serve_forever()
except KeyboardInterrupt:
    print 'Exiting'

今、クライアントは list_contents() の代わりに dir() という名前を使用します。

import xmlrpclib

proxy = xmlrpclib.ServerProxy('http://localhost:9000')
print 'dir():', proxy.dir('/tmp')
print 'list_contents():', proxy.list_contents('/tmp')

サーバには list_contents() という名前でハンドラが登録されていないので list_contents() を呼び出すとエラーになります。

$ python SimpleXMLRPCServer_alternate_name_client.py
dir(): ['.s.PGSQL.5432', '.s.PGSQL.5432.lock', '.X0-lock', '.X11-unix', 'ccc_exclude.1mkahl', 'ccc_exclude.BKG3gb', 'ccc_exclude.M5jrgo', 'ccc_exclude.SPecwL', 'com.hp.launchport', 'emacs527', 'hsperfdata_dhellmann', 'launch-8hGHUp', 'launch-RQnlcc', 'launch-trsdly', 'launchd-242.T5UzTy', 'temp_textmate.V6YKzm', 'var_backups']
list_contents():
Traceback (most recent call last):
  File "/Users/dhellmann/Documents/PyMOTW/in_progress/SimpleXMLRPCServer/SimpleXMLRPCServer_alternate_name_client.py", line 15, in <module>
    print 'list_contents():', proxy.list_contents('/tmp')
  File "/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/xmlrpclib.py", line 1147, in __call__
    return self.__send(self.__name, args)
  File "/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/xmlrpclib.py", line 1437, in __request
    verbose=self.__verbose
  File "/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/xmlrpclib.py", line 1201, in request
    return self._parse_response(h.getfile(), sock)
  File "/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/xmlrpclib.py", line 1340, in _parse_response
    return u.close()
  File "/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/xmlrpclib.py", line 787, in close
    raise Fault(**self._stack[0])
xmlrpclib.Fault: <Fault 1: '<type \'exceptions.Exception\'>:method "list_contents" is not supported'>

ドット名

個々の関数は Python の識別子のために通常は適切ではない名前で登録することができます。例えば、そのサービスの名前空間を分割するために名前に ‘.’ を含めることができます。この例では “ディレクトリ” サービスに “create” と “remove” の関数呼び出しを追加して拡張します。同一サーバ上で違う接頭辞で他のサービスを提供できるように、全ての関数を “dir.” を接頭辞にして登録します。この例では他にも関数によって None を返しています。そのため None の値から nil の値へ変換するためにサーバへ教える必要があります( XML-RPC Extensions を参照)。

from SimpleXMLRPCServer import SimpleXMLRPCServer
import os

server = SimpleXMLRPCServer(('localhost', 9000), allow_none=True)

server.register_function(os.listdir, 'dir.list')
server.register_function(os.mkdir, 'dir.create')
server.register_function(os.rmdir, 'dir.remove')

try:
    print 'Use Control-C to exit'
    server.serve_forever()
except KeyboardInterrupt:
    print 'Exiting'

クライアントからそのサービスの関数を呼び出すために単純にドット名で参照してください。

import xmlrpclib

proxy = xmlrpclib.ServerProxy('http://localhost:9000')
print 'BEFORE       :', 'EXAMPLE' in proxy.dir.list('/tmp')
print 'CREATE       :', proxy.dir.create('/tmp/EXAMPLE')
print 'SHOULD EXIST :', 'EXAMPLE' in proxy.dir.list('/tmp')
print 'REMOVE       :', proxy.dir.remove('/tmp/EXAMPLE')
print 'AFTER        :', 'EXAMPLE' in proxy.dir.list('/tmp')

そして(システム上に /tmp/EXAMPLE ファイルがない場合)、クライアントのサンプルスクリプトの出力は次のようになります。

$ python SimpleXMLRPCServer_dotted_name_client.py
BEFORE       : False
CREATE       : None
SHOULD EXIST : True
REMOVE       : None
AFTER        : False

任意の名前

利便性は低いですが、無効な属性名を登録する機能が潜在的におもしろいです。この例のサービスは “multiply args” という名前で関数を登録します。

from SimpleXMLRPCServer import SimpleXMLRPCServer

server = SimpleXMLRPCServer(('localhost', 9000))

def my_function(a, b):
    return a * b
server.register_function(my_function, 'multiply args')

try:
    print 'Use Control-C to exit'
    server.serve_forever()
except KeyboardInterrupt:
    print 'Exiting'

登録された名前がスペースを含むので、プロキシからドット表記で直接その名前にアクセスすることはできません。 しかし getattr() を使用することでアクセスできます。

import xmlrpclib

proxy = xmlrpclib.ServerProxy('http://localhost:9000')
print getattr(proxy, 'multiply args')(5, 5)

アクセスはできますが、このような名前でサービスを作成しないようにしてください。この例はそれが良いアイディアだと言うわけではなく、あなたが任意の名前を持つ既存のサービスに遭遇して無効な名前で呼び出す必要があるときの方法を提供します。

$ python SimpleXMLRPCServer_arbitrary_name_client.py
25

オブジェクトのメソッドを公開する

前半のセクションでは優れた命名規則や名前空間を使用して API を開発するためのテクニックについて説明しました。API に名前空間を組み込む他の方法として、クラスのインスタンスを使用してそのメソッドを公開する方法があります。最初のサンプルを1つのメソッドを持つインスタンスで再作成することができます。

from SimpleXMLRPCServer import SimpleXMLRPCServer
import os
import inspect

server = SimpleXMLRPCServer(('localhost', 9000), logRequests=True)

class DirectoryService:
    def list(self, dir_name):
        return os.listdir(dir_name)

server.register_instance(DirectoryService())

try:
    print 'Use Control-C to exit'
    server.serve_forever()
except KeyboardInterrupt:
    print 'Exiting'

クライアントは直接そのメソッドを呼び出すことができます。

import xmlrpclib

proxy = xmlrpclib.ServerProxy('http://localhost:9000')
print proxy.list('/tmp')

受け取った出力は次のようになります。

$ python SimpleXMLRPCServer_instance_client.py
['.s.PGSQL.5432', '.s.PGSQL.5432.lock', '.X0-lock', '.X11-unix', 'ccc_exclude.1mkahl',
'ccc_exclude.BKG3gb', 'ccc_exclude.M5jrgo', 'ccc_exclude.SPecwL', 'com.hp.launchport',
'emacs527', 'hsperfdata_dhellmann', 'launch-8hGHUp', 'launch-RQnlcc', 'launch-trsdly',
'launchd-242.T5UzTy', 'temp_textmate.XNiIdy', 'var_backups']

この方法はそのサービスから “dir.” の接頭辞をなくしましたが、そのためにクライアントから実行されるサービスツリーをセットアップするためのクラスを定義します。

from SimpleXMLRPCServer import SimpleXMLRPCServer
import os
import inspect

server = SimpleXMLRPCServer(('localhost', 9000), logRequests=True)

class ServiceRoot:
    pass

class DirectoryService:
    def list(self, dir_name):
        return os.listdir(dir_name)

root = ServiceRoot()
root.dir = DirectoryService()

server.register_instance(root, allow_dotted_names=True)

try:
    print 'Use Control-C to exit'
    server.serve_forever()
except KeyboardInterrupt:
    print 'Exiting'

allow_dotted_names を有効にした ServiceRoot インスタンスを登録することで getattr() でその名前のメソッドを見つけるためのリクエストを受け取ったとき、そのオブジェクトのツリーを辿る権限をサーバへ与えます。

import xmlrpclib

proxy = xmlrpclib.ServerProxy('http://localhost:9000')
print proxy.dir.list('/tmp')
$ python SimpleXMLRPCServer_instance_dotted_names_client.py
['.s.PGSQL.5432', '.s.PGSQL.5432.lock', '.X0-lock', '.X11-unix', 'ccc_exclude.1mkahl', 'ccc_exclude.BKG3gb', 'ccc_exclude.M5jrgo', 'ccc_exclude.SPecwL', 'com.hp.launchport', 'emacs527', 'hsperfdata_dhellmann', 'launch-8hGHUp', 'launch-RQnlcc', 'launch-trsdly', 'launchd-242.T5UzTy', 'temp_textmate.adghkQ', 'var_backups']

呼び出しをディスパッチする

デフォルトでは register_instance() は ‘_‘ で始まらない名前を持つインスタンスの全ての呼び出し可能な属性を見つけます。そして見つけた名前をサービスに登録します。もしメソッドの公開を慎重に行いたいなら、独自のディスパッチロジックを提供することができます。例えば、

from SimpleXMLRPCServer import SimpleXMLRPCServer
import os
import inspect

server = SimpleXMLRPCServer(('localhost', 9000), logRequests=True)

def expose(f):
    "Decorator to set exposed flag on a function."
    f.exposed = True
    return f

def is_exposed(f):
    "Test whether another function should be publicly exposed."
    return getattr(f, 'exposed', False)

class MyService:
    PREFIX = 'prefix'

    def _dispatch(self, method, params):
        # メソッド名から接頭辞を削除する
        if not method.startswith(self.PREFIX + '.'):
            raise Exception('method "%s" is not supported' % method)
        
        method_name = method.partition('.')[2]
        func = getattr(self, method_name)            
        if not is_exposed(func):
            raise Exception('method "%s" is not supported' % method)
        
        return func(*params)

    @expose
    def public(self):
        return 'This is public'
        
    def private(self):
        return 'This is private'

server.register_instance(MyService())

try:
    print 'Use Control-C to exit'
    server.serve_forever()
except KeyboardInterrupt:
    print 'Exiting'

MyServicepublic() メソッドは XML-RPC サービスへ公開する関数として expose としてマークします。一方 private() にはありません。クライアントが MyService にある関数へアクセスしようとするとき _dispatch() メソッドが実行されます。 _dispatch() メソッドは接頭辞の使用を強制します(このケースでは “prefix.” ですが、他の文字列も使用することができます)。それから exposed という属性が True である関数を要求します。exposed フラグはデコレータでセットすると便利です。

クライアントからの呼び出しは次のようになります。

import xmlrpclib

proxy = xmlrpclib.ServerProxy('http://localhost:9000')
print 'public():', proxy.prefix.public()
try:
    print 'private():', proxy.prefix.private()
except Exception, err:
    print 'ERROR:', err
try:
    print 'public() without prefix:', proxy.public()
except Exception, err:
    print 'ERROR:', err

結果の出力は予想した通りトラップしたエラーメッセージが表示されます。

$ python SimpleXMLRPCServer_instance_with_prefix_client.py
public(): This is public
private(): ERROR: <Fault 1: '<type \'exceptions.Exception\'>:method "prefix.private" is not supported'>
public() without prefix: ERROR: <Fault 1: '<type \'exceptions.Exception\'>:method "public" is not supported'>

SimpleXMLRPCServer から直接サブクラス化することも含めて、ディスパッチの仕組みをオーバーライドするために複数の方法があります。より詳細については SimpleXMLRPCServer の docstring を確認してください。

イントロスペクション API

多くのネットワークサービスと同様に、どんなメソッドがサポートされているかとその使用方法を尋ねるために XML-RPC サーバへクエリすることができます。 SimpleXMLRPCServer はこのイントロスペクションを実行する公開メソッドのセットを提供します。デフォルトではその機能は無効になっていますが、 register_introspection_functions() で有効にすることができます。サービスを行うクラスで _listMethods()_methodHelp() を定義することで system.listMethods()system.methodHelp() を明示的にサポートするように追加することができます。例えば、

from SimpleXMLRPCServer import SimpleXMLRPCServer, list_public_methods
import os
import inspect

server = SimpleXMLRPCServer(('localhost', 9000), logRequests=True)
server.register_introspection_functions()

class DirectoryService:
    
    def _listMethods(self):
        return list_public_methods(self)

    def _methodHelp(self, method):
        f = getattr(self, method)
        return inspect.getdoc(f)
    
    def list(self, dir_name):
        """list(dir_name) => [<filenames>]
        
        Returns a list containing the contents of the named directory.
        """
        return os.listdir(dir_name)

server.register_instance(DirectoryService())

try:
    print 'Use Control-C to exit'
    server.serve_forever()
except KeyboardInterrupt:
    print 'Exiting'

このケースでは、便利な関数 list_public_methods() が呼び出し可能な ‘_‘ で始まらない属性名を返すためにインスタンスを調べます。あなた好みにどんなルールでも _listMethods() を再定義することで適用することができます。同様にこの基本サンプルの _methodHelp() はその関数の docstring を返しますが、他のソースからのヘルプを返すようにはなっていません。

import xmlrpclib

proxy = xmlrpclib.ServerProxy('http://localhost:9000')
for method_name in proxy.system.listMethods():
    print '=' * 60
    print method_name
    print '-' * 60
    print proxy.system.methodHelp(method_name)
    print

その system メソッドが実行結果に含まれることに注意してください。

$ python SimpleXMLRPCServer_introspection_client.py
============================================================
list
------------------------------------------------------------
list(dir_name) => [<filenames>]

Returns a list containing the contents of the named directory.

============================================================
system.listMethods
------------------------------------------------------------
system.listMethods() => ['add', 'subtract', 'multiple']

Returns a list of the methods supported by the server.

============================================================
system.methodHelp
------------------------------------------------------------
system.methodHelp('add') => "Adds two integers together"

Returns a string containing documentation for the specified method.

============================================================
system.methodSignature
------------------------------------------------------------
system.methodSignature('add') => [double, int, int]

Returns a list describing the signature of the method. In the
above example, the add method takes two integers as arguments
and returns a double result.

This server does NOT support system.methodSignature.

See also

SimpleXMLRPCServer
本モジュールの標準ライブラリドキュメント
XML-RPC 入門
様々な言語でクライアントとサーバを実装するために XML-RPC の使用方法を説明します
XML-RPC Extensions
XML-RPC プロトコルの機能拡張に特化した内容です
xmlrpclib
XML-RPC クライアントライブラリ
Bookmark and Share