1. 程式人生 > >Python Web Flask原始碼解讀(二)——路由原理

Python Web Flask原始碼解讀(二)——路由原理

關於我
一個有思想的程式猿,終身學習實踐者,目前在一個創業團隊任team lead,技術棧涉及Android、Python、Java和Go,這個也是我們團隊的主要技術棧。
Github:https://github.com/hylinux1024
微信公眾號:終身開發者(angrycode)

接上一篇的話題,繼續閱讀Flask的原始碼,來看一下這個框架路由原理。

0x00 路由原理

首先看下Flask的簡易用法

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return f'Hello, World!'

if __name__ == '__main__':
    app.run()

Flask中是使用@app.route這個裝飾器來實現url和方法之間的對映的。

Flask.route

開啟route方法

def route(self, rule, **options):
    """這個方法的註釋非常詳細,為了避免程式碼篇幅過長,這裡省略註釋"""
    def decorator(f):
        self.add_url_rule(rule, f.__name__, **options)
        self.view_functions[f.__name__] = f
        return f

    return decorator

route方法中有兩個引數ruleoptionsruleurl規則,options引數主要是werkzeug.routing.Rule類使用。 方法內部還定義decorator方法,將url路徑規則,和方法名稱對應關係儲存起來,然後將函式方法名與函式物件也對應的儲存到一個字典中。

Flask.add_url_rule
def add_url_rule(self, rule, endpoint, **options):
    options['endpoint'] = endpoint
    options.setdefault('methods', ('GET',))
    self.url_map.add(Rule(rule, **options))

這個方法的註釋也是很詳細的,大概的意思如果定義了一個方法

@app.route('/')
def index():
    pass

等價於

def index():
    pass
app.add_url_rule('index', '/')
app.view_functions['index'] = index

最後呼叫url_map.add方法將ruleoption構造成Rule新增到一個Map物件中。

Rule

Rule表示url規則,它是在werkzeug函式庫中定義的類。

url_map是一個自定義的Map物件。它的目的就是實現url與方法之間對映關係。

Map.add
def add(self, rulefactory):
    """Add a new rule or factory to the map and bind it.  Requires that the
    rule is not bound to another map.

    :param rulefactory: a :class:`Rule` or :class:`RuleFactory`
    """
    for rule in rulefactory.get_rules(self):
        rule.bind(self)
        self._rules.append(rule)
        self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule)
    self._remap = True

add方法中就呼叫了rule中的bind方法,這裡才是真正實現繫結的邏輯。

Rule.bind
def bind(self, map, rebind=False):
    """Bind the url to a map and create a regular expression based on
    the information from the rule itself and the defaults from the map.

    :internal:
    """
    if self.map is not None and not rebind:
        raise RuntimeError('url rule %r already bound to map %r' %
                           (self, self.map))
    # 將url與map對應起來,即將map儲存在rule物件自身的map屬性上
    self.map = map
    if self.strict_slashes is None:
        self.strict_slashes = map.strict_slashes
    if self.subdomain is None:
        self.subdomain = map.default_subdomain

    rule = self.subdomain + '|' + (self.is_leaf and self.rule or self.rule.rstrip('/'))

    self._trace = []
    self._converters = {}
    self._weights = []

    regex_parts = []
    for converter, arguments, variable in parse_rule(rule):
        if converter is None:
            regex_parts.append(re.escape(variable))
            self._trace.append((False, variable))
            self._weights.append(len(variable))
        else:
            convobj = get_converter(map, converter, arguments)
            regex_parts.append('(?P<%s>%s)' % (variable, convobj.regex))
            self._converters[variable] = convobj
            self._trace.append((True, variable))
            self._weights.append(convobj.weight)
            self.arguments.add(str(variable))
            if convobj.is_greedy:
                self.greediness += 1
    if not self.is_leaf:
        self._trace.append((False, '/'))

    if not self.build_only:
        regex = r'^%s%s$' % (
            u''.join(regex_parts),
            (not self.is_leaf or not self.strict_slashes) and \
                '(?<!/)(?P<__suffix__>/?)' or ''
        )
        self._regex = re.compile(regex, re.UNICODE)

bind方法中的for迴圈中呼叫了parse_url方法,這是一個生成器函式,它使用正則進行並yield回一個元組。這個方法的細節還是挺多的,但這裡我們抓住主脈絡,先把整體流程搞清楚。

Flask啟動時從裝飾器route開始就把會把url和響應的函式方法對應起來。

呼叫邏輯為

Flask.route -> Flask.add_url_rule -> Map.add -> Rule.bind

0x01 響應請求

當服務啟動之後,Flask會預設開啟一個Web伺服器,便於開發除錯,而實際環境中可能會使用nginx+gunicorn等工具進行部署。由於部署不是本節主題,我們還是專注於客戶端請求是如何響應的。

在上一篇我們知道Flask通過Werkzeug函式庫中的run_simple方法將服務啟動了。

當客戶端傳送請求時這個方法會被執行

Flask.wsgi_app
def wsgi_app(self, environ, start_response):
    """The actual WSGI application.  This is not implemented in
    `__call__` so that middlewares can be applied:

        app.wsgi_app = MyMiddleware(app.wsgi_app)

    :param environ: a WSGI environment
    :param start_response: a callable accepting a status code, a list of headers and an optional
    exception context to start the response
    """
    with self.request_context(environ):
        rv = self.preprocess_request()
        if rv is None:
            rv = self.dispatch_request()
        response = self.make_response(rv)
        response = self.process_response(response)
        return response(environ, start_response)

environWeb伺服器傳遞過來的引數,request_context(environ)會建立一個請求上下文例項,通過預處理preprocess_request之後就會進入分發請求dispatch_request,然後是執行響應make_responseprocess_response,最後返回response

這裡我們重點關注dispatch_request

Flask.dispatch_request
def dispatch_request(self):
    """Does the request dispatching.  Matches the URL and returns the
    return value of the view or error handler.  This does not have to
    be a response object.  In order to convert the return value to a
    proper response object, call :func:`make_response`.
    """
    try:
        endpoint, values = self.match_request()
        return self.view_functions[endpoint](**values)
    except HTTPException as e:
        handler = self.error_handlers.get(e.code)
        if handler is None:
            return e
        return handler(e)
    except Exception as e:
        handler = self.error_handlers.get(500)
        if self.debug or handler is None:
            raise
        return handler(e)

這個方法的核心就是match_request,通過匹配客戶端請求的url規則找到對應函式方法。

Flask.match_request
def match_request(self):
    """Matches the current request against the URL map and also
    stores the endpoint and view arguments on the request object
    is successful, otherwise the exception is stored.
    """
    rv = _request_ctx_stack.top.url_adapter.match()
    request.endpoint, request.view_args = rv
    return rv

匹配完成後就會呼叫self.view_functions[endpoint](**values)來執行對應函式方法,並返回函式的返回值。

如果上述dispatch_request沒有匹配到url規則,則會執行error_handlers字典中找到對應的錯誤碼執行handler方法。

至此url路由規則匹配過程就完成了。

0x02 總結一下

Flask啟動後會把route裝飾器解析後,把url規則與函式方法進行對應儲存。
在客戶端請求時,Flask.wsgi_app方法會被執行,並開始匹配url找到對應的方法,執行後將結果返回。

0x03 學習資料

  • https://werkzeug.palletsprojects.com/en/0.15.x/
  • https://palletsprojects.com/p/flask/
  • https://docs.python.org/3/library/http.server.html#module-http.server