Python(3)WSGI:ウェブページからshutdown()メソッドでWSGIサーバを停止させる

ラベル:

前の関連記事:Python(2)WSGI:辞書environでウェブページからの情報を受け取る


serve_forever()で永続化したサーバをshutdown()で停止させるというのはすぐにわかったのですが、このshutdown()の使い方がちょっとくせものでした。

shutdown()メソッドはどうやって使うの?

def calc(val):  # 引数を2倍にして返す関数。
    return val*2
def app(environ, start_response):  # WSGIアプリ。辞書environから情報を得る。
    import cgi
    fs = cgi.FieldStorage(environ=environ)  # environを引数にしてcgi.FieldStorageオブジェクトを取得。
    val = int(fs.getfirst('value', 1))  # 最初に出会ったvalueの値を整数型にして取得。2番目の引数はデフォルト値。
    c = int(fs.getfirst('counter', 0))+1  # 最初に出会ったcounterの値を整数型にして取得。2番目の引数はデフォルト値。
    html = '''
    {0}(=2<sup>{1}</sup>)を2倍にした計算結果 2<sup>{2}</sup>={3}<br>
    <br>
    <a href="http://localhost:8000/?value={3}&counter={2}">この計算結果をさらに2倍する</a><br>
    <br>
    <a href="http://localhost:8000">最初に戻る</a>
    '''.format(val, c-1, c, calc(val))  # 出力するhtmlを作成。
    start_response('200 OK', [('Content-Type', 'text/html; charset=utf-8')])  # start_responseを設定。
    return [html.encode()]  # htmlをUTF-8からバイト列に変換して返す。
def run():
    port = 8000  # サーバが受け付けるポート番号を設定。
    from wsgiref.simple_server import make_server
    httpd = make_server("", port, app)  # appへの接続を受け付けるWSGIサーバを生成。第一引数はホストlocalhostがデフォルト。
    url = "http://localhost:{}".format(port)  # 出力先のurlを取得。
    import webbrowser
    webbrowser.open_new_tab(url)  # デフォルトのブラウザでurlを開く。
    httpd.serve_forever()  # WSGIサーバは手動で止めないといけません。
if __name__ == '__main__':
    run()
このスクリプトを実行すると24行目で起動したWSGIサーバがリクエストをずっと受け付けてくれます。

これにWSGIサーバを停止するメニューを追加したいと思いました。


21.4.3. wsgiref.simple_server – シンプルな WSGI HTTP サーバからたどって作ったこの継承図をみるとserve_forever()メソッドで永続的に起動したWSGIサーバを停止させるにはshutdown()メソッドを使えばいいはずです。
    httpd.serve_forever()  # WSGIサーバは手動で止めないといけません。
このhttpdに対してshutdown()すればよいはずです。

ところがhttpd.serve_forever()のあとに書いたコマンドは実行されません。
def calc(val):  # 引数を2倍にして返す関数。
    return val*2
def app(environ, start_response):  # WSGIアプリ。辞書environから情報を得る。
    import cgi
    fs = cgi.FieldStorage(environ=environ)  # environを引数にしてcgi.FieldStorageオブジェクトを取得。
    if fs.getfirst('stop', 0):  # stopの値が0以外のとき
        httpd.shutdown()  # WSGIサーバを停止する。
        import sys
        sys.exit()  # Pythonプログラムを終了させる。
    else:
        val = int(fs.getfirst('value', 1))  # 最初に出会ったvalueの値を整数型にして取得。2番目の引数はデフォルト値。
        c = int(fs.getfirst('counter', 0))+1  # 最初に出会ったcounterの値を整数型にして取得。2番目の引数はデフォルト値。
        html = '''
        {0}(=2<sup>{1}</sup>)を2倍にした計算結果 2<sup>{2}</sup>={3}<br>
        <br>
        <a href="http://localhost:8000/?value={3}&counter={2}">この計算結果をさらに2倍する</a><br>
        <br>
        <a href="http://localhost:8000">最初に戻る</a><br>
        <br>
        <a href="http://localhost:8000/?stop=1">WSGIサーバを停止する</a>
        '''.format(val, c-1, c, calc(val))  # 出力するhtmlを作成。
        start_response('200 OK', [('Content-Type', 'text/html; charset=utf-8')])  # start_responseを設定。
        return [html.encode()]  # htmlをUTF-8からバイト列に変換して返す。
if __name__ == "__main__":  # ここから開始。
    port = 8000  # サーバが受け付けるポート番号を設定。
    from wsgiref.simple_server import make_server
    httpd = make_server("", port, app)  # appへの接続を受け付けるWSGIサーバを生成。第一引数はホストlocalhostがデフォルト。
    url = "http://localhost:{}".format(port)  # 出力先のurlを取得。
    import webbrowser
    webbrowser.open_new_tab(url)  # デフォルトのブラウザでurlを開く。
    httpd.serve_forever()  # WSGIサーバを永続化。
5行目でWSGIアプリのなかで場合分けしてhttpd.shutdown()を実行するようにしましたが、これもうまくいきません。

「WSGIサーバを停止する」をクリックするとウェブブラウザはWSGIサーバの反応を待ったままになりますし、Pythonプログラムも終了しません。

socketserver.TCPServerでのshutdown()メソッドの使用例


Python 3.3.3 ドキュメントの21.21.4.3. 平行処理の Mix-inにsocketserver.TCPServerのshutdown()メソッドを使っている例をみつけました。

これはクライアントの要求に対して個別のスレッドを生成して要求を受け付けるTCPサーバの作り方の例です。
import socket
import threading
import socketserver
class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):  # socketserver.BaseRequestHandlerクラスのhandle()を上書きするためにそれを継承したクラスを新たに作成。
    def handle(self):  # handle()メソッドを上書きしてクライアントの要求に対する応答を定義します。
        data = str(self.request.recv(1024), 'ascii')  # self.requestがクライアントからの要求。socketオブジェクトが返る。recv(1024)でバッファサイズ1024でソケットデータを受け取りasciiに変換する。
        # self.requestがsocketオブジェクト(socket.socket)。
        # self.request.recv(1024) バッファサイズ1024でソケットデータをバイトオブジェクトで得る。(socket.socket.recv())
        # str(self.request.recv(1024), 'ascii') バイトオブジェクトをasciiでエンコードして返す。'ascii'を省くとutf-8でエンコードされてb'データ'が返る。
        cur_thread = threading.current_thread()  # 現在のスレッドのThreadオブジェクトを取得。
        response = bytes("{}: {}".format(cur_thread.name, data), 'ascii')  # asciiエンコーディングの文字列をバイトオブジェクトに変換して取得。
        # cur_thread.name 現在のThreadオブジェクトの名前。名前に機能上の意味(semantics)はなく、同じ名前のスレッドが複数あってもよい。
        self.request.sendall(response)  # ソケットにバイトオブジェクトを送信する。
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):  # socketserver.ThreadingMixInクラスとsocketserver.TCPServerクラスを継承してThreadedTCPServerクラスを作成。
    pass
def client(ip, port, message):  # ip, portへ接続要求するクライアントアプリ。
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # デフォルト値でソケットを作成。sock = socket.socket()でも可。
    sock.connect((ip, port))  # ip, portへ接続要求。
    try:
        sock.sendall(bytes(message, 'ascii'))  # サーバーにmessageをバイトオブジェクトにして送信。
        response = str(sock.recv(1024), 'ascii')  # サーバーから送り返されてきたバイトオブジェクトを文字列にして取得。13行目がサーバーの送信動作。
        print("Received: {}".format(response))  # クライアントで受け取ったデータを表示する。
    finally:
        sock.close()  # サーバーと送受信できないときはソケットを閉じる。
if __name__ == "__main__":  # ここから開始。
    HOST, PORT = "localhost", 0  # ホスト名とポート番号を設定。
    server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)  # (HOST, PORT)のリクエストに対してThreadedTCPRequestHandlerをインスタンス化するサーバーをインスタンス化。
    # ThreadedTCPServerはsocketserver.BaseServerを継承している。
    ip, port = server.server_address  # 要求待ちを行うサーバーのアドレスのタプルをipとportに取得。
    server_thread = threading.Thread(target=server.serve_forever)  # targetにメソッドを指定してスレッドオブジェクトをインスタンス化。
    server_thread.daemon = True  # 生成するスレッドをデーモンスレッドにする。これによりメインスレッド(スレッドを生成したこのスレッド)を終了することにより全てのデーモンスレッドを終了できる。
    server_thread.start()  # スレッドの受付を開始。要求によりtargetのメソッドを個別のスレッドで実行させる。
    print("Server loop running in thread:", server_thread.name)  # このスレッド(メインスレッド)の名前を表示。
    client(ip, port, "Hello World 1")  # ip, portに対して接続要求するクライアントアプリを起動。各クライアントごとにスレッドが生成される。
    client(ip, port, "Hello World 2")
    client(ip, port, "Hello World 3")
    server.shutdown()  # メインスレッドを終了する。
37行目でshutdown()メソッドを使っていますね。

実はこれ、37行目がなくてもPythonプログラムは正常終了します。

というのは40行目で生成したスレッドをデーモンスレッドにしているのでメインスレッドが最後まで実行されて終了すると勝手に全スレッドが終了されてPythonプログラムが終了する訳です。

ということで37行目でわざわざサーバを停止しなくてもいいわけです。

そこで40行目をコメントアウトにして生成したスレッドをデーモンスレッドにしないようにします。

すると37行目のshutdown()が生きてきます。

これがないと生成したスレッドが生きているのでPythonプログラムが終了せず手動でPythonプログラムを終了させないといけません。

以下はコメントをつけるのに参考にしたPythonドキュメントへのリンクです。

BaseRequestHandlerは検索 — Python 3.3.3 documentationで検索してもでてきません。

BaseRequestHandlerは21.21. socketserver — A framework for network servers — Python 3.3.3 ドキュメントのソースコードをクリックしてsocketserver.pyをみるとその定義をみることができます。

class BaseRequestHandlerはBaseServer.RequestHandlerClassを定義しており、そのインスタンスが21.21.3. RequestHandlerオブジェクトになっています。

socketserver.BaseRequestHandler.handle()は空っぽでこれを上書きしてクライアントの要求に対する応答を定義します。(RequestHandler.handle()

socket.socket([family[, type[, proto]]]) ソケットオブジェクト。

socket.recv(bufsize[, flags]) ソケットが受け取ったデータをバッファサイズを指定してバイトオブジェクトで返す。

class str(object=b'', encoding='utf-8', errors='strict') オブジェクトの文字列版を返す。

threading.current_thread() 現在のスレッドのThreadオブジェクトを返す。

class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None) Threadオブジェクト。

bytes([source[, encoding[, errors]]]) バイトオブジェクトにencodingのsourceを変換する。

socket.sendall(bytes[, flags]) ソケットにバイトオブジェクトでデータを送信する。

shutdown()メソッドはマルチスレッドで使うもの


上のsocketserver.TCPServerでのshutdown()メソッドの使用例はサーバで実行しています。

とりあえずshutdown()はマルチスレッドにすれば使えることがわかりました。
    def shutdown(self):
        """Stops the serve_forever loop.
        Blocks until the loop has finished. This must be called while
        serve_forever() is running in another thread, or it will
        deadlock.
        """
Lib/socketserver.py
BaseServer.shutdown()には書いてないのですがソースコードに、shutdown()メソッドはserve_forever()が他のスレッドで動いているときに実行しないといけない、とちゃんと書いてありました。
from wsgiref import simple_server
import socketserver
class ThreadedWSGIServer(socketserver.ThreadingMixIn, simple_server.WSGIServer):  # WSGIServerをマルチスレッド対応にしたクラスを作成。
    pass
def calc(val):  # 引数を2倍にして返す関数。
    return val*2
def app(environ, start_response):  # WSGIアプリ。辞書environから情報を得る。
    # import threading
    # print(threading.current_thread().name)  # スレッド名を出力。
    import cgi
    fs = cgi.FieldStorage(environ=environ)  # environを引数にしてcgi.FieldStorageオブジェクトを取得。
    if fs.getfirst('stop', 0):  # stopの値が0以外のとき
        server.shutdown()  # WSGIサーバを停止する。
        html = "終了"  # 出力するhtmlを作成。
    else:
        val = int(fs.getfirst('value', 1))  # 最初に出会ったvalueの値を整数型にして取得。2番目の引数はデフォルト値。
        c = int(fs.getfirst('counter', 0))+1  # 最初に出会ったcounterの値を整数型にして取得。2番目の引数はデフォルト値。
        html = '''
        {0}(=2<sup>{1}</sup>)を2倍にした計算結果 2<sup>{2}</sup>={3}<br>
        <br>
        <a href="http://{4}:{5}/?value={3}&counter={2}">この計算結果をさらに2倍する</a><br>
        <br>
        <a href="http://{4}:{5}">最初に戻る</a><br>
        <br>
        <a href="http://{4}:{5}/?stop=1">WSGIサーバを停止する</a>
        '''.format(val, c-1, c, calc(val), host, port)  # 出力するhtmlを作成。
    start_response('200 OK', [('Content-Type', 'text/html; charset=utf-8')])  # start_responseを設定。
    return [html.encode()]  # htmlをUTF-8からバイト列に変換して返す。
if __name__ == "__main__":  # ここから開始。
    host, port = "localhost", 8000
    server = simple_server.make_server(host, port, app, ThreadedWSGIServer)  # appへの接続を受け付けるThreadedWSGIServerを生成。
    url = "http://{}:{}".format(host, port)  # 出力先のurlを取得。
    import threading
    server_thread = threading.Thread(target=server.serve_forever)  # 要求によりスレッドを生成するメソッドをtargetに指定。
    # server_thread.daemon = True  # デーモンスレッドにするとメインスレッドが終わるとPythonプログラムが終了してしまう。
    server_thread.start()  # スレッドの受付を開始。
    import webbrowser
    webbrowser.open_new_tab(url)  # デフォルトのブラウザでurlを開く。
ということでWSGIサーバをマルチスレッド化してみたらうまくいきました。

「WSGIサーバを停止する」をクリックするとウェブページには「終了」と表示され、Pythonプログラムも正常終了するはずです。

8行目と9行目のコメントアウトをはずすとスレッド名が出力されます。

"C:\Program Files (x86)\LibreOffice 4\program\python.exe"
Thread-2
127.0.0.1 - - [08/May/2014 19:53:41] "GET / HTTP/1.1" 200 375
Thread-3
127.0.0.1 - - [08/May/2014 19:53:43] "GET /?value=2&counter=1 HTTP/1.1" 200 375
Thread-4
127.0.0.1 - - [08/May/2014 19:53:44] "GET /?value=4&counter=2 HTTP/1.1" 200 375
Thread-5
127.0.0.1 - - [08/May/2014 19:53:45] "GET /?value=8&counter=3 HTTP/1.1" 200 377
Thread-6
127.0.0.1 - - [08/May/2014 19:53:48] "GET /?stop=1 HTTP/1.1" 200 6

Process finished with exit code 0

素早くクリックするとひとつのスレッドで二つのリクエストを受けているときがあります。

start_forever()のポーリング間隔との兼ね合いでしょうかね。

35行目のコメントをはずしてデーモンスレッドを生成するようにすると、38行目でウェブブラウザを起動したところでメインスレッドが終了してPythonプログラムも終了してしまうのでウェブブラウザでurlを開くことができません。

参考にしたサイト


21.4.3. wsgiref.simple_server – シンプルな WSGI HTTP サーバ
Pythonから簡単にウェブサーバが起動できます。

次の関連記事:Python(4)関数の引数の変数名やそれに代入されているコードを取得

PR

0 件のコメント:

コメントを投稿