CLOVER🍀

That was when it all began.

PythonのTCPServer/HTTPServerをマルチスレッドで使う

これは、なにをしたくて書いたもの?

Pythonには、TCPでサーバーを書けるTCPServer、HTTPでサーバーを書けるHTTPServerがあるのですが(そのまま)、これらが
シングルスレッドで動作しているので、マルチスレッドにするには?ということで調べてみました。

環境

今回の環境は、こちら。

$ python3 -V
Python 3.6.8

TCPServer

まずは、ふつうにTCPServerを使ってみましょう。お題は、Echoで。

socketserver.TCPServer

echo_server.py

from socketserver import TCPServer, BaseRequestHandler

address = ('localhost', 5000)

class EchoHandler(BaseRequestHandler):
    def handle(self):
        message = self.request.recv(1024)
        self.request.sendall(b'Reply: ' + message)

TCPServer.allow_reuse_address = True  # avoid, OSError: [Errno 98] Address already in use

with TCPServer(address, EchoHandler) as server:
    server.serve_forever()

あんまり本体とは関係ないですが、以下の1文を入れているのは短い間隔でサーバーを再起動した時に、ポートバインドで失敗させないためです。

TCPServer.allow_reuse_address = True  # avoid, OSError: [Errno 98] Address already in use

BaseServer / allow_reuse_address

それ以外にSocketのオプションを触りたかったら、TCPServerのserver_bindメソッドをオーバーライドしましょう。

https://github.com/python/cpython/blob/v3.6.8/Lib/socketserver.py#L462-L471

脱線しました。

確認しましょう。サーバーを起動。

$ python3 echo_server.py

ncで確認。

$ echo 'Hello' | nc localhost 5000
Reply: Hello

とりあえず、これが基本形です。

ThreadingTCPServerを使う

では、先ほどのEchoサーバーをマルチスレッドにしてみます。

変更箇所は少なくて、TCPServerをThreadingTCPServerにするだけです。

socketserver.ThreadingTCPServer

threading_echo_server.py

from socketserver import ThreadingTCPServer, BaseRequestHandler
import threading

address = ('localhost', 5000)

class EchoHandler(BaseRequestHandler):
    def handle(self):
        thread_name = threading.current_thread().getName()
        prefix = '[{}] Reply: '.format(thread_name)
        message = self.request.recv(1024)
        self.request.sendall(prefix.encode('utf-8') + message)

ThreadingTCPServer.allow_reuse_address = True

with ThreadingTCPServer(address, EchoHandler) as server:
    server.serve_forever()

こちらも忘れずに。

ThreadingTCPServer.allow_reuse_address = True

あと、レスポンスの内容にスレッド名を入れておきました。

起動。

$ python3 threading_echo_server.py

確認。

$ echo 'Hello' | nc localhost 5000
[Thread-1] Reply: Hello
$ echo 'Hello' | nc localhost 5000
[Thread-2] Reply: Hello
$ echo 'Hello' | nc localhost 5000
[Thread-3] Reply: Hello
$ echo 'Hello' | nc localhost 5000
[Thread-4] Reply: Hello
$ echo 'Hello' | nc localhost 5000
[Thread-5] Reply: Hello
$ echo 'Hello' | nc localhost 5000
[Thread-6] Reply: Hello
$ echo 'Hello' | nc localhost 5000
[Thread-7] Reply: Hello
$ echo 'Hello' | nc localhost 5000
[Thread-8] Reply: Hello
$ echo 'Hello' | nc localhost 5000
[Thread-9] Reply: Hello
$ echo 'Hello' | nc localhost 5000
[Thread-10] Reply: Hello

スレッドが使われていることがわかりますが、どんどんスレッドの名前が変わっていってますね。

素直に、毎回スレッドを作ってstartしているみたいです。

https://github.com/python/cpython/blob/v3.6.8/Lib/socketserver.py#L662-L663

ところで余談ですが、マルチスレッドではなく、マルチプロセスなTCPServerもあるようです。

socketserver.ForkingTCPServer

ThreadPoolExecutorを使う

ThreadingTCPServerでもいいのですが、スレッドを絞りたい時などはThreadPoolExecutorが使えた方がいいかもしれません。

ThreadPoolExecutorを使うようなTCPServerはないので、簡単に自分で作ってみました。
threadpool_echo_server.py

from concurrent.futures import ThreadPoolExecutor
from socketserver import ThreadingTCPServer, BaseRequestHandler
import threading

address = ('localhost', 5000)

workers = 3

class EchoHandler(BaseRequestHandler):
    def handle(self):
        thread_name = threading.current_thread().getName()
        prefix = '[{}] Reply: '.format(thread_name)
        message = self.request.recv(1024)
        self.request.sendall(prefix.encode('utf-8') + message)


class ThreadPoolTCPServer(ThreadingTCPServer):
    def __init__(self, address, handler, workers):
        super().__init__(address, handler)
        self.executor = ThreadPoolExecutor(max_workers = workers)        

    def process_request(self, request, client_address):
        self.executor.submit(self.process_request_thread, request, client_address)

    def server_close(self):
        super().server_close()
        self.executor.shutdown(wait = True)
                             

ThreadPoolTCPServer.allow_reuse_address = True

with ThreadPoolTCPServer(address, EchoHandler, workers) as server:
    server.serve_forever()

ThreadingTCPServerクラスを継承したクラスを作成。

class ThreadPoolTCPServer(ThreadingTCPServer):

このあたりを見つつ、process_request、server_closeメソッドをオーバーライドすれば良さそうです。

https://github.com/python/cpython/blob/v3.6.8/Lib/socketserver.py#L660-L678

オーバーライドしているメソッドが定義してあるのは、ThreadingMixInです。

socketserver.ThreadingMixIn

スレッド数は、3つにしてみました。

workers = 3

確認。

$ python3 threadpool_echo_server.py

結果。

$ echo 'Hello' | nc localhost 5000
[ThreadPoolExecutor-0_0] Reply: Hello
$ echo 'Hello' | nc localhost 5000
[ThreadPoolExecutor-0_1] Reply: Hello
$ echo 'Hello' | nc localhost 5000
[ThreadPoolExecutor-0_0] Reply: Hello
$ echo 'Hello' | nc localhost 5000
[ThreadPoolExecutor-0_1] Reply: Hello
$ echo 'Hello' | nc localhost 5000
[ThreadPoolExecutor-0_2] Reply: Hello
$ echo 'Hello' | nc localhost 5000
[ThreadPoolExecutor-0_0] Reply: Hello

OKそうです。

まあ、元のソースを見てオーバーライドしているので、バージョンに合わせてソースコードを確認した方が良さそうですが…。
通常は、ThreadingTCPServerでいいのかな、と。

HTTPServer

続いて、HTTPServer。

21.22. http.server --- HTTP サーバ — Python 3.6.9 ドキュメント

よく、こうやって起動するあれです。

$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

これを、プログラムから使います。

http_server.py

from http.server import HTTPServer, SimpleHTTPRequestHandler

address = ('localhost', 8080)

with HTTPServer(address, SimpleHTTPRequestHandler) as server:
    server.serve_forever()

SimpleHTTPRequestHandlerというのは、カレントディレクトリ配下のファイルを公開するHandlerです。

http.server.SimpleHTTPRequestHandler

https://github.com/python/cpython/blob/v3.6.8/Lib/http/server.py#L1175-L1211

これで、「python3 -m http.server」と同義です。

$ python3 http_server.py

ちゃんと自分でリクエストを処理するコードを書く場合は、BaseHTTPRequestHandlerクラスを継承したHandlerを作成するようですが、
今回は簡単に済ませました…。

http.server.BaseHTTPRequestHandler

動作確認は、省略。

ところで、「allow_reuse_address」の指定がありませんが、HTTPServerの場合はデフォルトでTrue(1)になっています。

https://github.com/python/cpython/blob/v3.6.8/Lib/http/server.py#L132

HTTPServerをマルチスレッドにしたい

HTTPServerをマルチスレッドにするのは、Python 3.7かそれ以前で方法が変わります。

今回はPython 3.6を使っているので、ThreadingTCPServerを使って、HandlerだけHTTPのものを使うことになります。

threading_http_server.py

from socketserver import ThreadingTCPServer, BaseRequestHandler
from http.server import SimpleHTTPRequestHandler

address = ('localhost', 8080)

ThreadingTCPServer.allow_reuse_address = True

with ThreadingTCPServer(address, SimpleHTTPRequestHandler) as server:
    server.serve_forever()

HTTPServerはTCPServerを継承したクラスで、server_nameくらいしか差異がありません。

https://github.com/python/cpython/blob/v3.6.8/Lib/http/server.py#L130-L139

class HTTPServer(socketserver.TCPServer):

    allow_reuse_address = 1    # Seems to make sense in testing environment

    def server_bind(self):
        """Override server_bind to store the server name."""
        socketserver.TCPServer.server_bind(self)
        host, port = self.server_address[:2]
        self.server_name = socket.getfqdn(host)
        self.server_port = port

場合によっては、この部分を引っ張ってきてもよいでしょう。

Python 3.7の場合は、ThreadingHTTPServerというクラスが追加されます。

http.server.ThreadingHTTPServer

https://github.com/python/cpython/blob/v3.7.4/Lib/http/server.py#L143-L144

どちらにしろ、スレッドの部分を扱っているのはThreadingMixInなので、TCPServerの時と同じ読み方で良さそうです。

なお、ThreadingHTTPServerの登場に伴い、これで起動するHTTPサーバーもマルチスレッドになったようです。

$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

https://github.com/python/cpython/blob/v3.7.4/Lib/http/server.py#L1219-L1220

つまり、Python 3.6まではシングルスレッド…。