本文簡單的分析了 Flask 的源碼,主要關注 WSGI、Flask 對象的數據結構、Flask 應用啓動過程、請求處理過程、視圖函數、URL 的映射、應用上下文和請求上下文。講解這些主題時也不會面面俱到,請按照你閱讀源碼的須要自行探索。要讀懂本文,你須要較爲熟悉 Flask,好比已經用 Flask 寫過一個小項目,而且有必定的閱讀代碼的能力,並對 web 框架的功能有基本瞭解。python
本文會不時更新,最近更新日期:2017年9月10日。web
這是 Flask 官方欽定的 Demo 代碼:正則表達式
from flask import Flask
app = Flask(__name__)
@app.route(‘/‘)
def index():
return ‘Hello, world!’
if __name__ == ‘__main__’:
app.run()
複製代碼
這篇文章從這個簡單的代碼開始,簡要介紹了 WSGI、Flask 對象的數據結構、Flask 應用啓動過程、請求處理過程、視圖函數、URL 的映射、request 和 response 類(應用上下文和請求上下文),這些主題涵蓋了一個 web 框架的核心。shell
在用戶發起的請求到達服務器以後,會被一個 HTTP 服務器所接收,而後交給 web 應用程序作業務處理,這樣 HTTP 服務器和 web 應用之間就須要一個接口,在 Python web 開發的世界裏,Python 官方欽定了這個接口並命名爲 WSGI,由 PEP333 所規定。只要服務器和框架都遵照這個約定,那麼就能實現服務器和框架的任意組合。按照這個規定,一個面向 WSGI 的框架必需要實現這個方法:數據庫
def application(environ, start_response)
複製代碼
在工做過程當中,HTTP 服務器會調用上面這個方法,傳入請求信息,即名爲 environ
的字典和 start_response
函數,應用從 environ
中獲取請求信息,在進行業務處理後調用 start_response
設置響應頭,並返回響應體(必須是一個可遍歷的對象,好比列表、字典)給 HTTP 服務器,HTTP 服務器再返回響應給用戶。express
因此 Flask 做爲一個開發 web 應用的 web 框架,負責解決的問題就是:flask
__call__
方法下面就來看看 Flask 是如何解決這些問題的。數組
參考閱讀:一塊兒寫一個 web 服務器,該系列文章可以讓你基本理解 web 服務器和框架是如何經過 WSGI 協同工做的。安全
源碼閱讀:
app.py
中Flask
類的代碼。服務器
Demo 代碼的第二行建立了一個 Flask 類的實例,傳入的參數是當前模塊的名字。咱們先來看看 Flask 應用究竟是什麼,它的數據結構是怎樣的。
Flask
是這樣一個類:
The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the application. Once it is created it will act as a central registry for the view functions, the URL rules, template configuration and much more.
The name of the package is used to resolve resources from inside the package or the folder the module is contained in depending on if the package parameter resolves to an actual python package (a folder with an
__init__.py
file inside) or a standard module (just a.py
file).
一個 Flask 對象其實是一個 WSGI 應用。它接收一個模塊或包的名字做爲參數。它被建立以後,全部的視圖函數、URL 規則、模板設置等都會被註冊到它上面。之因此要傳入模塊或包的名字,是爲了定位一些資源。
Flask 類有這樣一些屬性:
request_class = Request
設置請求的類型response_class = Response
設置響應的類型這兩個類型都來源於它的依賴庫 werkzeug
並作了簡單的拓展。
Flask 對象的 __init__
方法以下:
def __init__(self, package_name):
#: Flask 對象有這樣一個字典來保存全部的視圖函數
self.view_functions = {}
#: 這個字典用來保存全部的錯誤處理視圖函數
#: 字典的 key 是錯誤類型碼
self.error_handlers = {}
#: 這個列表用來保存在請求被分派以前應當執行的函數
self.before_request_funcs = []
#: 在接收到第一個請求的時候應當執行的函數
self.before_first_request_funcs = []
#: 這個列表中的函數在請求完成以後被調用,響應對象會被傳給這些函數
self.after_request_funcs = []
#: 這裏設置了一個 url_map 屬性,並把它設置爲一個 Map 對象
self.url_map = Map()
複製代碼
到這裏一個 Flask 對象建立完畢並被變量 app
所指向,其實它就是一個保存了一些配置信息,綁定了一些視圖函數而且有個 URL 映射對象(url_map
)的對象。但咱們還不知道這個 Map 對象是什麼,有什麼做用,從名字上看,彷佛其做用是映射 URL 到視圖函數。源代碼第 21 行有 from werkzeug.routing import Map, Rule
,那咱們就來看看 werkzeug
這個庫中對 Map 的定義:
The map class stores all the URL rules and some configuration parameters. Some of the configuration values are only stored on the
Map
instance since those affect all rules, others are just defaults and can be overridden for each rule. Note that you have to specify all arguments besides therules
as keyword arguments!
能夠看到這個類的對象儲存了全部的 URL 規則和一些配置信息。因爲 werkzeug
的映射機制比較複雜,咱們下文中講到映射機制的時候再深刻了解,如今只要記住 Flask 應用(即一個 Flask
類的實例)存儲了視圖函數,並經過 url_map
這個變量存儲了一個 URL 映射機構就能夠了。
源碼閱讀:
app.py
中Flask
類的代碼和werkzeug.serving
的代碼,特別注意run_simple
BaseWSGIServer
WSGIRequestHandler
。
Demo 代碼的第 6 行是一個限制,表示若是 Python 解釋器是直接運行該文件或包的,則運行 Flask 程序:在 Python 中,若是直接執行一個模塊或包,那麼解釋器就會把當前模塊或包的 __name__
設置爲爲 __main_
。
第 7 行中的 run
方法啓動了 Flask 應用:
def run(self, host=None, port=None, debug=None, **options):
from werkzeug.serving import run_simple
if host is None:
host = '127.0.0.1'
if port is None:
server_name = self.config['SERVER_NAME']
if server_name and ':' in server_name:
port = int(server_name.rsplit(':', 1)[1])
else:
port = 5000
if debug is not None:
self.debug = bool(debug)
options.setdefault('use_reloader', self.debug)
options.setdefault('use_debugger', self.debug)
try:
run_simple(host, port, self, **options)
finally:
# reset the first request information if the development server
# reset normally. This makes it possible to restart the server
# without reloader and that stuff from an interactive shell.
self._got_first_request = False
複製代碼
能夠看到這個方法基本上是在配置參數,實際上啓動服務器的是 werkzeug
的 run_simple
方法,該方法在默認狀況下啓動了服務器 BaseWSGIServer
,繼承自 Python 標準庫中的 HTTPServer.TCPServer
。注意在調用 run_simple
時,Flask 對象把本身 self
做爲參數傳進去了,這是正確的,由於服務器在收到請求的時候,必需要知道應該去調用誰的 __call__
方法。
按照標準庫中 HTTPServer.TCPServer
的模式,服務器必須有一個類來做爲 request handler 來處理收到的請求,而不是由 HTTPServer.TCPServer
自己的實例來處理,werkzeug
提供了 WSGIRequestHandler
類來做爲 request handler,這個類在被 BaseWSGIServer
調用時,會執行這個函數:
def execute(app):
application_iter = app(environ, start_response)
try:
for data in application_iter:
write(data)
if not headers_sent:
write(b'')
finally:
if hasattr(application_iter, 'close'):
application_iter.close()
application_iter = None
複製代碼
函數的第一行就是按照 WSGI 要求的,調用了 app 並把 environ
和 start_response
傳入。咱們再看看 flask 中是如何按照 WSGI 要求對服務器的調用進行呼應的。
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
ctx.push()
error = None
try:
try:
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
複製代碼
能夠看到 Flask 按照 WSGI 的要求實現了 __call__
方法,所以成爲了一個可調用的對象。但它不是在直接在 __call__
裏寫邏輯的,而是調用了 wsgi_app
方法,這是爲了中間件的考慮,不展開談了。這個方法返回的 response(environ, start_response)
中,response
是 werkzueg.response
類的一個實例,它也是個能夠調用的對象,這個對象會負責生成最終的可遍歷的響應體,並調用 start_response
造成響應頭。
源碼閱讀:
app.Flask
的代碼。
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
ctx.push()
error = None
try:
try:
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
複製代碼
wsgi_app
方法中裏面的內容就是對請求處理過程的一個高度抽象。
首先,在接收到服務器傳過來的請求時,Flask 調用 request_context
函數創建了一個 RequestContext
請求上下文對象,並把它壓入 _request_ctx_stack
棧。關於上下文和棧的內容下文會再講到,你如今須要知道的是,這些操做是爲了 flask 在處理多個請求的時候不會混淆。以後,Flask 會調用 full_dispatch_request
方法對這個請求進行分發,開始實際的請求處理過程,這個過程當中會生成一個響應對象並最終經過調用 response
對象來返回給服務器。若是當中出錯,就聲稱相應的錯誤信息。不論是否出錯,最終 Flask 都會把請求上下文推出棧。
full_dispatch_request
是請求分發的入口,咱們再來看它的實現:
def full_dispatch_request(self):
self.try_trigger_before_first_request_functions()
try:
rv = self.preprocess_request()
if rv is None:
rv = self.dispatch_request()
except Exception as e:
rv = self.handle_user_exception(e)
return self.finalize_request(rv)
複製代碼
首先調用 try_trigger_before_first_request_functions
方法來嘗試調用 before_first_request
列表中的函數,若是 Flask
的 _got_first_request
屬性爲 False
,before_first_request
中的函數就會被執行,執行一次以後,_got_first_request
就會被設置爲 True
從而再也不執行這些函數。
而後調用 preprocess_request
方法,這個方法調用 before_request_funcs
列表中全部的方法,若是這些 before_request_funcs
方法中返回了某種東西,那麼就不會真的去分發這個請求。好比說,一個 before_request_funcs
方法是用來檢測用戶是否登陸的,若是用戶沒有登陸,那麼這個方法就會調用 abort
方法從而返回一個錯誤,Flask 就不會分發這個請求而是直接報 401 錯誤。
若是 before_request_funcs
中的函數沒有返回,那麼再調用 dispatch_request
方法進行請求分發。這個方法首先會查看 URL 規則中有沒有相應的 endpoint
和 value
值,若是有,那麼就調用 view_functions
中相應的視圖函數(endpoint
做爲鍵值)並把參數值傳入(**req.view_args
),若是沒有就由 raise_routing_exception
進行處理。視圖函數的返回值或者錯誤處理視圖函數的返回值會返回給 wsgi_app
方法中的 rv
變量。
def dispatch_request(self):
req = _request_ctx_stack.top.request
if req.routing_exception is not None:
self.raise_routing_exception(req)
rule = req.url_rule
if getattr(rule, 'provide_automatic_options', False) \
and req.method == 'OPTIONS':
return self.make_default_options_response()
return self.view_functions[rule.endpoint](**req.view_args)
def finalize_request(self, rv, from_error_handler=False):
response = self.make_response(rv)
try:
response = self.process_response(response)
request_finished.send(self, response=response)
except Exception:
if not from_error_handler:
raise
self.logger.exception('Request finalizing failed with an '
'error while handling an error')
return response
def make_response(self, rv):
if isinstance(rv, self.response_class):
return rv
if isinstance(rv, basestring):
return self.response_class(rv)
if isinstance(rv, tuple):
return self.response_class(*rv)
return self.response_class.force_type(rv, request.environ)
複製代碼
而後 Flask 就會根據 rv
生成響應,這個 make_response
方法會查看 rv 是不是要求的返回值類型,不然生成正確的返回類型。好比 Demo 中返回值是字符串,就會知足 isinstance(rv, basestring)
判斷並從字符串生成響應。這一步完成以後,Flask 查看是否有後處理視圖函數須要執行(在 process_response
方法中),並最終返回一個徹底處理好的 response
對象。
在請求處理過程一節中,咱們已經看到了 Flask 是如何調用試圖函數的,這一節咱們要關注 Flask 如何構建和請求分派相關的數據結構。咱們將主要關注 view_functions
,由於其餘的數據結構如 before_request_funcs
的構建過程大同小異,甚至更爲簡單。咱們也將仔細講解在應用的建立一節中遺留的問題,即 url_map
究竟是什麼。
Demo 代碼的第 4 行用修飾器 route
註冊一個視圖函數,這是 Flask 中受到普遍稱讚的一個設計。在 Flask 類的 route
方法中,能夠看到它調用了 add_url_rule
方法。
def route(self, rule, **options):
def decorator(f):
endpoint = options.pop('endpoint', None)
self.add_url_rule(rule, endpoint, f, **options)
return f
return decorator
def add_url_rule(self, rule, endpoint, **options):
if endpoint is None:
endpoint = _endpoint_from_view_func(view_func)
options['endpoint'] = endpoint
methods = options.pop('methods', None)
if methods is None:
methods = getattr(view_func, 'methods', None) or ('GET',)
if isinstance(methods, string_types):
raise TypeError('Allowed methods have to be iterables of strings, '
'for example: @app.route(..., methods=["POST"])')
methods = set(item.upper() for item in methods)
required_methods = set(getattr(view_func, 'required_methods', ()))
provide_automatic_options = getattr(view_func,
'provide_automatic_options', None)
if provide_automatic_options is None:
if 'OPTIONS' not in methods:
provide_automatic_options = True
required_methods.add('OPTIONS')
else:
provide_automatic_options = False
methods |= required_methods
rule = self.url_rule_class(rule, methods=methods, **options)
rule.provide_automatic_options = provide_automatic_options
self.url_map.add(rule)
if view_func is not None:
old_func = self.view_functions.get(endpoint)
if old_func is not None and old_func != view_func:
raise AssertionError('View function mapping is overwriting an '
'existing endpoint function: %s' % endpoint)
self.view_functions[endpoint] = view_func
複製代碼
這個方法負責註冊視圖函數,並實現 URL 到視圖函數的映射。首先,它要準備好一個視圖函數所支持的 HTTP 方法(基本上一半多的代碼都是在作這個),而後經過 url_rule_class
建立一個 rule
對象,並把這個對象添加到本身的 url_map
裏。咱們那個遺留問題在這裏就獲得解答了:rule
對象是一個保存合法的(Flask 應用所支持的) URL、方法、endpoint
(在 **options
中) 及它們的對應關係的數據結構,而 url_map
是保存這些對象的集合。而後,這個方法將視圖函數添加到 view_functions
當中,endpoint
做爲它的鍵,其值默認是函數名。
咱們再來深刻了解一下 rule
,它被定義在 werkzeug.routing.Rule
中:
A Rule represents one URL pattern. There are some options for
Rule
that change the way it behaves and are passed to theRule
constructor. 一個 Rule 對象表明了一種 URL 模式,能夠經過傳入參數來改變它的許多行爲。
Rule 的 __init__
方法爲:
def __init__(self, string, defaults=None, subdomain=None, methods=None, build_only=False, endpoint=None, strict_slashes=None, redirect_to=None, alias=False, host=None):
if not string.startswith('/'):
raise ValueError('urls must start with a leading slash')
self.rule = string
self.is_leaf = not string.endswith('/')
self.map = None
self.strict_slashes = strict_slashes
self.subdomain = subdomain
self.host = host
self.defaults = defaults
self.build_only = build_only
self.alias = alias
if methods is None:
self.methods = None
else:
if isinstance(methods, str):
raise TypeError('param `methods` should be `Iterable[str]`, not `str`')
self.methods = set([x.upper() for x in methods])
if 'HEAD' not in self.methods and 'GET' in self.methods:
self.methods.add('HEAD')
self.endpoint = endpoint
self.redirect_to = redirect_to
if defaults:
self.arguments = set(map(str, defaults))
else:
self.arguments = set()
self._trace = self._converters = self._regex = self._weights = None
複製代碼
一個 Rule 被建立後,經過 Map
的 add
方法被綁定到 Map
對象上,咱們以前說過 flask.url_map
就是一個 Map
對象。
def add(self, 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
複製代碼
而 Rule
的 bind
方法的內容,就是添加 Rule
對應的 Map
,而後調用 compile
方法生成一個正則表達式,compile
方法比較複雜,就不展開了。
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))
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
self.compile()
複製代碼
在 Flask 應用收到請求時,這些被綁定到 url_map
上的 Rule
會被查看,來找到它們對應的視圖函數。這是在請求上下文中實現的,咱們先前在 dispatch_request
方法中就見過——咱們是從 _request_ctx_stack.top.request
獲得 rule
並從這個 rule
找到 endpoint
,最終找到用來處理該請求的正確的視圖函數的。因此,接下來咱們須要看請求上下的具體實現,而且看一看 Flask 是如何從 url_map
中找到這個 rule
的。
def dispatch_request(self):
req = _request_ctx_stack.top.request
if req.routing_exception is not None:
self.raise_routing_exception(req)
rule = req.url_rule
if getattr(rule, 'provide_automatic_options', False) \
and req.method == 'OPTIONS':
return self.make_default_options_response()
return self.view_functions[rule.endpoint](**req.view_args)
複製代碼
源碼閱讀:
ctx.RequestContext
的代碼。
請求上下文是如何、在什麼時候被建立的呢?咱們先前也見過,在服務器調用應用的時候,Flask 的 wsgi_app
中有這樣的語句,就是建立了請求上下文並壓棧。
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
ctx.push()
複製代碼
request_context
方法很是簡單,就是建立了 RequestContext
類的一個實例,這個類被定義在 flask.ctx
文件中,它包含了一系列關於請求的信息,最重要的是它自身的 request
屬性指向了一個 Request
類的實例,這個類繼承自 werkzeug.Request
,在 RequestContext
的建立過程當中,它會根據傳入的 environ
建立一個 werkzeug.Request
的實例。
接着 RequestContext
的 push
方法被調用,這個方法將本身推到 _request_ctx_stack
的棧頂。
_request_ctx_stack
被定義在 flask.global
文件中,它是一個 LocalStack
類的實例,是 werkzeug.local
所實現的,若是你對 Python 的 threading 熟悉的話,就會發現這裏實現了線程隔離,就是說,在 Python 解釋器運行到 _request_ctx_stack
相關代碼的時候,解釋器會根據當前進程來選擇正確的實例。
可是,在整個分析 Flask 源碼的過程當中,咱們也沒發現 Flask 在被調用以後建立過線程啊,那麼爲何要作線程隔離呢?看咱們開頭提到的 run
函數,其實它能夠傳一個 threaded
參數。當不傳這個函數的時候,咱們啓動的是 BasicWSGIServer
,這個服務器是單線程單進程的,Flask 的線程安全天然沒有意義,可是當咱們傳入這個參數的時候,咱們啓動的是 ThreadedWSGIServer
,這時 Flask 的線程安全就是有意義的了,在其餘多線程的服務器中也是同樣。
這裏,咱們經過追蹤一個請求到達服務器並返回(固然是經過「變成」一個相應)的旅程,串講本文的內容。
environ
和 make_response
函數,而後調用了本身身上註冊的 Flask 應用。application(environ, make_response)
方法。在 Flask 中,這個方法是個被 __call__
中轉的叫作 wsgi_app
的方法。它首先經過 environ
建立了請求上下文,並將它推入棧,使得 flask 在處理當前請求的過程當中均可以訪問到這個請求上下文。before_first_request_funcs
before_request_funcs
view_functions
中的函數,並最終經過 finalize_request
生成一個 response
對象,當中只要有函數返回值,後面的函數組就不會再執行,after_request_funcs
進行 response
生成後的後處理。response
對象,最終調用了 make_response
函數,並返回了一個可遍歷的響應內容。在分析過程當中,能夠很明顯地看出 Flask 和 werkzeug
是強耦合的,實際上 werkzeug
是 Flask 惟一不可或缺的依賴,一些很是細節的工做,其實都是 werkzeug
庫完成的,在本文的例子中,它至少作了這些事情:
Response
和 Request
類型供 Flask 使用,在實際開發中,咱們在請求和響應對象上的操做,調用的實際上是 werkzeug
的方法。_request_ctx_stack
對 Flask 實現線程保護。對於 Flask 的源碼分析先暫時到這裏。有時間的話,我會分析 Flask 中的模板渲染、import request
、藍圖和一些好用的變量及函數,或者深刻分析 werkzeug
庫。