Flask源碼分析系列(1) -Werkzeug源碼分析

轉(zhuǎn)載請(qǐng)注明出處即可
源碼地址github werkzeug
主要參考文檔為werkzeug
環(huán)境為MacOS, Python 3.7+, IDE Pycharm

注意:文章中的源碼存在刪減,主要是為了減少篇幅和去除非核心邏輯,但不會(huì)影響對(duì)執(zhí)行流程的理解。

一、WSGI簡(jiǎn)介

WSGI是類似于Servlet規(guī)范的一個(gè)通用的接口規(guī)范。和Servlet類似,只要編寫的程序符合WSGI規(guī)范,就可以在支持WSGI規(guī)范的Web服務(wù)器中運(yùn)行,就像符合Servlet規(guī)范的應(yīng)用可以在Tomcat和Jetty中運(yùn)行一樣。
一個(gè)最小的Hello World的WSGI程序如下。

from wsgiref import simple_server


def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return [b'Hello World!']


http_server = simple_server.make_server('0.0.0.0', 5000, application)
http_server.serve_forever()

注意如果訪問后報(bào)了500, 錯(cuò)誤為write() argument must be a bytes instance,需要注意return時(shí), 不要直接返回字符串,需要返回bytes。

可以看到wsgi程序的定義只需要實(shí)現(xiàn)一個(gè)application即可。很簡(jiǎn)單的3行代碼就實(shí)現(xiàn)了對(duì)http請(qǐng)求的處理。其中enviorn參數(shù)是一個(gè)dict,包含了系統(tǒng)的環(huán)境變量和HTTP請(qǐng)求的相關(guān)參數(shù)。

enviorn中的系統(tǒng)環(huán)境變量

enviorn中的Http請(qǐng)求參數(shù)

關(guān)于start_response,我們現(xiàn)在這里復(fù)習(xí)下Http協(xié)議的內(nèi)容
Http Request需要包含以下部分

  • 請(qǐng)求方法 --- 統(tǒng)一資源標(biāo)識(shí)符(Uniform Resource Identifier, URI) --- 協(xié)議/版本
  • 請(qǐng)求頭(Header)
  • 實(shí)體(Body)

具體示例為:

POST /examples/default HTTP/1.1
Accept: text/plain; text/hteml
Accept-Language: en-gb
Connection: Keep-Alive
Host: locahost
User-Agent: Mozilla/4.0 (compatible; MSIE 4.0.1; Windoes 98)
Content-Length: 33
Content-Type application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate

lastName=Franks&firstName=Michael

其中body上面的空行為CRLF(\r\n), 對(duì)協(xié)議很重要,決定著request body從哪里開始解析。

Http Response需要包含以下部分

  • 協(xié)議 --- 狀態(tài)碼 --- 描述
  • 響應(yīng)頭(header)
  • 響應(yīng)實(shí)體(body)

具體示例為:

HTTP/1.1 200 OK
Server: Microsoft-IIS/4.0
Content-Type: text/plain
Content-Length: 12

Hello world!

那么現(xiàn)在再來看start_response函數(shù), 第一個(gè)參數(shù)在寫著狀態(tài)碼描述。第二個(gè)參數(shù)是一個(gè)列表,寫著response header。而application的返回值則代表著response body。

二、Werkzeug的Demo

了解了WSGI,我們?cè)倏聪氯绾问褂肳erkzeug來寫Hello World。

from wsgiref import simple_server

from werkzeug.wrappers import Response


def application(environ, start_response):
    response = Response('Hello World!', mimetype='text/plain')
    return response(environ, start_response)


http_server = simple_server.make_server('0.0.0.0', 5000, application)
http_server.serve_forever()
from wsgiref import simple_server

from werkzeug.wrappers import Request, Response


def application(environ, start_response):
    request = Request(environ)
    text = 'Hello %s!' % request.args.get('name', 'World')
    response = Response(text, mimetype='text/plain')
    return response(environ, start_response)


http_server = simple_server.make_server('0.0.0.0', 5000, application)
http_server.serve_forever()

在這里可以看到Werkzeug的作用,如果自己手寫WSGI的程序的話,需要自己解析environ,以及自己處理返回值。而使用了Werkzeug就可以通過該庫所提供的Request和Response來簡(jiǎn)化開發(fā)。正如官網(wǎng)的介紹Werkzeug is a utility library for WSGI

在這篇文章中主要分析Werkzeug是如何實(shí)現(xiàn)相關(guān)的工具,進(jìn)而簡(jiǎn)化WSGI程序的開發(fā)的。了解
Werkzeug也為后續(xù)理解Flask打下了堅(jiān)實(shí)的基礎(chǔ)。

三、Werkzeug提供的工具

(1) Request和Response對(duì)象,方便處理請(qǐng)求和響應(yīng)
(2) Map、Rule以及MapAdapter,方便處理請(qǐng)求路由
(3) WSGI Helper, 比如一些編解碼的處理,以及一些方便對(duì)stream的處理等。
(4) Context Locals提供了Local,類似于Java的ThreadLocal
(5) Http Exception用于處理相關(guān)的異常,比如404等。
(6) http.py中還提供了很多的http code和header的定義
除了這些工具還有很多,具體可以查看下官網(wǎng)。

在這篇文章中重點(diǎn)來解析Request和Response以及路由相關(guān)的源碼。

四、wrappers分析

在Werkzeug并沒有多少的包, wrappers是其中之一。


wrappers包

我們先從request = Request(environ)這行代碼入手。分析Request。
注意,下面的復(fù)制粘貼的源碼會(huì)刪除掉與主流程不太相關(guān)的代碼。方便理解核心流程。

(1) class Request分析

首先,其實(shí)不用多說也知道Request無非是解析了environdict而已。
Request繼承了很多類,可以看到存在著Accept、ETAG、CORS等相關(guān)Header的解析

class Request(
    BaseRequest, 
    AcceptMixin, 
    ETagRequestMixin, 
    UserAgentMixin,
    AuthorizationMixin,
    CORSRequestMixin,
    CommonRequestDescriptorsMixin,
):

BaseRequest的構(gòu)造方法為

    def __init__(self, environ, populate_request=True, shallow=False):
        self.environ = environ
        if populate_request and not shallow:
            self.environ["werkzeug.request"] = self
        self.shallow = shallow

因?yàn)镽equest的方法和屬性眾多,這里找?guī)讉€(gè)比較常見的來分析下實(shí)現(xiàn)。

1. request.query_string和request.method

query_string = environ_property(
        "QUERY_STRING",
        "",
        read_only=True,
        load_func=lambda x: x.encode("latin1"),
        doc="The URL parameters as raw bytes.",

environ_property是一個(gè)類,實(shí)現(xiàn)了一個(gè)lookup方法,這個(gè)obj其實(shí)傳的就是Request,其實(shí)lookup的調(diào)用其實(shí)就是獲取了environ dict。

class environ_property(_DictAccessorProperty):
   read_only = True

   def lookup(self, obj):
       return obj.environ

environ_property繼承了_DictAccessorProperty其中的__get__方法實(shí)現(xiàn)為

def __get__(self, obj, type=None):
    if obj is None:
        return self
    storage = self.lookup(obj)
    if self.name not in storage:
        return self.default
    rv = storage[self.name]
    if self.load_func is not None:
        try:
            rv = self.load_func(rv)
        except (ValueError, TypeError):
            rv = self.default
    return rv

可以看到先通過lookup方法獲取了environ dict,也就是stroage變量,然后在獲取了rv。也就是environdict里面的key='QUERY_STRING'的value。
其實(shí)獲取method(GET, POST)也是一樣的實(shí)現(xiàn)

method = environ_property(
        "REQUEST_METHOD",
        "GET",
        read_only=True,
        load_func=lambda x: x.upper(),
        doc="The request method. (For example ``'GET'`` or ``'POST'``).",
    )

2. request.data

這個(gè)是獲取Request Body, 在environ dict中,通過wsgi.input來獲取的BufferedReader類來讀取body中的數(shù)據(jù)。

wsgi.input

在Werkzeug中的實(shí)現(xiàn)也是類似的,具體源碼如下。

@cached_property
def data(self):
    return self.get_data(parse_form_data=True)

def get_data(self, cache=True, as_text=False, parse_form_data=False):
    rv = getattr(self, "_cached_data", None)
    if rv is None:
        if parse_form_data:
            self._load_form_data()
        rv = self.stream.read()
        if cache:
            self._cached_data = rv
    if as_text:
        rv = rv.decode(self.charset, self.encoding_errors)
    return rv

主要分析下self.stream.read()這行

@cached_property
def stream(self):
    return get_input_stream(self.environ)

def get_input_stream(environ, safe_fallback=True):
    stream = environ["wsgi.input"]
    content_length = get_content_length(environ)

    if environ.get("wsgi.input_terminated"):
        return stream

    if content_length is None:
        return io.BytesIO() if safe_fallback else stream

    return LimitedStream(stream, content_length)

簡(jiǎn)單來說就是獲取wsgi.inputBufferedReader對(duì)象,然后判斷下是否存在content_length(http request header里面正常情況下都會(huì)有),創(chuàng)建LimitedStream類,最多只讀取content_length長(zhǎng)度的內(nèi)容。
如果content_length不存在的話,則判斷了是否設(shè)置了safe_fallback=True,會(huì)返回空的BytesIO對(duì)象,默認(rèn)是True。

3. request.args

這里的實(shí)現(xiàn)就不詳細(xì)解釋了,無非就是獲取QUERY_STRING,然后通過&進(jìn)行分割,然后在用=切個(gè),前面的作為key, 后面的作為value而已。需要注意的是這里用了MultiDict,目的是為了同一個(gè)鍵的存儲(chǔ)多個(gè)值。

def url_decode(
    s,
    charset="utf-8",
    decode_keys=None,
    include_empty=True,
    errors="replace",
    separator="&",
    cls=None,
):
    if cls is None:
        from .datastructures import MultiDict

        cls = MultiDict
    if isinstance(s, str) and not isinstance(separator, str):
        separator = separator.decode(charset or "ascii")
    elif isinstance(s, bytes) and not isinstance(separator, bytes):
        separator = separator.encode(charset or "ascii")
    return cls(_url_decode_impl(s.split(separator), charset, include_empty, errors))

4. request.path

path獲取的是environ中的PATH_INFO,然后最后一行處理了這種情況,比如http://localhost:5000//default,如果多寫了/,在這里會(huì)比換成單個(gè)/>

def path(self):
    raw_path = _wsgi_decoding_dance(
        self.environ.get("PATH_INFO") or "", self.charset, self.encoding_errors
    )
    return "/" + raw_path.lstrip("/")

(2) class Response分析

Response類的核心功能有兩個(gè),一個(gè)是通過一定的封裝構(gòu)造返回值,另一個(gè)是返回一個(gè)符合WSGI規(guī)范的函數(shù)。具體的實(shí)現(xiàn)比較簡(jiǎn)單不在詳述。

# Response的init函數(shù)
def __init__(
    self,
    response=None,
    status=None,
    headers=None,
    mimetype=None,
    content_type=None,
    direct_passthrough=False,
)

# Response的call函數(shù)
def __call__(self, environ, start_response):
    app_iter, status, headers = self.get_wsgi_response(environ)
    start_response(status, headers)
    return app_iter

五、Map、Rule和MapAdapter

以一個(gè)Demo為例, 看下這三個(gè)類的使用。

from wsgiref import simple_server

from werkzeug.routing import Map, Rule, HTTPException
from werkzeug.wrappers import Response, Request

url_map = Map([
    Rule('/test1', endpoint='test1'),
    Rule('/test2', endpoint='test2'),
])


def test1(request, **args):
    return Response('test1')


def test2(request, **args):
    return Response('test2')


views = {'test1': test1, 'test2': test2}


def application(environ, start_response):
    request = Request(environ)
    try:
        return url_map.bind_to_environ(environ) \
            .dispatch(
            lambda endpoint, args: views[endpoint](request, **args)
        )(environ, start_response)
    except HTTPException as e:
        return e(environ, start_response)


http_server = simple_server.make_server('0.0.0.0', 5000, application)
http_server.serve_forever()

其中每個(gè)Rule都代表著一個(gè)URL匹配模式,并且第一個(gè)參數(shù)string是可以放<converter(arguments):name>,比如/all/page/<int:page>。endpoint可以放字符串,函數(shù)等等,代表著如果匹配到相應(yīng)的路徑,則返回endpoint的值。因?yàn)榇蟛糠謶?yīng)用至少會(huì)有1個(gè)接口,所以Rule的存在意義是可以定義一個(gè)path到具體的處理函數(shù)(或者用字符串表示函數(shù))的一個(gè)映射,簡(jiǎn)化了多接口的開發(fā)。
Map可以存放多個(gè)Rule,并且在調(diào)用bind_to_environ函數(shù)后,返回一個(gè)MapAdapter對(duì)象,然后通過MapAdapterdispatch方法來獲取匹配的Rule,但這里并沒有把Rule對(duì)象返回,而是返回了endpointargs,那么通過endpoint就可以獲取具體的執(zhí)行函數(shù)(或者endpoint本身就是一個(gè)執(zhí)行函數(shù)),最后執(zhí)行到具體的執(zhí)行函數(shù)中, 在返回Response。

如果這么這段問題看得比較蒙圈,沒關(guān)系,現(xiàn)在就來解釋下具體的請(qǐng)求流程是怎么處理的,看下bind_to_environdispatch兩個(gè)函數(shù)具體的執(zhí)行邏輯。
根據(jù)上面的Demo代碼,接到請(qǐng)求后,首先通過bind_to_environ函數(shù)獲取了MapAdapter


def bind_to_environ(self, environ, server_name=None, subdomain=None):
    environ = _get_environ(environ)
    wsgi_server_name = get_host(environ).lower()
    scheme = environ["wsgi.url_scheme"]

    # 存在刪減

    def _get_wsgi_string(name):
        val = environ.get(name)
        if val is not None:
            return _wsgi_decoding_dance(val, self.charset)

    script_name = _get_wsgi_string("SCRIPT_NAME")
    path_info = _get_wsgi_string("PATH_INFO")
    query_args = _get_wsgi_string("QUERY_STRING")
    return Map.bind(
        self,
        server_name,
        script_name,
        subdomain,
        scheme,
        environ["REQUEST_METHOD"],
        path_info,
        query_args=query_args,
    )

主體邏輯可以理解為是通過environ獲取了部分參數(shù),然后在調(diào)用bind方法。bind方法,最后其實(shí)就是通過這些參數(shù)創(chuàng)建了MapAdapter對(duì)象

def bind(
    self,
    server_name,
    script_name=None,
    subdomain=None,
    url_scheme="http",
    default_method="GET",
    path_info=None,
    query_args=None,
):
    # 存在刪減
    return MapAdapter(
        self,
        server_name,
        script_name,
        subdomain,
        url_scheme,
        path_info,
        default_method,
        query_args,
    )

然后在來看下dispatch函數(shù)

def dispatch(
    self, view_func, path_info=None, method=None, catch_http_exceptions=False
):
    try:
        try:
            endpoint, args = self.match(path_info, method)
        except RequestRedirect as e:
            return e
        return view_func(endpoint, args)
    except HTTPException as e:
        if catch_http_exceptions:
            return e
        raise

dispatch函數(shù)很簡(jiǎn)單,上面的邏輯

return url_map.bind_to_environ(environ) \
            .dispatch(
            lambda endpoint, args: views[endpoint](request, **args)
        )(environ, start_response)

可以改寫為

endpoint, args = url_map.bind_to_environ(environ).match()
return views[endpoint](request, **args)(environ, start_response)

dispatch只是用view_func接了下尋找具體的執(zhí)行函數(shù)的過程而已。然后重點(diǎn)看下match方法。
去掉了websocket和redirect的邏輯后,代碼如下。

def match(
    self,
    path_info=None,
    method=None,
    return_rule=False,
    query_args=None,
    websocket=None,
):

    for rule in self.map._rules:
        try:
            rv = rule.match(path, method)
        except:
            pass
        if rv is None:
            continue
        if rule.methods is not None and method not in rule.methods:
            have_match_for.update(rule.methods)
            continue

        if return_rule:
            return rule, rv
        else:
            return rule.endpoint, rv


    raise NotFound()

其實(shí)可以看到,對(duì)于path到Rule的匹配是通過for循環(huán)來進(jìn)行的。rule.match用來判斷path和Rule是否匹配,然后在判斷對(duì)應(yīng)的methods是否匹配,如果是匹配的則終止循環(huán),返回了endpoint。
筆者曾經(jīng)在flask上擴(kuò)展了一個(gè)根據(jù)版本號(hào)的路由@app.route('/main.json', version=['<=1.3'])類似于這樣。支持了以下幾種版本號(hào)的定義。

R: 1.6
R0: 1.6-1.9
R1: =1.6
R2: > 1.6
R3: < 1.6
R4: >=1.6
R5: <=1.6

所做的更改就是在match方法這里進(jìn)行的處理,具體的邏輯寫在了判斷methods之后。

if rule.methods is not None and method not in rule.methods:
    have_match_for.update(rule.methods)
    continue

# determine version
version = get_version(self.request)
if self.request and version:
    if not isinstance(rule.version, list) or not rule.version:
        rule.version = list()

    version_list = self.version_dict.get(rule.rule)

    if len(rule.version) == 0 \
            and version_list is not None \
            and determine_version(version, version_list):
        continue
    elif len(rule.version) != 0 and not determine_version(version, rule.version):
        continue

最后在說下rule.match(path)方法,是通過正則判斷是否匹配來判斷path是否和Rule匹配的。
routing.py源碼第855行。

self._regex = re.compile(regex)

routing.py源碼第871行的Rule.match方法

m = self._regex.search(path)

六、結(jié)束語

本文主要分析了Werkzeug部分核心源碼,下篇文章打算分析下Flask是如何用Werkzeug提供的工具來構(gòu)造了一個(gè)優(yōu)秀的框架。

參考

https://werkzeug.palletsprojects.com/en/1.0.x/#
《深入剖析Tomcat》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容