SocketServer – ネットワークサービスを作成する

目的:ネットワークサービスを作成する
利用できるバージョン:1.4

SocketServer モジュールはネットワークサービスを作成するフレームワークです。TCP, UDP, Unix ストリームや Unix データグラムで(受け取ったリクエストの処理が完了するまでサーバのリクエストハンドラがブロックする)同期ネットワークリクエストを扱うクラスを定義します。さらに状況に応じて、それぞれのリクエストにスレッドかプロセスを使うように簡単にサーバを移行する mix-in クラスも提供します。

1つのリクエストを処理するレスポンス機能はサーバクラスとリクエストハンドラクラスに分割されます。サーバは通信関連の処理(ソケットを listen する、コネクションを accept する等)を扱います。リクエストハンドラは “プロトコル” 関連の処理(入力データを解釈して処理する、クライアントにデータを送り返す)を扱います。このレスポンス機能の分割により、多くのケースにおいて、何の変更もなく既存のサーバクラスの1つをシンプルに使用できて、独自プロトコルと連携するようにリクエストハンドラクラスを提供します。

サーバタイプ

SocketServer モジュールには5つのサーバクラスが定義されています。 BaseServer は API を定義しますが、実際にインスタンス化して直接使うことはありません。 TCPServer は通信のための TCP/IP ソケットを使用します。 UDPServer はデータグラムソケットを使用します。 UnixStreamServerUnixDatagramServer は Unix フラットフォームでのみ利用可能な Unix ドメインソケットを使用します。

サーバオブジェクト

サーバを構築するには、リクエストを受け付けるアドレスとリクエストハンドラ クラス (インスタンスではない) を引数として渡してください。アドレスのフォーマットはサーバタイプと使用されるソケットファミリーに依存します。詳細は socket モジュールのドキュメントを参照してください。

サーバオブジェクトをインスタンス化すると、リクエストを処理するために handle_request()serve_forever() のどちらか一方を使用します。 serve_forever() メソッドは単純な無限ループで handle_request() を呼び出します。そのため、複数サーバの複数ソケットを監視するために別のイベントループか select() を使用してサーバを統合する必要があるなら、独自に handle_request() を呼び出すことができます。詳細はこの後で紹介するサンプルを見てください。

サーバを実装する

サーバを作成するなら通常は既存クラスの1つを再利用して、カスタムリクエストハンドラクラスを提供できます。もしも要望にあわないなら、 BaseServer をサブクラス化して利用可能なメソッドをオーバーライドします。

  • verify_request(request, client_address) - リクエストを処理すれば True を、無視すれば False を返します。例えば、サーバへのアクセスから特定のクライアントをブロックしたい場合、IP アドレスの範囲を指定してリクエストを拒否します。
  • process_request(request, client_address) - 通常は実際の処理を行う finish_request() を呼び出すだけです。mix-in クラスが行うように、スレッドかプロセスかを分割して作成することもできます。(後述)
  • finish_request(request, client_address) - 与えられたクラスからサーバのコンストラクタに対してリクエストハンドラインスタンスを作成します。リクエストを処理するにはリクエストハンドラで handle() を呼び出してください。

リクエストハンドラ

リクエストハンドラは大半の入力リクエストの受信処理を行い、どんなアクションを取るかを決定します。ハンドラはソケット層のトップ(例えば、HTTP や XML-RPC)で “プロトコル” を実装する責任を持ちます。リクエストハンドラは入力データチャンネルからリクエストを読み込み、その処理を行い、レスポンスを書き込みます。次の3つのメソッドが利用可能でオーバーライドされます。

  • setup() - リクエストに対するリクエストハンドラを準備します。例えば、 StreamRequestHandler では setup() メソッドはソケットに対して読み書きするためにファイルのようなオブジェクトを作成します。
  • handle() - リクエストに対して実際の処理を行います。入力リクエストを解析して、処理を行い、レスポンスを送ります。
  • finish() - setup() で作成したものをクリーンアップします。

多くのケースでは、シンプルに handle() を提供します。

Echo サンプル

TCP コネクションを受け付けて、クライアントから送られたデータをそのまま返すシンプルなサーバ/リクエストハンドラを見てみましょう。サンプルコードで実際に必要なメソッドは EchoRequestHandler.handle() のみですが、サンプルプログラムの出力がどの順番で呼び出されるかを解説するために logging を追加するように、前節で説明した全てのメソッドをオーバーライドしています。

あと残っている処理はサーバを作成して、1つのスレッド内で実行し、データがそのまま送り返されるようにどのメソッドが呼び出されるかを理解するためにそのサーバへ接続する、シンプルなプログラムです。

import logging
import sys
import SocketServer

logging.basicConfig(level=logging.DEBUG,
                    format='%(name)s: %(message)s',
                    )

class EchoRequestHandler(SocketServer.BaseRequestHandler):
    
    def __init__(self, request, client_address, server):
        self.logger = logging.getLogger('EchoRequestHandler')
        self.logger.debug('__init__')
        SocketServer.BaseRequestHandler.__init__(self, request, client_address, server)
        return

    def setup(self):
        self.logger.debug('setup')
        return SocketServer.BaseRequestHandler.setup(self)

    def handle(self):
        self.logger.debug('handle')

        # クライアントへ echo back する
        data = self.request.recv(1024)
        self.logger.debug('recv()->"%s"', data)
        self.request.send(data)
        return

    def finish(self):
        self.logger.debug('finish')
        return SocketServer.BaseRequestHandler.finish(self)

class EchoServer(SocketServer.TCPServer):
    
    def __init__(self, server_address, handler_class=EchoRequestHandler):
        self.logger = logging.getLogger('EchoServer')
        self.logger.debug('__init__')
        SocketServer.TCPServer.__init__(self, server_address, handler_class)
        return

    def server_activate(self):
        self.logger.debug('server_activate')
        SocketServer.TCPServer.server_activate(self)
        return

    def serve_forever(self):
        self.logger.debug('waiting for request')
        self.logger.info('Handling requests, press <Ctrl-C> to quit')
        while True:
            self.handle_request()
        return

    def handle_request(self):
        self.logger.debug('handle_request')
        return SocketServer.TCPServer.handle_request(self)

    def verify_request(self, request, client_address):
        self.logger.debug('verify_request(%s, %s)', request, client_address)
        return SocketServer.TCPServer.verify_request(self, request, client_address)

    def process_request(self, request, client_address):
        self.logger.debug('process_request(%s, %s)', request, client_address)
        return SocketServer.TCPServer.process_request(self, request, client_address)

    def server_close(self):
        self.logger.debug('server_close')
        return SocketServer.TCPServer.server_close(self)

    def finish_request(self, request, client_address):
        self.logger.debug('finish_request(%s, %s)', request, client_address)
        return SocketServer.TCPServer.finish_request(self, request, client_address)

    def close_request(self, request_address):
        self.logger.debug('close_request(%s)', request_address)
        return SocketServer.TCPServer.close_request(self, request_address)

if __name__ == '__main__':
    import socket
    import threading

    address = ('localhost', 0) # カーネルにポート番号を割り当てさせる
    server = EchoServer(address, EchoRequestHandler)
    ip, port = server.server_address # 与えられたポート番号を調べる

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True) # 終了時にハングアップしない
    t.start()

    logger = logging.getLogger('client')
    logger.info('Server on %s:%s', ip, port)

    # サーバへ接続する
    logger.debug('creating socket')
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    logger.debug('connecting to server')
    s.connect((ip, port))

    # データを送る
    message = 'Hello, world'
    logger.debug('sending data: "%s"', message)
    len_sent = s.send(message)

    # レスポンスを受けとる
    logger.debug('waiting for response')
    response = s.recv(len_sent)
    logger.debug('response from server: "%s"', response)

    # クリーンアップ
    logger.debug('closing socket')
    s.close()
    logger.debug('done')
    server.socket.close()

このプログラムの出力は次のようになります。

$ python SocketServer_echo.py
EchoServer: __init__
EchoServer: server_activate
EchoServer: waiting for request
client: Server on 127.0.0.1:59613
EchoServer: Handling requests, press <Ctrl-C> to quit
client: creating socket
EchoServer: handle_request
client: connecting to server
client: sending data: "Hello, world"
client: waiting for response
EchoServer: verify_request(<socket._socketobject object at 0x1004ca1a0>, ('127.0.0.1', 59614))
EchoServer: process_request(<socket._socketobject object at 0x1004ca1a0>, ('127.0.0.1', 59614))
EchoServer: finish_request(<socket._socketobject object at 0x1004ca1a0>, ('127.0.0.1', 59614))
EchoRequestHandler: __init__
EchoRequestHandler: setup
EchoRequestHandler: handle
EchoRequestHandler: recv()->"Hello, world"
EchoRequestHandler: finish
client: response from server: "Hello, world"
EchoServer: close_request(<socket._socketobject object at 0x1004ca1a0>)
client: closing socket
EchoServer: handle_request
client: done

使用されるポート番号は実行する度に変わります。カーネルが自動的に利用可能なポート番号を割り当てます。実行するときにサーバに特定のポート番号を指定したいなら、アドレスのタプルに 0 ではなく、その番号を指定してください。

ここに logging を取り除いて同じことをするシンプルなバージョンがあります。

import SocketServer

class EchoRequestHandler(SocketServer.BaseRequestHandler):

    def handle(self):
        # クライアントへ echo back する
        data = self.request.recv(1024)
        self.request.send(data)
        return

if __name__ == '__main__':
    import socket
    import threading

    address = ('localhost', 0) # カーネルにポート番号を割り当てさせる
    server = SocketServer.TCPServer(address, EchoRequestHandler)
    ip, port = server.server_address # 与えられたポート番号を調べる

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True) # 終了時にハングアップしない
    t.start()

    # サーバへ接続する
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # データを送る
    message = 'Hello, world'
    print 'Sending : "%s"' % message
    len_sent = s.send(message)

    # レスポンスを受けとる
    response = s.recv(len_sent)
    print 'Received: "%s"' % response

    # クリーンアップ
    s.close()
    server.socket.close()

このケースでは TCPServer が全てのサーバへの要求を扱うので特別なサーバクラスは必要ありません。

$ python SocketServer_echo_simple.py
Sending : "Hello, world"
Received: "Hello, world"

スレッドとフォーク

サーバに対してスレッドかフォークの機能を追加するには、サーバのクラス階層で適切な mix-in を含めることでシンプルに実現できます。mix-in クラスはリクエストを扱うときに新たなスレッドかプロセスを生成するために process_request() をオーバーライドします。そして、生成されたスレッドかプロセスでその処理が行われます。

スレッドで扱いたいなら ThreadingMixIn を使用してください。

import threading
import SocketServer

class ThreadedEchoRequestHandler(SocketServer.BaseRequestHandler):

    def handle(self):
        # クライアントへ echo back する
        data = self.request.recv(1024)
        cur_thread = threading.currentThread()
        response = '%s: %s' % (cur_thread.getName(), data)
        self.request.send(response)
        return

class ThreadedEchoServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass

if __name__ == '__main__':
    import socket
    import threading

    address = ('localhost', 0) # カーネルにポート番号を割り当てさせる
    server = ThreadedEchoServer(address, ThreadedEchoRequestHandler)
    ip, port = server.server_address # 与えられたポート番号を調べる

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True) # 終了時にハングアップしない
    t.start()
    print 'Server loop running in thread:', t.getName()

    # サーバへ接続する
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # データを送る
    message = 'Hello, world'
    print 'Sending : "%s"' % message
    len_sent = s.send(message)

    # レスポンスを受けとる
    response = s.recv(1024)
    print 'Received: "%s"' % response

    # クリーンアップ
    s.close()
    server.socket.close()

サーバからのレスポンスにはリクエストを扱ったスレッドの ID を含みます。

$ python SocketServer_threaded.py
Server loop running in thread: Thread-1
Sending : "Hello, world"
Received: "Thread-2: Hello, world"

プロセスを分割したいなら ForkingMixIn を使用してください。

import os
import SocketServer

class ForkingEchoRequestHandler(SocketServer.BaseRequestHandler):

    def handle(self):
        # クライアントへ echo back する
        data = self.request.recv(1024)
        cur_pid = os.getpid()
        response = '%s: %s' % (cur_pid, data)
        self.request.send(response)
        return

class ForkingEchoServer(SocketServer.ForkingMixIn, SocketServer.TCPServer):
    pass

if __name__ == '__main__':
    import socket
    import threading

    address = ('localhost', 0) # カーネルにポート番号を割り当てさせる
    server = ForkingEchoServer(address, ForkingEchoRequestHandler)
    ip, port = server.server_address # 与えられたポート番号を調べる

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True) # 終了時にハングアップしない
    t.start()
    print 'Server loop running in process:', os.getpid()

    # サーバへ接続する
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # データを送る
    message = 'Hello, world'
    print 'Sending : "%s"' % message
    len_sent = s.send(message)

    # レスポンスを受けとる
    response = s.recv(1024)
    print 'Received: "%s"' % response

    # クリーンアップ
    s.close()
    server.socket.close()

このケースでは、子プロセスのプロセス ID がサーバからのレスポンスに含められます。

$ python SocketServer_forking.py
Server loop running in process: 71832
Sending : "Hello, world"
Received: "71833: Hello, world"
Exception in thread Thread-1 (most likely raised during interpreter shutdown):
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.py", line 552, in __bootstrap_inner
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.py", line 505, in run
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/SocketServer.py", line 230, in serve_forever

See also

SocketServer
本モジュールの標準ライブラリドキュメント
asyncore
リクエスト処理中にブロッキングしない非同期サービスを作成するには asyncore を使用してください
SimpleXMLRPCServer
SocketServer で構築する XML-RPC サーバ
Bookmark and Share