Flask 請求處理流程(一):WSGI 和 路由

flask 一個基於 python 實現的Web開發微框架,主要依賴:

  • Werkzeug:一個 python 的 WSGI 工具包,也可以作爲一個 Web 框架的底層庫。
  • Jinja2:爲 python 提供的一個功能齊全的模板引擎

flask 只建立 WerkezugJinja2 的橋樑,前者實現一個合適的 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 內置了一個 WSGIREFWSGI 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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章