LoginSignup
9
11

More than 1 year has passed since last update.

Djangoのrunserverで開発サーバーが立ち上がるまで

Last updated at Posted at 2021-05-09

今回やる事

おなじみコマンド「パイソンマナゲランサーバー」を追ってみる。

全体的な流れ

  1. ターミナルなどで「python manage.py runserver」と入力する。
  2. __init__.pyが動き設定などの取り込み(省略)
  3. manage.pyが動き、コマンドラインで入力された文字のチェック。
  4. 3つ目のコマンドライン引数(runserver)がサブコマンドとして認識される。
  5. django.core.management.commandsの中のファイルから、該当ファイルの該当クラスを取得。
  6. runserver.pyのCommandクラスのinner_runメソッドにより、runメソッドが呼ばれる。
  7. runメソッドにより、ソケットbind, listenで接続準備しserve_foreverでリクエスト待ち
  8. リクエストが来たらserve_foreverによりsocket.acceptが呼ばれリクエストを処理するhandlerにリクエストらを渡す。

※ipやポートなどはrunserver.pyのコマンドクラスに、デフォルトが指定されているためそれが使用される。

実装をひたすら追ってみる

まず python manage.py runserverにより、直下のmanage.pyが動く。

1. manage.pyが動き出す

manage.py
def main():
    os.environ.setdefault('DJANGO_SETTING_MODULE', 'アプリ名.settings')
    ...
    execute_from_command_line(sys.argv)     

if __name__ == '__main__':
    main()

manage.pyでは、環境変数を設定した後、execute_from_commmand_lineというメソッドにコマンドラインで渡された引数をリストで渡し、メソッドを呼んでいる。


sys.argvとする事で、コマンドラインで入力したpython manage.py runserverのpython以降の ['manage.py', 'runserver'] がリスト形式で取得出来る

2. execute_from_command_lineメソッド

django.core.management.__init__.py
def execute_from_command_line(argv=None):
    utility = ManagementUtility(argv)
    utility.execute()

execute_from_commmand_lineでは、ManagementUtilityクラスをインスタンス化し、ManagementUtilityクラスのexecuteメソッドを呼ぶ。

3. ManagementUtility.executeメソッド

django.core.management.__init__.py
class ManagementUtility:
    def __init__(self, argv=None):
        """
        コマンドライン引数をセット
          self.argv = ['manage.py', 'runserver']
        """
        self.argv = argv or sys.argv[:]
        self.prog_name = os.path.basename(self.argv[0])
        if self.prog_name == '__main__.py':
            self.prog_name = 'python -m django'
        self.settings_exception = None

    def execute(self):
        try:
            # subcommandはrunserverになる。
            subcommand = self.argv[1]
        except IndexError:
            subcommand = 'help'

        # CommandParserでコマンドライン引数の詳細設定を行う。
        # runserverだけだったらここは重要じゃない。
        parser = CommandParser(
            prog=self.prog_name,
            usage='%(prog)s subcommand [options] [args]',
            add_help=False,
            allow_abbrev=False
        )
        parser.add_argument('--settings')
        parser.add_argument('--pythonpath')
        parser.add_argument('args', nargs='*')
        try:
       # runserverだけだったらindexは1までしかない。
            options, args = parser.parse_known_args(self.argv[2:])
            handle_default_options(options)
        except CommandError:
            pass
        ...

        # エラーチェックとか

        ...
        if subcommand == 'help':
        ...
        else:
           self.fetch_command(subcommand).run_from_argv(self.argv)

runserverのコマンドをsubcommandという変数に持ったりエラーチェックなどをした後、fetch_commandで該当のコマンドのクラスを取得し、そのrun_from_argvメソッドを呼ぶ。

4. ManagementUtility.fetch_commandメソッド

Commandクラスを返すメソッド。
コマンドが正しく入力されていなかったりしたら、見つからないよメッセージなどを出力する。

django.core.management.__init__.py
    def fetch_command(self, subcommand):

        # get_commandsメソッドは、django.core.management.commandsの中のコマンドクラス一覧を取得し、
        # {コマンド名: アプリ名}という形式の辞書で返すメソッド。アプリ名はソース内で実装されているクラス。
        # runserverだったら {'runserver', Commandクラス}になる。
        commands = get_commands()
        try:
            app_name = commands[subcommand]
        except KeyError:
            ...
        if isinstance(app_name, BaseCommand):
            klass = app_name
        else:
            klass = load_command_class(app_name, subcommand)
        return klass

feach_commandメソッドによってdjango.core.management.commands.runsercer.pyのCommandクラスが返され、そのクラスのrun_from_argvメソッドが呼ばれる。

5. Command.run_from_argvメソッド (runserver.py)

django.core.management.commands.runserver.py

# runserverのコマンドクラス
class Command(BaseCommand):
    ...
    default_addr = '127.0.0.1'
    default_addr_ipv6 = '::1'
    default_port = '8000'
    protocol = 'http'
    server_cls = WSGIServer

    ... 

run_from_argvはこのクラスには実装されていないため、親クラスを見てみる。

BaseCommandクラスのrun_from_argメソッド
django.core.management.base.py
class BaseCommand:
    """
    argv = ['manage.py', 'runserver']
    """
    def run_from_argv(self, argv):
        ...
        try:
            # executeメソッドを
            self.execute(*args, **cmp_options)
        except Exception as e:
            ...
        finally:
            try:
                connections.close_all()

runserverのCommandクラスの親クラスのBaseCommandクラスでは、executeメソッドを呼び出している。

runserverのCommandクラスでexecuteメソッドは実装されているが、ほぼ中身は無く親クラスのexecuteを呼び出す形になっているためBaseCommandクラスのexecuteメソッドを見てみる。

6. BaseCommandクラスのexecuteメソッド

django.core.management.base.py
class BaseCommand:
    def execute():
        # Djangoプロジェクトのエラーチェックなど
        ...
        output = self.handle(*args, **options)
        ...
        return output

エラーチェックなどを行った後、handleメソッドを呼んでいる。handleメソッドはrunserver.pyのコマンドクラスに実装されている。

7. runserverのCommandクラスのhandle ~ innerrunメソッド

django.core.management.commands.runserver.py
class Command(BaseCommand):
    def handle(self, *args, **options):
        # エラーチェックなど。
        # 「You must set settings.ALLOWED_HOSTS if DEBUG is False.」などのエラーはここで発生させたりしている。

        ...
        # runメソッドを呼ぶ。
        self.run(**options)

    def run(self, **options):
        # --noreloadなど指定しているかのチェック
        use_reloader = options['use_reloader']
        if use_reloader:
            autoreload.run_with_reloader(self.inner_run, **options)
        else:
            # 今回はこっち
            self.inner_run(None, **options)

    def inner_run(self, *args, **options):
        # エラーチェックなど。
        # マイグレーションしてない時ターミナルに出るエラーメッセージ、立ち上げた時のIP、ポートなどはここで出力している。
        ...

        try:
            # get_handlerメソッドは、settings.pyにWSGI_APPLICATIONで指定しているWSGIHandlerが返される。
            handler = self.get_handler(*args, **options)

            # runメソッドによってサーバーを立ち上げる。
            run(self.addr, int(self.port), handler, ipv6=self.use_ipv6, ipv6, threading=threading., server_cls=self.server_cls)
        except:
            ...

ここでも色々なエラーチェックなどをした後、runメソッドを呼ぶ。

handler = self.get_handler(*args, **options)を追っていくと、get_wsgi_applicationというメソッドにたどり着き、そのメソッドでWSGIHandlerがインスタンス化されてreturnされる事で、WSGIHandlerのinitがここで呼ばれるっぽい。

djnago.core.wsgi.py
def get_wsgi_application():
    ...
    return WSGIHandler()

WSGIHandlerのinitが呼ばれ、ミドルウェア関連の検証など

django.core.handlers.py
class WSGIHandler(base.BaseHandler):

    def __init__(self, *args, **kwargs):
        # 親クラスではinitは無いためほぼ意味ない
        super().__init__(self, *args, **kwargs)
        # ミドルウェアを順に検証し色々セットしてる
        self.load_middleware()

ミドルウェア関連の処理は色々分かりにくいので別でまとめる予定。

8. runメソッドによりソケット待機状態にし、リクエスト待ちとなる

django.core.servers.basehttp.py
def run(addr, port, wsgi_handler, ipv6=False, threading=False, server_cls=WSGIServer):
    """
    ip, portはrunserver.pyで定義されたデフォルトが入る。
        addr = '127.0.0.1'
        port = '8000'
        wsgi_handler = WSGIHandler
        ipv6 = False (コマンドラインからuse_ipv6で指定したか)
        theading = False (コマンドラインからuse_threadingで指定したか)
        server_cls = WSGIServer
    """

    server_address = (addr, port)
    if threading:
        httpd_cls = type('WSGIServer', (socketserver.ThreadingMixIn, server_cls), {})
    else:
        # 今回はこちら
        httpd_cls = server_cls

    # WSGIServerをインスタンス化する事でソケットがbind, listenされる。
    httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6)
    ...

    # WSGIServerにWSGIHandlerをセットする。
    httpd.set_app(wsgi_handler)

    # serve_foreverを呼ぶ。
    httpd.serve_forever()

WSGIServerをインスタンス化する事で、ipやportやリクエストハンドラー(WSGIRequestHandler)などを設定し、継承している親クラスのTCPServerで、socket.bind, socket.listenが動いてソケットを待機状態にし、serve_foreverを呼ぶことでリクエスト待ち状態にしている。

このserve_foreverというのはPython標準ライブラリのsocketserverのBaseServerクラスに実装されているメソッド。github上の該当ソース

まとめ

標準ライブラリのサーバーを立ち上げるまでに、どのようなチェックをしているのかが大まかに理解出来た。立ち上げたサーバーにリクエストが来たらどのような動きをするのかもまとめたいと思った。

9
11
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
9
11