關於我
一個有思想的程序猿,終身學習實踐者,目前在一個創業團隊任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方法中有兩個參數rule和options。rule是url規則,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方法將rule和option構造成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)
environ 是Web服務器傳遞過來的參數,request_context(environ)會創建一個請求上下文實例,通過預處理preprocess_request之后就會進入分發請求dispatch_request,然后是執行響應make_response和process_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找到對應的方法,執行后將結果返回。
