轉(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ù)。


關(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是其中之一。

我們先從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ù)。

在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.input的BufferedReader對(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ì)象,然后通過MapAdapter的dispatch方法來獲取匹配的Rule,但這里并沒有把Rule對(duì)象返回,而是返回了endpoint和args,那么通過endpoint就可以獲取具體的執(zhí)行函數(shù)(或者endpoint本身就是一個(gè)執(zhí)行函數(shù)),最后執(zhí)行到具體的執(zhí)行函數(shù)中, 在返回Response。
如果這么這段問題看得比較蒙圈,沒關(guān)系,現(xiàn)在就來解釋下具體的請(qǐng)求流程是怎么處理的,看下bind_to_environ和dispatch兩個(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》