flask 一個基於 python 實現的Web開發微框架,主要依賴:
flask 只建立 Werkezug
和 Jinja2
的橋樑,前者實現一個合適的 WSGI 應用,後者處理模板。 Flask 也綁定了一些通用的標準庫包,比如 logging
。其它所有一切取決於擴展。
一、WSGI
WSGI(Web Server Gateway Interface)的本質是一種約定,是 Python web 開發中 web 服務器與 web 應用程序之間數據交互的約定。它封裝了接受 HTTP 請求
、解析 HTTP 請求
、發送 HTTP
,響應等等的這些底層的代碼和操作,使開發者可以高效的編寫Web應用。
網關協議的本質是爲了解耦,實現 web 服務器(提供 web 服務)和 web 應用程序(資源處理)的分離,WSGI 就是一個支持 WSGI 的 web 服務器與 Python web 應用程序之間的約定。
WSGI 服務器
一個 WSGI 服務器需要實現兩個函數
:
1、解析 http 請求,爲應用程序提供 environ 字典
def get_environ(self):
env = {}
env['wsgi.version'] = (1, 0)
env['wsgi.url_scheme'] = 'http'
env['wsgi.input'] = StringIO.StringIO(self.request_data)
env['wsgi.errors'] = sys.stderr
env['wsgi.multithread'] = False
env['wsgi.multiprocess'] = False
env['wsgi.run_once'] = False
env['REQUEST_METHOD'] = self.request_method # GET
env['PATH_INFO'] = self.path # /hello
env['SERVER_NAME'] = self.server_name # localhost
env['SERVER_PORT'] = str(self.server_port) # 8888
return env
2、實現 start_response 函數
def start_response(self, status, response_headers, exc_info=None):
# Add necessary server headers
server_headers = [
('Date', 'Tue, 31 Mar 2015 12:54:48 GMT'),
('Server', 'WSGIServer 0.2'),
]
self.headers_set = [status, response_headers + server_headers]
# To adhere to WSGI specification the start_response must return
# a 'write' callable. We simplicity's sake we'll ignore that detail
# for now.
服務器與應用程序交互過程
主要過程是:
服務器從客戶端獲取到請求,然後通過
get_env
獲得env
變量。再調用應用程序,傳入env
變量(字典) 和start_response
函數, 並獲得響應。最後將響應返回給客戶端。
代碼如下:
def handle_one_request(self):
self.request_data = request_data = self.client_connection.recv(1024)
print(''.join(
'< {line}\n'.format(line=line)
for line in request_data.splitlines()
))
self.parse_request(request_data)
env = self.get_environ() #獲取 environ
result = self.application(env, self.start_response) #調用應用程序
self.finish_response(result)
在上述這個過程中,Python 應用程序主要工作就是根據輸入的 environ 字典信息生成相應的 http 報文返回給服務器。
下面是一個簡單的例子:
from wsgiref.simple_server import make_server
def simple_app(environ, start_response):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [u"This is hello wsgi app".encode('utf8')]
httpd = make_server('', 8000, simple_app)
print "Serving on port 8000..."
httpd.serve_forever()
其中:
-
environ
: 一個包含全部 HTTP 請求信息的字典,由WSGI Server
解包 HTTP 請求生成。 -
start_response
: 一個WSGI Server
提供的函數,調用可以發送響應的狀態碼和 HTTP 報文頭, 函數在返回前必須調用一次start_response()
。 -
simple_app()
應當返回一個可以迭代的對象(HTTP正文)。 -
simple_app()
函數由WSGI Server
直接調用和提供參數。 - Python 內置了一個
WSGIREF
的WSGI Server
,不過性能不是很好,一般只用在開發環境。可以選擇其他的如Gunicorn
。
總結
flask 中實現 WSGI 接口
1.通過 __call__
方法將 Flask 對象變爲可調用
def __call__(self, environ, start_response):
"""Shortcut for :attr:`wsgi_app`."""
return self.wsgi_app(environ, start_response)
2. 實現 wsgi_app
函數處理 web 服務器轉發的請求
def wsgi_app(self, environ, start_response):
"""
The actual WSGI application.
: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 responseresponse
"""
ctx = self.request_context(environ)
error = None
try:
try:
ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except:
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
二、路由
Flask 類中支持路由功能的數據結構,在 __init__
函數中初始化:
url_rule_class = Rule
self.url_map = Map()
self.view_functions = {}
Rule 和 Map 是 werkzeug
中實現的 路由類
和 映射類
。
>>> m = Map([
... # Static URLs
... Rule('/', endpoint='static/index'),
... Rule('/about', endpoint='static/about'),
... Rule('/help', endpoint='static/help'),
... # Knowledge Base
... Subdomain('something', [
... Rule('/', endpoint='something/index'),
... Rule('/browse/', endpoint='something/browse'),
... Rule('/browse/<int:id>/', endpoint='something/browse'),
... Rule('/browse/<int:id>/<int:page>', endpoint='something/browse')
... ])
... ], default_subdomain='www')
通過 Map 我們就可以實現動態路由的功能。這裏我們注意到 Map 類先建立了 url 到 endpoint 的映射,而不是直接映射到函數,這是爲什麼呢?
主要是兩個原因:
- 一是爲了實現動態路由功能,
- 二是爲不同的 url 映射到同一個視圖函數提供了便利。
而 view_functions
是一個字典,它負責建立 endpoint
和視圖函數之間的映射關係。
下面是一個小實驗,證明我們所說的映射關係:
>>> from flask import Flask
>>> app = Flask(__name__)
>>> @app.route('/')
... def index():
... return "hello world"
...
>>> app.url_map
Map([<Rule '/' (HEAD, GET, OPTIONS) -> index>,
<Rule '/static/<filename>' (HEAD, GET, OPTIONS) -> static>])
>>> app.view_functions
{'index': <function index at 0x7f6ced14c840>, 'static': <bound method _PackageBoundObject.send_static_file of <Flask '__main__'>>}
這裏我們可以看到
<Rule '/' (HEAD, GET, OPTIONS) -> index>,'index': <function index at 0x7f6ced14c840>
通過 endpoint
這個中間量,我們讓把 路由
和 函數
建立了映射關係。
要注意一下,爲什麼會有 '/static/<filename>'
這個路由呢,這是因爲在初始化時 flask 調用了 add_url_rule
函數做了如下綁定:
if self.has_static_folder:
assert bool(static_host) == host_matching, 'Invalid static_host/host_matching combination'
self.add_url_rule(
self.static_url_path + '/<path:filename>',
endpoint='static',
host=static_host,
view_func=self.send_static_file
)
總結
註冊路由
在 flask 中註冊路由有兩種方式,
- 一種是用 route 裝飾器,如上所示,
@app.route()
- 另一種是直接調用
add_url_rule
函數綁定視圖類,
但是本質上二者都是調用 add_url_rule
函數,下面我們來看一下 add_url_rule
函數的實現。
在 Flask 的 add_url_rule
函數很長,但是核心的代碼爲以下幾行:
self.url_map.add(rule)
rule = self.url_rule_class(rule, methods=methods, **options)
self.view_functions[endpoint] = view_func
1. 裝飾器
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
2. 視圖類
class CounterAPI(MethodView):
def get(self):
return session.get('counter', 0)
def post(self):
session['counter'] = session.get('counter', 0) + 1
return 'OK'
app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter'))
註冊路由之後,flask 就需要分發路由,調用相應的視圖函數。
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)