socketserver --- ネットワークサーバーのフレームワーク

ソースコード: Lib/socketserver.py


socketserver モジュールを使うと、ネットワークサーバを簡単に作成できます。

利用可能な環境: not WASI.

このモジュールは WebAssembly では動作しないか、利用不可です。詳しくは、WebAssembly プラットフォーム を見てください。

基本的な具象サーバクラスが4つあります:

class socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)

クライアントとサーバの間で連続したデータストリームを提供する、インターネットの TCP プロトコルを使います。 bind_and_activate が真の場合、コンストラクターは server_bind() と server_activate() を自動的に呼び出そうとします。 ほかの引数は基底クラス BaseServer に渡されます。

バージョン 3.15 で変更: socketserver.TCPServer のデフォルトのキューサイズが socket.SOMAXCONN になりました。

class socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)

データグラムを使います。 データグラムは個々に独立した情報のパケットで、順序が入れ替わって到着したり、転送中に失われたりすることがあります。 引数は TCPServer と同じです。

class socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate=True)
class socketserver.UnixDatagramServer(server_address, RequestHandlerClass, bind_and_activate=True)

あまり使われないこれらのクラスは TCP や UDP のクラスと似ていますが、Unix ドメインソケットを使います。 Unix 以外のプラットフォームでは利用できません。 引数は TCPServer と同じです。

これらの 4 つのクラスは要求を 同期的に (synchronously) 処理します; 各要求は次の要求を開始する前に完結していなければなりません。同期的な処理は、サーバで大量の計算を必要とする、あるいはクライアントが処理するには時間がかかりすぎるような大量のデータを返す、といった理由によってリクエストに長い時間がかかる状況には向いていません。こうした状況の解決方法は別のプロセスを生成するか、個々の要求を扱うスレッドを生成することです; ForkingMixIn および ThreadingMixIn 配合クラス (mix-in classes) を使えば、非同期的な動作をサポートできます。

サーバの作成にはいくつかのステップが必要です。最初に、 BaseRequestHandler クラスをサブクラス化して要求処理クラス (request hander class) を生成し、その handle() メソッドをオーバーライドしなければなりません; このメソッドは入力される要求を処理します。次に、サーバクラスのうち一つを、サーバのアドレスと要求処理クラスを渡してインスタンス化しなければなりません。サーバを with 文中で使うことが推奨されます。そして、handle_request() または serve_forever() メソッドを呼び出して、一つのまたは多数の要求を処理します。最後に、(with 文を使っていなかったなら) server_close() を呼び出してソケットを閉じます。

ThreadingMixIn から継承してスレッドを利用した接続を行う場合、突然終了する場合のスレッドの動作を明示的に指定する必要があります。 ThreadingMixIn クラスには daemon_threads 属性があり、サーバがスレッドの終了を待ち合わせるかどうかを指定する事ができます。 スレッドの終了を待たずに終了させたい場合は、このフラグを明示的に設定してください。 デフォルトは False で、Pythonは ThreadingMixIn クラスが起動した全てのスレッドが終了するまで実行し続けます。

サーバクラス群は使用するネットワークプロトコルに関わらず、同じ外部メソッドおよび属性を持ちます。

サーバ生成に関するノート

継承図にある五つのクラスのうち四つは四種類の同期サーバを表わしています:

+------------+
| BaseServer |
+------------+
      |
      v
+-----------+        +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+        +------------------+
      |
      v
+-----------+        +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+        +--------------------+

UnixDatagramServer は UDPServer から派生していて、 UnixStreamServer からではないことに注意してください --- IP と Unix サーバの唯一の違いはアドレスファミリーです。

class socketserver.ForkingMixIn
class socketserver.ThreadingMixIn

それぞれのタイプのサーバのフォークしたりスレッド実行したりするバージョンはそれらのミクシン(mix-in)クラスを使って作ることができます。たとえば、ThreadingUDPServer は以下のようにして作られます:

class ThreadingUDPServer(ThreadingMixIn, UDPServer):
    pass

ミクシンクラスは UDPServer で定義されるメソッドをオーバライドするために、最初に来ます。様々な属性を設定することで元になるサーバ機構の振る舞いを変えられます。

ForkingMixIn と以下で説明する Forking クラスは、fork() をサポートする POSIX プラットフォームでのみ利用できます。

block_on_close

ForkingMixIn.server_close は、block_on_close 属性が False でない限り、すべての子プロセスの終了を待ちます。

ThreadingMixIn.server_close は、block_on_close 属性が False でない限り、すべての非デーモンスレッドの終了を待ちます。

max_children

ForkingMixIn で、同時に要求を処理する子プロセスの最大数を指定します。 上限に達すると、新たな要求は子プロセスが 1 つ終了するまで待機します。

daemon_threads

ThreadingMixIn でスレッドの終了を待たないようにするには、ThreadingMixIn.daemon_threads を True に設定してデーモンスレッドを使ってください。

バージョン 3.7 で変更: ForkingMixIn.server_close と ThreadingMixIn.server_close は、すべての子プロセスと非デーモンスレッドが終了するまで待つようになりました。 3.7 より前の動作を選べるよう、クラス属性 ForkingMixIn.block_on_close が追加されました。

class socketserver.ForkingTCPServer
class socketserver.ForkingUDPServer
class socketserver.ThreadingTCPServer
class socketserver.ThreadingUDPServer
class socketserver.ForkingUnixStreamServer
class socketserver.ForkingUnixDatagramServer
class socketserver.ThreadingUnixStreamServer
class socketserver.ThreadingUnixDatagramServer

これらのクラスは、ミクシンクラスを使ってあらかじめ定義されています。

Added in version 3.12: ForkingUnixStreamServer クラスと ForkingUnixDatagramServer クラスが追加されました。

サービスの実装には、 BaseRequestHandler からクラスを派生させてその handle() メソッドを再定義しなければなりません。このようにすれば、サーバクラスと要求処理クラスを結合して様々なバージョンのサービスを実行することができます。要求処理クラスはデータグラムサービスかストリームサービスかで異なることでしょう。この違いは処理サブクラス StreamRequestHandler または DatagramRequestHandler を使うという形で隠蔽できます。

もちろん、まだ頭を使わなければなりません! たとえば、サービスがリクエストによっては書き換えられるようなメモリ上の状態を使うならば、フォークするサーバを使うのは馬鹿げています。というのも子プロセスでの書き換えは親プロセスで保存されている初期状態にも親プロセスから分配される各子プロセスの状態にも届かないからです。この場合、スレッド実行するサーバを使うことはできますが、共有データの一貫性を保つためにロックを使わなければならなくなるでしょう。

一方、全てのデータが外部に(たとえばファイルシステムに)保存される HTTP サーバを作っているのだとすると、同期クラスではどうしても一つの要求が処理されている間サービスが「耳の聞こえない」状態を呈することになります --- この状態はもしクライアントが要求した全てのデータをゆっくり受け取るととても長い時間続きかねません。こういう場合にはサーバをスレッド実行したりフォークすることが適切です。

ある場合には、要求の一部を同期的に処理する一方で、要求データに依って子プロセスをフォークして処理を終了させる、といった方法も適当かもしれません。こうした処理方法は同期サーバを使って要求処理クラスの handle() メソッドの中で自分でフォークするようにして実装することができます。

スレッドも fork() もサポートされていない環境 (あるいはコストが高すぎたり、そのサービスに適さなかったりする場合) で複数の要求を同時に処理する方法もあります。 処理途中の要求を明示的に表で管理し、selectors を使って次に処理する要求 (または新しく届いた要求を処理するかどうか) を決める方法です。 これは、各クライアントが長時間接続し続ける可能性のあるストリームサービスで、スレッドやサブプロセスを使えない場合に特に重要です。

Serverオブジェクト

class socketserver.BaseServer(server_address, RequestHandlerClass)

このモジュールのすべてのサーバオブジェクトのスーパークラスです。 以下のインターフェースを定義しますが、ほとんどのメソッドは実装せず、サブクラスで実装します。 2 つの引数は、それぞれ server_address 属性と RequestHandlerClass 属性に格納されます。

fileno()

サーバが要求待ちを行っているソケットのファイル記述子を整数で返します。この関数は一般的に、同じプロセス中の複数のサーバを監視できるようにするために、 selectors に渡されます。

handle_request()

単一の要求を処理します。この関数は以下のメソッド: get_request() 、 verify_request() 、および process_request() を順番に呼び出します。ハンドラ中でユーザによって提供された handle() が例外を送出した場合、サーバの handle_error() メソッドが呼び出されます。 timeout 秒以内にリクエストが来なかった場合、 handle_timeout() が呼ばれて、 handle_request() が終了します。

serve_forever(poll_interval=0.5)

shutdown() が明示的に要求されるまで、要求を処理します。 poll_interval 秒ごとに終了要求の有無を調べます。 timeout 属性は無視します。 また、service_actions() も呼び出します。 サブクラスやミクシンは、このメソッドを使ってサービス固有の処理を行えます。 例えば ForkingMixIn は service_actions() を使ってゾンビ状態の子プロセスを回収します。

バージョン 3.3 で変更: serve_forever メソッドに service_actions の呼び出しが追加されました。

service_actions()

serve_forever() のループ内で呼び出されます。 サブクラスやミクシンクラスでオーバーライドして、クリーンアップなどのサービス固有の処理を行えます。

Added in version 3.3.

shutdown()

serve_forever() のループに停止を指示し、停止するまで待ちます。 shutdown() は、serve_forever() が別のスレッドで実行されている間に呼び出す必要があります。 そうしないとデッドロックします。

server_close()

サーバをクリーンアップします。上書き出来ます。

address_family

サーバのソケットが属するプロトコルファミリーです。 よく使われるものには socket.AF_INET、socket.AF_INET6、socket.AF_UNIX があります。 IPv6 サーバのクラスが必要な場合は、このモジュールの TCP または UDP サーバクラスを継承し、クラス属性を address_family = AF_INET6 と設定してください。

RequestHandlerClass

ユーザが提供する要求処理クラスです; 要求ごとにこのクラスのインスタンスが生成されます。

server_address

サーバが要求待ちを行うアドレスです。アドレスの形式はプロトコルファミリによって異なります。詳細は socket モジュールを参照してください。インターネットプロトコルでは、この値は例えば ('127.0.0.1', 80) のようにアドレスを与える文字列と整数のポート番号を含むタプルです。

socket

サーバが入力の要求待ちを行うためのソケットオブジェクトです。

サーバクラスは以下のクラス変数をサポートします:

allow_reuse_address

サーバがアドレスの再使用を許すかどうかを示す値です。この値は標準で False で、サブクラスで再使用ポリシを変更するために設定することができます。

request_queue_size

要求待ち行列 (queue) のサイズです。単一の要求を処理するのに長時間かかる場合には、サーバが処理中に届いた要求は最大 request_queue_size 個まで待ち行列に置かれます。待ち行列が一杯になると、それ以降のクライアントからの要求は "接続拒否 (Connection denied)" エラーになります。標準の値は通常 5 ですが、この値はサブクラスで上書きすることができます。

socket_type

サーバが使うソケットの型です; 一般的な2つの値は、 socket.SOCK_STREAM と socket.SOCK_DGRAM です。

timeout

タイムアウト時間(秒)、もしくは、タイムアウトを望まない場合に None 。 handle_request() がこの時間内にリクエストを受信しない場合、 handle_timeout() メソッドが呼ばれます。

TCPServer のような基底クラスのサブクラスで上書きできるサーバメソッドは多数あります; これらのメソッドはサーバオブジェクトの外部のユーザにとっては役にたたないものです。

finish_request(request, client_address)

RequestHandlerClass をインスタンス化し、 handle() メソッドを呼び出して、実際に要求を処理します。

get_request()

ソケットから要求を受理して、クライアントとの通信に使われる 新しい ソケットオブジェクト、およびクライアントのアドレスからなる、2 要素のタプルを返します。

handle_error(request, client_address)

この関数は RequestHandlerClass インスタンスの handle() メソッドが例外を送出した際に呼び出されます。標準の動作では標準エラー出力へトレースバックを出力し、後続する要求を継続して処理します。

バージョン 3.6 で変更: Exception クラスから派生した例外に対してのみ呼び出すようになりました。

handle_timeout()

この関数は timeout 属性が None 以外に設定されて、リクエストがないままタイムアウト秒数が過ぎたときに呼ばれます。 fork型サーバーでのデフォルトの動作は、終了した子プロセスの情報を集めるようになっています。スレッド型サーバーではこのメソッドは何もしません。

process_request(request, client_address)

finish_request() を呼び出して、 RequestHandlerClass のインスタンスを生成します。必要なら、この関数から新たなプロセスかスレッドを生成して要求を処理することができます; その処理は ForkingMixIn または ThreadingMixIn クラスが行います。

server_activate()

サーバを有効にするために、サーバのコンストラクターから呼び出されます。 TCP サーバのデフォルトの動作は、サーバのソケットに対して listen() を呼び出すだけです。 オーバーライドできます。

server_bind()

サーバのコンストラクタによって呼び出され、適切なアドレスにソケットをバインドします。このメソッドは上書きできます。

verify_request(request, client_address)

ブール値を返さなければなりません; 値が True の場合には要求が処理され、 False の場合には要求は拒否されます。サーバへのアクセス制御を実装するためにこの関数を上書きすることができます。標準の実装では常に True を返します。

バージョン 3.6 で変更: context manager プロトコルのサポートが追加されました。コンテキストマネージャを終了することは、 server_close() を呼ぶことと同一です。

要求処理オブジェクト

class socketserver.BaseRequestHandler

すべての要求処理オブジェクトのスーパークラスです。 以下のインターフェースを定義します。 具象の要求処理サブクラスは handle() メソッドを新たに定義する必要があり、ほかのメソッドもオーバーライドできます。 要求ごとにサブクラスの新しいインスタンスを作成します。

setup()

handle() メソッドより前に呼び出され、何らかの必要な初期化処理を行います。標準の実装では何も行いません。

handle()

この関数は、要求に応えるために必要なすべての処理を行わなければなりません。 デフォルトの実装は何もしません。 いくつかのインスタンス属性を利用できます。 要求は request、クライアントのアドレスは client_address に格納されています。 サーバ固有の情報が必要な場合は、server からサーバのインスタンスにアクセスできます。

request の型は、データグラムサービスとストリームサービスで異なります。 ストリームサービスでは request はソケットオブジェクトです。 データグラムサービスでは request はバイト列とソケットのペアです。

finish()

handle() メソッドより後に呼び出され、何らかの必要なクリーンアップ処理を行います。標準の実装では何も行いません。 setup() メソッドが例外を送出した場合、このメソッドは呼び出されません。

request

The new socket.socket object to be used to communicate with the client.

client_address

BaseServer.get_request() が返したクライアントのアドレスです。

server

要求の処理に使う BaseServer オブジェクトです。

class socketserver.StreamRequestHandler
class socketserver.DatagramRequestHandler

これらの BaseRequestHandler のサブクラスは、setup() メソッドと finish() メソッドをオーバーライドし、rfile 属性と wfile 属性を提供します。

rfile

受信した要求を読み取るためのファイルオブジェクトです。 io.BufferedIOBase の読み取り用インターフェースをサポートします。

wfile

応答を書き込むためのファイルオブジェクトです。 io.BufferedIOBase の書き込み用インターフェースをサポートします。

バージョン 3.6 で変更: wfile も io.BufferedIOBase の書き込み用インターフェースをサポートするようになりました。

使用例

socketserver.TCPServer の例

サーバサイドの例です:

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):
    """
    このサーバの要求処理クラス。

    サーバへの接続ごとに 1 回インスタンス化される。
    handle() メソッドをオーバーライドして、
    クライアントとの通信を実装する必要がある。
    """

    def handle(self):
        # self.request はクライアントと接続された TCP ソケット
        pieces = [b'']
        total = 0
        while b'\n' not in pieces[-1] and total < 10_000:
            pieces.append(self.request.recv(2000))
            total += len(pieces[-1])
        self.data = b''.join(pieces)
        print(f"Received from {self.client_address[0]}:")
        print(self.data.decode("utf-8"))
        # 同じデータを大文字に変えて送り返す
        self.request.sendall(self.data.upper())
        # このメソッドから戻るとソケットは閉じられる。

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999

    # localhost のポート 9999 にバインドしてサーバを作成する
    with socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server:
        # サーバを起動する。
        # Ctrl-C でプログラムを中断するまで動き続ける。
        server.serve_forever()

別の、ストリーム(標準のファイル型のインターフェースを利用して通信をシンプルにしたファイルライクオブジェクト)を使うリクエストハンドラクラスの例です:

class MyTCPHandler(socketserver.StreamRequestHandler):

    def handle(self):
        # self.rfile はハンドラーが作成したファイル類似オブジェクト。
        # recv() を直接呼ぶ代わりに、例えば readline() を使える。
        # 送信側による悪用を防ぐため、10000 バイトに制限する。
        self.data = self.rfile.readline(10000).rstrip()
        print(f"{self.client_address[0]} wrote:")
        print(self.data.decode("utf-8"))
        # 同様に、self.wfile はクライアントへの書き込みに使う
        # ファイル類似オブジェクト。
        self.wfile.write(self.data.upper())

2 番目のハンドラーでは、readline() が改行文字に到達するまで recv() を繰り返し呼び出す点が異なります。 最初のハンドラーでは、改行に到達するまでデータを蓄積するために、自分で recv() のループを書く必要がありました。 ループを使わずに recv() を 1 回だけ呼び出していたら、その時点までにクライアントから受信できたデータだけが返されていたでしょう。 TCP はストリームに基づいています。 データは送信した順に届きますが、クライアントでの send() や sendall() の呼び出し回数と、それを受信するためにサーバで必要な recv() の呼び出し回数には対応関係がありません。

クライアントサイドの例:

import socket
import sys

HOST, PORT = "localhost", 9999
data = " ".join(sys.argv[1:])

# ソケットを作成する (SOCK_STREAM は TCP ソケットを表す)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    # サーバに接続してデータを送信する
    sock.connect((HOST, PORT))
    sock.sendall(bytes(data, "utf-8"))
    sock.sendall(b"\n")

    # サーバからデータを受信して終了する
    received = str(sock.recv(1024), "utf-8")

print("Sent:    ", data)
print("Received:", received)

この例の出力は次のようになります:

サーバー:

$ python TCPServer.py
127.0.0.1 wrote:
b'hello world with TCP'
127.0.0.1 wrote:
b'python is nice'

クライアント:

$ python TCPClient.py hello world with TCP
Sent:     hello world with TCP
Received: HELLO WORLD WITH TCP
$ python TCPClient.py python is nice
Sent:     python is nice
Received: PYTHON IS NICE

socketserver.UDPServer の例

サーバサイドの例です:

import socketserver

class MyUDPHandler(socketserver.BaseRequestHandler):
    """
    This class works similar to the TCP handler class, except that
    self.request consists of a pair of data and client socket, and since
    there is no connection the client address must be given explicitly
    when sending data back via sendto().
    """

    def handle(self):
        data = self.request[0].strip()
        socket = self.request[1]
        print(f"{self.client_address[0]} wrote:")
        print(data)
        socket.sendto(data.upper(), self.client_address)

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    with socketserver.UDPServer((HOST, PORT), MyUDPHandler) as server:
        server.serve_forever()

クライアントサイドの例:

import socket
import sys

HOST, PORT = "localhost", 9999
data = " ".join(sys.argv[1:])

# SOCK_DGRAM は UDP ソケットに使うソケットの種類
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# UDP には接続がないため、connect() は呼び出さない。
# 代わりに sendto() で受信側へ直接データを送信する。
sock.sendto(bytes(data + "\n", "utf-8"), (HOST, PORT))
received = str(sock.recv(1024), "utf-8")

print("Sent:    ", data)
print("Received:", received)

この例の出力は、TCPサーバーの例と全く同じようになります。

非同期処理の Mix-in

複数の接続を非同期に処理するハンドラを作るには、 ThreadingMixIn か ForkingMixIn クラスを利用します。

ThreadingMixIn クラスの利用例:

import socket
import threading
import socketserver

class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):

    def handle(self):
        data = str(self.request.recv(1024), 'ascii')
        cur_thread = threading.current_thread()
        response = bytes("{}: {}".format(cur_thread.name, data), 'ascii')
        self.request.sendall(response)

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass

def client(ip, port, message):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.connect((ip, port))
        sock.sendall(bytes(message, 'ascii'))
        response = str(sock.recv(1024), 'ascii')
        print("Received: {}".format(response))

if __name__ == "__main__":
    # ポート 0 は、未使用の任意のポートを選ぶことを意味する
    HOST, PORT = "localhost", 0

    server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
    with server:
        ip, port = server.server_address

        # サーバを実行するスレッドを開始する。
        # このスレッドが要求ごとにさらに 1 つスレッドを開始する。
        server_thread = threading.Thread(target=server.serve_forever)
        # メインスレッドの終了時にサーバスレッドも終了させる
        server_thread.daemon = True
        server_thread.start()
        print("Server loop running in thread:", server_thread.name)

        client(ip, port, "Hello World 1")
        client(ip, port, "Hello World 2")
        client(ip, port, "Hello World 3")

        server.shutdown()

この例の出力は次のようになります:

$ python ThreadedTCPServer.py
Server loop running in thread: Thread-1
Received: Thread-2: Hello World 1
Received: Thread-3: Hello World 2
Received: Thread-4: Hello World 3

ForkingMixIn クラスは同じように利用することができます。この場合、サーバーはリクエスト毎に新しいプロセスを作成します。fork() をサポートする POSIX プラットフォームでのみ利用可能です。