1. 程式人生 > >轉:Django 原始碼閱讀(一):概覽從入口到請求到響應

轉:Django 原始碼閱讀(一):概覽從入口到請求到響應

轉載:Django 原始碼閱讀(一):概覽從入口到請求到響應————作者:hongweipeng
起步
在我研究完 django 的自動載入機制後,有了閱讀 django 原始碼的想法。那就看看吧,也不知道能堅持到什麼地方。我閱讀的版本也是我正在使用的 1.10.5 版本,算是比較新的了。

一般執行 django 程式都是通過: python manage.py runserver 開始的,那我們就從這個入口開始。

入口檔案
manage.py 檔案裡只有簡單的幾行程式碼:

#!/usr/bin/env python
import os
import sys

if __name__ == "__main__":
    # 將settings模組設定到環境變數中
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webui.settings")
    from django.core.management import execute_from_command_line
    # 執行命令
    execute_from_command_line(sys.argv)

在設定環境變數之後,命令引數的列表傳到了 execute_from_command_line 中:

def execute_from_command_line(argv=None):
    """
    A simple method that runs a ManagementUtility.
    """
    utility = ManagementUtility(argv)
    utility.execute()

命令管理工具
命令引數又傳到了 ManagementUtility 類中:

class ManagementUtility(object):
    def __init__(self, argv=None):
        self.argv = argv or sys.argv[:]
        self.prog_name = os.path.basename(self.argv[0])
        self.settings_exception = None

prog_name 就是 manage.py。例項化後呼叫了 execute() 方法,在這個方法中,會對命令引數進行處理。當解析的的命令是 runserver 時,會有兩條路,第一個是會自動重灌的路線,通過 autoreload.check_errors(django.setup)() 代理完成。另一個路線是引數中有 --noreload 時,就用 django.setup() 來啟動服務。

如果不是 runserver 而是其他命令,那麼會對命令引數 self.argv[1] 進行判斷,包括錯誤處理,是否是 help ,是否是 version ,根據不同的情況展示不同的資訊。

最重要的是最後一句,即前面的情況都不是,就進入 self.fetch_command(subcommand).run_from_argv(self.argv) ,這邊分兩步,一步是獲取執行命令所需要的類,其次是將命令引數作為引數傳遞給執行函式執行:

def fetch_command(self, subcommand):
    commands = get_commands()
    try:
        app_name = commands[subcommand]
    except KeyError:
        sys.exit(1)

    if isinstance(app_name, BaseCommand):
        # If the command is already loaded, use it directly.
        klass = app_name
    else:
        klass = load_command_class(app_name, subcommand)
    return klass

get_commands() 是返回是一個命令與模組對映作用的字典:

{
    "makemessages": "django.core",
    "makemigrations": "django.core",
    "migrate": "django.core",
    "runserver": "django.contrib.staticfiles",
    "startapp": "django.core",
    "startproject": "django.core",
    "createsuperuser": "django.contrib.auth"
    ...
}

動態載入模組
模組是通過 load_command_class 來動態載入的:

def load_command_class(app_name, name):
    module = import_module('%s.management.commands.%s' % (app_name, name))
    return module.Command()

如執行 runserver 命令的模組就是 django.contrib.staticfiles.management.commands.runserver 返回該模組中定義的 Command 類的例項。獲得例項後呼叫了 run_from_argv(self.argv) :

def run_from_argv(self, argv):
    self._called_from_command_line = True
    parser = self.create_parser(argv[0], argv[1])

    options = parser.parse_args(argv[2:]) # Namespace(addrport=None, ...) 返回一個Namespace的例項
    cmd_options = vars(options) # 物件轉成字典
    # Move positional args out of options to mimic legacy optparse
    args = cmd_options.pop('args', ())
    handle_default_options(options)     # 設定預設引數
    try:
        self.execute(*args, **cmd_options) # 異常捕獲包裹的execute
    except Exception as e:
        sys.exit(1)
    finally:
        connections.close_all()

設定請求控制代碼
在 execute 中會做一些設定引數的錯誤檢查,然後設定控制代碼:

def handle(self, *args, **options):
    if not settings.DEBUG and not settings.ALLOWED_HOSTS:
        raise CommandError('You must set settings.ALLOWED_HOSTS if DEBUG is False.')

    self.use_ipv6 = options['use_ipv6']
    if self.use_ipv6 and not socket.has_ipv6:
        raise CommandError('Your Python does not support IPv6.')
    self._raw_ipv6 = False
    if not options['addrport']:
        self.addr = ''                  # 預設地址
        self.port = self.default_port  # 預設埠
    else: # 如果設定了ip地址和埠號,用正則匹配出來
        m = re.match(naiveip_re, options['addrport'])
        if m is None:
            raise CommandError('"%s" is not a valid port number '
                               'or address:port pair.' % options['addrport'])
        self.addr, _ipv4, _ipv6, _fqdn, self.port = m.groups()
        if not self.port.isdigit():
            raise CommandError("%r is not a valid port number." % self.port)
        if self.addr:
            if _ipv6:
                self.addr = self.addr[1:-1]
                self.use_ipv6 = True
                self._raw_ipv6 = True
            elif self.use_ipv6 and not _fqdn:
                raise CommandError('"%s" is not a valid IPv6 address.' % self.addr)
    if not self.addr:
        self.addr = '::1' if self.use_ipv6 else '127.0.0.1' #如果沒有設定ip地址使用127.0.0.1代替
        self._raw_ipv6 = self.use_ipv6
    self.run(**options) # 執行命令

run 方法主要時呼叫了 inner_run(*args, **options) 這個方法:

def inner_run(self, *args, **options):
    threading = options['use_threading']
    # 'shutdown_message' is a stealth option.
    shutdown_message = options.get('shutdown_message', '')
    quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C'
    # 輸出基礎資訊
    self.stdout.write("Performing system checks...\n\n")
    self.check(display_num_errors=True)
    # Need to check migrations here, so can't use the
    # requires_migrations_check attribute.
    self.check_migrations()
    now = datetime.now().strftime('%B %d, %Y - %X')
    if six.PY2:
        now = now.decode(get_system_encoding())
    self.stdout.write(now)
    self.stdout.write((
        "Django version %(version)s, using settings %(settings)r\n"
        "Starting development server at http://%(addr)s:%(port)s/\n"
        "Quit the server with %(quit_command)s.\n"
    ) % {
        "version": self.get_version(),
        "settings": settings.SETTINGS_MODULE,
        "addr": '[%s]' % self.addr if self._raw_ipv6 else self.addr,
        "port": self.port,
        "quit_command": quit_command,
    })

    try:
        # 獲取處理 http 的控制代碼
        handler = self.get_handler(*args, **options)
        run(self.addr, int(self.port), handler,
            ipv6=self.use_ipv6, threading=threading)
    except socket.error as e:
        os._exit(1)
    except KeyboardInterrupt:
        if shutdown_message:
            self.stdout.write(shutdown_message)
        sys.exit(0)

這部分除了有熟悉的資訊輸出外,重要的是這個控制代碼:

def get_handler(self, *args, **options):
    """
    Returns the default WSGI handler for the runner.
    """
    return get_internal_wsgi_application()
get_handler 函式最終會返回一個 WSGIHandler 的例項。WSGIHandler 類只實現了 def __call__(self, environ, start_response) , 使它本身能夠成為 WSGI 中的應用程式, 並且實現 __call__ 能讓類的行為跟函式一樣。

def run(addr, port, wsgi_handler, ipv6=False, threading=False):
    server_address = (addr, port)
    if threading:
        httpd_cls = type(str('WSGIServer'), (socketserver.ThreadingMixIn, WSGIServer), {})
    else:
        httpd_cls = WSGIServer
    httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6)
    if threading:
        httpd.daemon_threads = True
    httpd.set_app(wsgi_handler)
    httpd.serve_forever()

這是一個標準的 wsgi 實現。httpd_cls 是 WSGIServer 類,最終的例項化方法在父類 SocketServer 中的 TCPServer 和 BaseServer 中。包括初始化執行緒,初始化網路控制代碼,像下面的 __is_shut_down 和 __shutdown_request 都是在其中初始化的。

處理請求

def serve_forever(self, poll_interval=0.5):
    """
    處理一個 http 請求直到關閉
    """
    #__is_shut_down為一個初始化的threading.Event()的控制代碼,用於執行緒間通訊
    self.__is_shut_down.clear() #.clear()將標識設定為false
    try:
        with _ServerSelector() as selector:

            selector.register(self, selectors.EVENT_READ)

            while not self.__shutdown_request:
                # 下面的函式就是一個封裝好了的select函式,超時時間 0.5 s
                ready = selector.select(poll_interval)
                if ready:
                    self._handle_request_noblock()

                self.service_actions()
    finally:
        self.__shutdown_request = False
        self.__is_shut_down.set() #將標識設定為true

當發現有請求後,就呼叫 _handle_request_noblock 進行處理:

def _handle_request_noblock(self):
    try:
        # 返回請求控制代碼,客戶端地址,get_request()中呼叫了self.socket.accept()來實現客戶端的連線
        request, client_address = self.get_request()
    except OSError:
        return
    if self.verify_request(request, client_address): # 驗證請求合法性
        try:
            #真正的處理連線請求的地方,呼叫了self.finish_request(request, client_address)
            self.process_request(request, client_address)
        except Exception:
            self.handle_error(request, client_address)
            self.shutdown_request(request)
        except:
            self.shutdown_request(request)
            raise
    else:
        self.shutdown_request(request)

在 finish_request 函式返回 django.core.servers.basehttp.WSGIRequestHandler 的例項,其父類 BaseHTTPRequestHandler 類中有對 http 包解包的過程,從其父類的初始化:

class BaseRequestHandler:
    def __init__(self, request, client_address, server):
        self.request = request
        self.client_address = client_address
        self.server = server
        self.setup()
        try:
            self.handle()
        finally:
            self.finish()

響應請求
可以看出,會回撥 handle() 函式,也就是子類 WSGIRequestHandler 覆蓋的方法:

def handle(self):
    self.raw_requestline = self.rfile.readline(65537)
    if len(self.raw_requestline) > 65536:
        self.requestline = ''
        self.request_version = ''
        self.command = ''
        self.send_error(414)
        return
#傳入的引數,讀,寫,錯誤,環境變數。在其父類SimpleHandler中進行了初始化,並且打開了多執行緒和多程序選項
handler = ServerHandler(
    self.rfile, self.wfile, self.get_stderr(), self.get_environ()
)
handler.request_handler = self
handler.run(self.server.get_app())

handler.run(self.server.get_app()) 中就是呼叫之前設定控制代碼的 WSGIHandler 類:

class WSGIHandler(base.BaseHandler):
    request_class = WSGIRequest

    def __init__(self, *args, **kwargs):
        super(WSGIHandler, self).__init__(*args, **kwargs)
        self.load_middleware()

    def __call__(self, environ, start_response):
        ...
        response = self.get_response(request)

        response._handler_class = self.__class__

        status = '%d %s' % (response.status_code, response.reason_phrase)
        response_headers = [(str(k), str(v)) for k, v in response.items()]
        for c in response.cookies.values():
            response_headers.append((str('Set-Cookie'), str(c.output(header=''))))
        start_response(force_str(status), response_headers)
        if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'):
            response = environ['wsgi.file_wrapper'](response.file_to_stream)
        return response

就有一個 response 響應返回啦。