LoginSignup
7
13

More than 1 year has passed since last update.

Djangoのサーバーがリクエストを受け取ってからの流れ ①

Last updated at Posted at 2021-05-19

今回やる事

前回Djangoの開発サーバーが立ち上がるまでを追ってみて、そこからリクエストが来たらどういう風に動くのかが気になって追ってみた。

環境

Django 3.2.3

リクエストを受け取ってからの全体のおおまかな流れ

  • serve_foreverでリクエストを受信したらsocket.acceptが呼ばれ、リクエストとアドレスを取得
  • WSGIRequestHandlerにコネクション(リクエスト)、アドレス、ストリーム関連などをセット
  • StreamRequestHandlerにより、コネクションに関連付いたファイルオブジェクトを作成
  • WSGIHandlerがインスタンス化される事で、__init__が呼ばれミドルウェアチェーンを作成したりする
  • WSGIHandlerを関数のように呼ぶ事で、WSGIHandler.__call__が呼ばれる
  • ミドルウェアチェーンにリクエストを渡す ← 今回はここらへんまで。

続きはこちらの記事でまとめました。
Djangoのサーバーがリクエストを受け取ってからの流れ ②

実装を追ってみる

1. serve_foreverでソケットを監視

読み込み可能になったら、_handle_request_noblock()が呼ばれる。

cpython.Lib.socketserver.py
class BaseServer:

    def serve_forever(self, poll_interval=0.5):
        """
        シャットダウンするまで1度に1つのリクエストを処理する。

        self.timeoutは無視して、poll_intervalの間隔でシャットダウンをポーリングする。
        定期的なタスクを実行する必要がある場合は別スレッドを立ち上げる必要がある。
        """
        self.__is_shut_down.clear()
        try:
            # selectorsというpython3.4で追加されたモジュールを使ってI/Oイベントを監視する。
            with _ServerSelector() as selector:
                # 監視するI/Oイベントを登録する。ここでは受信(読み込み)可能かを監視する。
                selector.register(self, selectors.EVENT_READ)

                while not self.__shutdown_request:
                    # selectメソッドはunixのselect()を呼ぶメソッドで、
                    # 複数のファイルディスクリプタ(ソケット)を監視し、ここでは【読み込み可能】になるまで待つメソッド。
                    ready = selector.select(poll_interval)
                    if self.__shutdown_request:
                        # shutdownのリクエストだったらbreakする
                        break
                    if ready:
              # 読み込み可能になったら、リクエストを処理する
                        self._handle_request_noblock()

                    # service_actions()は、このループの間に実装が必要な処理がある場合、
                    # サブクラスかMixinなどでオーバーライドして実装する。今回は何も実装していないと思われる。
                    self.service_actions()

selectorsというのはpython3.4で追加された標準ライブラリで、ドキュメントから引用すると

selectors --- 高水準の I/O 多重化¶

このモジュールにより、select モジュールプリミティブに基づく高水準かつ効率的な I/O の多重化が行えます。
OS 水準のプリミティブを使用した正確な制御を求めない限り、このモジュールの使用が推奨されます。
このモジュールは BaseSelector 抽象基底クラスと、いくつかの具象実装 (KqueueSelector, EpollSelector...) を定義しており、これらは複数のファイルオブジェクトの I/O の準備状況の通知の待機に使用できます。

となっており、selectorsを用いる事で、ソケットを定期的に監視し送受信が可能かどうかをチェック出来るものらしい。

2. BaseServer._handle_request_noblockメソッド

cpython.Lib.socketserver.py
class BaseServer:

    def _handle_request_noblock(self):
        try:
            # get_requestメソッドでは、socket.acceptが呼ばれrequestとaddrのペアを取得する。(clientsocket, address)
            request, client_address = self.get_request()
        ...
        # リクエストを検証するメソッドだが、オーバーライドされていないっぽくてTrueが返る。
        if self.verify_request(request, client_address):
            try:
                # リクエストを処理するメソッドへ
                self.process_request(request, client_address)
            except Exception:
                ...
        else:
            # socket.closeが呼ばれソケットが閉じられる。
            self.shutdown_request(request)

BaseServerのget_requestメソッドによりsocket.acceptが呼ばれると、リクエストとアドレスのペアを取得出来る。acceptの説明は公式ドキュメントによると以下の通り。

socket.accept()
接続を受け付けます。ソケットはアドレスにbind済みで、listen中である必要があります。戻り値は (conn, address) のペアで、 conn は接続を通じてデータの送受信を行うための 新しい ソケットオブジェクト、 address は接続先でソケットにbindしているアドレスを示します。

3. BaseServer.process_requestメソッド

cpython.Lib.socketserver.py
class BaseServer:

    def process_request(self, request, client_address):
        # リクエストハンドラーのインスタンス化
        self.finish_request(request, client_address)
        # ソケットを閉じる。
        self.shutdown_request(request)

    def finish_request(self, request, client_address):
        # このメソッドによってWSGIRequestHandlerがインスタンス化される。
        self.RequestHandlerClass(request, client_address, self)

ここでは最終的に、finish_requestメソッドの中で、「WSGIRequestHandler」をインスタンス化する。
※WSGIServerのインスタンス化の際指定した。

4. WSGIRequestHandlerのインスタンス化

django.core.servers.basehttp.py
class WSGIRequestHandler(simple_server.WSGIRequestHandler):
   # 同クラスには、__init__が実装されていないので、親クラスの__init__が呼ばれる。

cpython.Lib.socketserver.py
class BaseRequestHandler:

    def __init__(self, request, client_address, server):
        """
        request: socket.acceptで取得したソケットオブジェクト
        client_address: socket.acceptで取得したクライアントのアドレス
        server: WSGIServer
        """
        # 各種属性をセット
        self.request = request
        self.client_address = client_address
        self.server = server

        # StreamRequestHandlerクラスのsetupメソッドが呼ばれ、バイナリストリームをセットする
        self.setup()
        try:
            # WSGIRequestHandlerのhandleメソッドが呼ばれる
            self.handle()
        finally:
            # 諸々終わったらStreamRequestHandlerクラスのflushメソッドによりでストリームに書き込んだ後、ストリームを閉じる。
            self.finish()

__init__により、リクエストやアドレス等の情報をセットする。
そしたら次にStreamRequestHandlerのsetupが呼ばれる。

※リクエストハンドラーを作るときは、__init__メソッドはBaseRequestHandlerのを用いて、実装するクラスでhandleメソッドをオーバーライドするのが一般的らしい。

setupメソッド

StreamRequestHandlerのsetupにより、ストリーム周りの設定
cpython.Lib.socketserver.py
class StreamRequestHandler(BaseRequestHandler):
    rbufsize = -1
    wbufsize = 0

    def setup(self):
        """
        rfile:
            ソケットに関連付けられたファイルオブジェクトで、データが大きいと非常に遅いためデフォルトではバッファリングされる。

        wfile:
            - wbufsize=0の場合
                _SocketWriterはデータをバッファに保持せず、sendallメソッドによりデータを送信するため、flushを呼び出す必要が無い。

            - wbufsizeが0じゃない場合
                ファイルオブジェクトを使用するためflushを呼び出す必要がある。
        """
        self.connection = self.request
        ...
        self.rfile = self.connection.makefile('rb', self.rbufsize)
        if self.wbufsize == 0:
            # wbufsize=0だと書き込むデータをバッファに保存しないため_SocketWriterを使用する。
            # 通常はこちらを使用するっぽい?
            self.wfile = _SocketWriter(self.connection)
        else:
            self.wfile = self.connection.makefile('wb', self.wbufsize)

    def finish(self):
        if not self.wfile.closed:
            try:
                self.wfile.flush()
            except socket.error:
                pass
        self.wfile.close()
        self.rfile.close()

読み込み用のストリームはファイルオブジェクトを使うけど、書き込み用のストリームはwbufsizeによって、ファイルオブジェクトを使用するか、使用せずにsocket.sendallでそのままソケットにデータを送信する形にするか指定している。

_SocketWriterのwriteメソッドは内部でsendallを呼ぶようにオーバーライドされている。

ファイルオブジェクトを使わない場合はflushでバッファを出力する必要がないみたい。

makefileはファイルオブジェクトを返すメソッドで、公式ドキュメントによると以下の通り。

socket.makefile(mode='r', buffering=None, *, encoding=None, errors=None, newline=None)¶
ソケットに関連付けられた ファイルオブジェクト を返します。戻り値の正確な型は、 makefile() に指定した引数によります。これらの引数は、組み込み関数 open() の引数と同様に解釈されます。

5. WSGIRequestHandler.handleメソッド

handleメソッド
 ↓
handle_one_requestメソッド
 ↓
エラーなどの検証をした後、ServerHandlerをインスタンス化し、ServerHandler.runメソッドを呼ぶ。

django.core.servers.basehttp.py
class WSGIRequestHandler(simple_server.WSGIRequestHandler):

    def handle(self):
        self.close_connection = True
        self.handle_one_request()
        while not self.close_connection:
            self.handle_one_request()
        try:
            # socket.SHUT_WRにより、以降は送信を行えないようにする。
            # ちなみにSHUT_RDだと受信、SHUT_RDWRだと送受信が行えないようにする。
            self.connection.shutdown(socket.SHUT_WR)
        ...

    def handle_one_request(self):
        # rfileからデータを読み込む
        self.raw_requestline = self.rfile.readline(65537)

        # 16bitより長ければ414(URI Too Long)を投げる
        if len(self.raw_requestline) > 65536:
            ...
            # send_errorメソッドはエラーコードやメッセージなどを正しい形式に纏めてバッファに書き込むメソッド。
            self.send_error(414)
            return

        if not self.parse_request():
            # リクエストが正しい形式かを検証するメソッド。正しくない場合はsend_errorメソッドでエラーを送信する。
            return

        # ServerHandlerのインスタンス化
        handler = ServerHandler(
            self.rfile, self.wfile, self.get_stderr(), self.get_environ()
        )
        handler.request_handler = self
        # ServerHandlerのrunメソッドが呼ばれる
        handler.run(self.server.get_app())

バイナリからデータを読み込んで、ヘッダーの形式が正しいかチェックし、おかしければwfile.writeでエラーを返したりしている。

最終的にServerHandlerにrfile, wfileなどを渡しインスタンス化してrunメソッドを呼ぶ。

6. ServerHandlerのインスタンス化

django.core.servers.basehttp.py
class ServerHandler(simple_server.ServerHandler):

    def __init__(self, stdin, stdout, stderr, environ, **kwargs):
        try:
            # さっき設定したリクエストのcontent_lengthを取得
            content_length = int(environ.get('CONTENT_LENGTH'))
        ...
        super().__init__(LimitedStream(stdin, content_length), stdout, stderr, environ, **kwargs)

親クラスのinitが呼ばれるが、その前にLimitedStreamを追ってみる。

django.core.handlers.wsgi.py
class LimitedStream:

    def __init__(self, stream, limit, buf_size=64 * 1024 * 1024):
        self.stream = stream
        self.remaining = limit
        self.buffer = 'b'
        self.buf_size = buf_size

    # read, readlineメソッドが実装されている
    ...

このクラスは、他のストリームをラップしてバイト数を超えて読み取れないようにするようにするクラスみたい。
そのためにread, readlineを、バイト数を超えて読み取れないようにオーバーライドして実装している。

次にServerHandlerの親クラスのinitを見てみる。

ServerHandlerの親クラス(simple_server.ServerHandler)にはinitは無いため、その親のinitが動く。

wsgiref.handlers.py
class SimpleHandler(BaseHandler):

    def __init__(self, stdin, stdout, stderr, environ, multithread=True, multiprocess=False):
        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr 
        self.base_env = environ
        self.wsgi_multithread = multithread
        self.wsgi_multiprocess = multiprocess

ただ単に設定しているだけだった。initの次はrunメソッドが呼ばれる。

7. ServerHandlerのrunメソッド

・self.server.get_appメソッドが呼ばれ、WSGIHandlerクラスの__init__が動く。
・WSGIHandler.runメソッド呼ばれる

self.server.get_app()

get_appにより、serve_foreverを呼ぶときにサーバーにセットしたself.application(WSGIHandler)を取得する。
これはsettings.pyに設定しているWSGI_APPLICATIONで、追っていくとget_wsgi_applicationというメソッドが呼ばれて最終的にWSGIHandlerクラスがインスタンス化されて返される。

django.core.wsgi.py
def get_wsgi_application():
    # djangoの初期化
    django.setup()
    # この時点で__init__が呼ばれる。
    return WSGIHandler()
django.core.handlers.wsgi.py
class WSGIHandler(base.BaseHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.load_middleware()

initが動くことでミドルウェア関連の処理が走る。

django.core.handlers.base.py
class BaseHandler:

    def load_middleware(self, is_async=False):
        # Viewミドルウェア、テンプレートミドルウェアなどを保持するリスト
        self._view_middleware = []
        self._template_middleware = []
        self._exception_middleware = []

        # ここでは get_response = self._get_response
        get_response = self._get_response_async if is_async else self._get_response

        # このhandlerはget_responseメソッドを記憶したinnerメソッド
        handler = convert_exception_to_response(get_response)
        ...
        # ミドルウェアを下から順に処理していく 
        #    => デコレータの関係でこうする事により、記載したミドルウェアが上から順に呼ばれ、最後に素のget_responseが動く。
        for middleware_path in reversed(settings.MIDDLEWARE):
            middleware = import_string(middleware_path)
            ...

            try:
                # get_responseメソッドをasyncフラグによって正しいmodeに切り替える。get_responseメソッドが返る。
                adapted_handler = self.adapt_method_mode(
                    middleware_is_async, handler, handler_is_async,
                    debug=settings.DEBUG, name='middleware %s' % middleware_path,
                )

                # get_responseメソッドをインスタンスにセットする
                mw_instance = middleware(adapted_handler)
            ...

            else:
                handler = adapted_handler
            ...

            # viewミドルウェアだったらviewミドルウェアリストの先頭に追加
            is hasattr(mw_instance, 'process_view'):
                self._view_middleware.insert(
                    0,
                    self.adapt_method_mode(is_async, mw_instance.process_view),
                )

            # templateミドルウェアだったらtemplateミドルウェアリストに追加
            if hasattr(mw_instance, 'process_template_response'):
                self._template_response_middleware.append(
                    self.adapt_method_mode(is_async, mw_instance.process_template_response),
                )
            ...

            # ミドルウェアを記憶したinner関数を作成する
            # ループの次の周からは1個前のミドルウェアをget_responseとしてインスタンスにセット
            handler = convert_exception_to_response(mw_instance)
            handler_is_async = middleware_is_async

        handler = self.adapt_method_mode(is_async, handler, handler_is_async)

        # self._middleware_chainは初期化が完了したフラグとして扱われている。これがあるなら初期化が完了している。
        # 中身は一連のmiddlewareがラップされている関数となっており、これにrequestを渡す事で一連のミドルウェアが順に動く。
        self._middleware_chain = handler

ここで定義したself._middleware_chainに、後でrequestを渡す事になる。

handler.run (ServerHandler)

runメソッドはServerHandlerでは実装されてないので、追ってみたら基底クラスで実装されており、
WSGIHandler(application)をメソッドみたいに呼ぶ事で__call__メソッドを呼び出すようになっている。

wsgiref.handlers.py
class BaseHandler:

    def run(self, application):
        try:
            # 環境変数のセットとか
            ...

            # applicationは既にインスタンス化されたWSGIHandlerで、
            # それをメソッドみたいに呼ぶとWSGIHandler.__call__が呼ばれresultに入る。
            self.result = application(self.environ, self.start_response)

            # ストリームによってself.resultをwrite, flushかsendallかでデータ送信し、close
            self.finish_response()
        ...

WSGIHandler.__call__が呼ばれ、その結果がresultに入る。

WSGIHandler.__call__の実装

callの中では、BaseHandlerのget_responseメソッドを呼ぶことで、レスポンスを取得しレスポンスを返すような処理が実装されている。

django.core.handlers.wsgi.py
class WSGIHandler(base.BaseHandler):
    request_class = WSGIRequest

    def __call__(self, environ, start_response):
        """
        environ: ServerHandlerから渡された環境変数

        start_response: 
            wsgiref.handlers.pyのBaseHandlerに実装されているメソッドが渡される。
            write, flushが返されるが、ここではstatus, headerのチェックとセットを行っている。
        """
        ...

        # シグナル関連
        signals.request_started.send(sender=self.__class__, environ=environ)

        # WSGIRequestクラスのインスタンス化
        request = self.request_class(environ)

        # djangoのBaseHandlerのget_responseメソッドを呼び、レスポンスを取得する。
        response = self.get_response(request) # 1

        ...
        # status, header等のチェックとセット
        start_response(status, response_headers) # 2
        ...
        return response

7.1. BaseHandler.get_response

django.core.handlers.base.py
class BaseHandler:
    def get_response(self, request):
        set_urlconf(settings.ROOT_URLCONF)
        response = self._middleware_chain(request)
        ...
        return response

get_responseメソッドにより、ミドルウェアチェーンにリクエストを渡す事で、一連のミドルウェアに通した後、URLResolverだったりの処理が走り、その結果レスポンスを取得する。

middleware_chainの仕組みについては以下記事でまとめました。
Djangoのmiddleware_chain

7.2. BaseHandler.finish_response

wsgiref.handlers.py
class BaseHandler:
    try:
        if not self.result_is_file() or not self.sendfile():
            for data in self.result:
                self.write(data)
            self.finish_content()
    except:
        if hasattr(self.result, 'close'):
            self.result.close()
        raise
    else:
        self.close()

最後にfinish_responseメソッドが呼ばれ、writeとflushが呼ばれ、resultにあるデータがクライアントに返される。

まとめ

簡単にまとめると以下の様に動く事が分かった。

・サーバーがリクエストを受け取る
 ↓
・ストリーム関連セット
 ↓
・ミドルウェアチェーン作成
 ↓
・リクエストをミドルウェアチェーンに渡す
 ↓
・ミドルウェアのリクエストの処理が終わったら_get_responseメソッドが走る。
 ↓
・レスポンスを取得しクライアントにデータ送信

続きはこちらの記事でまとめました。
Djangoのサーバーがリクエストを受け取ってからの流れ ②

7
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
13