1.引入
Werkzeug是WSGI協定的一個工具包,封裝了服務請求響應相關的實作。
2. WSGI
WSGI(web service gateway interface)定義了Web伺服器與Web應用之間交流的标準接口或規範。
它規定了Web程式必須有一個可調用的對象(可以是一個函數,可以是一個類),且該可調用對象接收兩個參數,傳回一個可疊代的對象。
- environ : 是一個字典,包含請求相關的所有資訊。
- start_response : 是一個函數,用于發起響應,包含狀态碼,響應參數等。
2.1 基于WSGI協定實作的伺服器和用戶端
- 用戶端
class application():def __init__(self):passdef __call__(self, environ, start_response):status = "200 OK"headrs = [('Content-Type', 'application/json')]
start_response(status, headrs)
print(environ)return str(environ['wsgi.input'])
application = application()複制代碼
- 服務端
from wsgiref.simple_server import make_serverfrom client import application'''
内置的WSGI伺服器wsgiref,simple_server服務于WSGI的http伺服器。
'''httpd = make_server('localhost', 9090, application)
httpd.serve_forever()複制代碼
2.1.1 解析
make_server 函數的描述
def make_server(host, port, app, server_class=WSGIServer, handler_class=WSGIRequestHandler):"""Create a new WSGI server listening on `host` and `port` for `app`"""server = server_class((host, port), handler_class)
server.set_app(app) #設定可調用應用return server複制代碼
該函數接在接收到host和port後,為該應用程式指定了WSGI伺服器,用于接收請求,同時指定了處理該請求的類WSGIRequestHandler。
WSGI應用請求分析
3.Werkzeug
- 中間件(middleware)
- 請求響應封裝(wrappers)
- 異常類
- 本地上下文(local)
- 路由(routing)
3.1 middleware
- dispatcher 程式排程中間件
#用戶端
from werkzeug.middleware.dispatcher import DispatcherMiddleware
def app1(env, start_response): start_response('200 OK', [('Content-Type', 'application/json')]) return str(env['wsgi.run_once'])
def app2(env, start_response): start_response('200 OK', [('Content-Type', 'application/json')]) return str(env['wsgi.input'])
app = DispatcherMiddleware(app1, {'/12': app2}) '''
實作了call方法,接收env和start_response參數
'''
# 服務端
from werkzeug.serving import run_simple from client import app
run_simple('localhost', 9090, app, use_reloader=True)
複制代碼
對于不同的請求配置設定到不同的WSGI應用進行處理,預設請求的應用是app1,若請求以/12結尾,則會請求app2應用。
run_simple 内部調用兩個方法(make_server 和serve_forever),使用的預設接收請求和處理請求的類是BaseWSGIServer、WSGIRequestHandler
- http_proxy 代理中間件 (用于請求外部伺服器)
from werkzeug.middleware.http_proxy import ProxyMiddlewaredef app1(env, start_response):start_response('200 OK', [('Content-Type', 'application/json')])return str(env['wsgi.run_once'])
app = ProxyMiddleware(app1, {'/12/': {'target':"http://localhost:9000",
}})複制代碼
ProxyMiddleware的call方法
def __call__(self, environ, start_response):path = environ["PATH_INFO"]
app = self.appfor prefix, opts in self.targets.items():if path.startswith(prefix):
app = self.proxy_to(opts, path, prefix)breakreturn app(environ, start_response)#proxy_toproxy_to 建構外部伺服器的請求if target.scheme == "http":
con = client.HTTPConnection(
target.ascii_host, target.port or 80, timeout=self.timeout
)elif target.scheme == "https":
con = client.HTTPSConnection(
target.ascii_host,
target.port or 443,
timeout=self.timeout,
context=opts["ssl_context"],
)else:raise RuntimeError("Target scheme must be 'http' or 'https', got '{}'.".format(
target.scheme
)
)
con.connect()複制代碼
- shared_data 靜态資源通路中間件
from werkzeug.middleware.shared_data import SharedDataMiddlewareimport osdef app1(env, start_response):start_response('200 OK', [('Content-Type', 'application/json')])return str(env['wsgi.run_once'])
app = SharedDataMiddleware(app1, {'/static':os.path.join(os.path.dirname(__file__),'1.jpg')})#若在服務端條件了參數static_files,則用戶端可以不使用SharedDataMiddleware中間件if static_files:from .middleware.shared_data import SharedDataMiddleware
application = SharedDataMiddleware(application, static_files)複制代碼
SharedDataMiddleware的call方法主要代碼為:wrap_file(environ, f) f為檔案句柄,其實際執行了一個FileWrapper檔案裝飾器,其内部實作了疊代器的協定方法
def __iter__(self):return selfdef __next__(self):data = self.file.read(self.buffer_size)if data:return dataraise StopIteration()複制代碼
3.2 wrappers
from werkzeug.wrappers import Request,Response#方式1def app(env, start_response):request = Request(env)
resp = Response('hello werkzeug from ' + str(request.environ))return resp(env, start_response)#方式[email protected] app1(request):return Response('hello werkzeug from '+ str(request.environ['HTTP_USER_AGENT']))複制代碼
對于方式1,Request類對env進行包裝,将env指派給其屬性變量environ,傳回的resp是一個滿足wsgi協定的應用。
def __call__(self, environ, start_response):"""Process this response as WSGI application.
:param environ: the WSGI environment.
:param start_response: the response callable provided by the WSGI
server.
:return: an application iterator
"""app_iter, status, headers = self.get_wsgi_response(environ)
start_response(status, headers)return app_iter複制代碼
對于方式2,使用到了裝飾器,其中Request類的application是一個類方法classmethod,其内部函數為:
'''
f即為被裝飾的函數```app1```, 位置參數args為支援wsgi協定的預設應用入口的參數(environ,start_response)。
'''@classmethoddef application(cls, f):def application(*args):request = cls(args[-2]) #等價于Request(environ)with request:try:#移除原協定參數,同時将request作為請求參數,即為原app1(request)resp = f(*args[:-2] + (request,))except HTTPException as e:
resp = e.get_response(args[-2])return resp(*args[-2:]) #即resp(environ,start_response)return update_wrapper(application, f)#update_wrapper 用于保留裝飾後原函數的屬性,即對(__dict__,__name__等)進行拷貝複制代碼
方式2其實是執行方式1,是等價的。
request=Request(environ)包含一些接收資料的方法
- request.args 接收get請求的參數,其類型為ImmutableMultiDict,不可變的多值字典對象,繼承标準字典的所有方法。
第一種情況:當請求參數為一個鍵對應的多個值時,通過字典的方法,則隻會取到第一組資料。此時應使用ImmutableMultiDict的lists()方法拿到資料([('name', [u'123', u'222'])])
第二種情況:鍵值對一一對應,即可以通過lists()方法取資料,字典的方法取資料。
>>> d = MultiDict([('a', 'b'), ('a', 'c')])
>>> d
MultiDict([('a', 'b'), ('a', 'c')])
>>> d['a']'b'>>> d.getlist('a')
['b', 'c']
>>> 'a' in dTrue複制代碼
- request.get_data()用于接收post請求的參數,類型為字典序列化後的字元串。如使用request.data接收請求參數,則請求時需要指定contentType(非application/x-www-form-urlencoded),否則接收到的資料為空。
3.3 local
本地上下文主要使用到了協程greenlet, local其實質是在全局環境下,保證資料的隔離性,不同協程間的資料互不幹擾。
from werkzeug.local import Localfrom greenlet import getcurrent as get_ident , greenlet
l = Local()def t1():l.a = 12l.A = 34print('協程t1:%s'% l.__ident_func__(), l.a)
gr2.switch()
l.la = 'greenlet'def t2():l.b = 21l.B = 43print('協程t2:%s'% l.__ident_func__(), l.b)
gr1.switch()if __name__ == "__main__":
l.m = 8l.M = 9print('預設協程:%s'%l.__ident_func__(), l.m)
gr1 = greenlet(t1)
gr2 = greenlet(t2)
gr1.switch()
print('協程開辟獨立空間存儲的資料:')for k, v in l:
print('%s==%s'%(k, v))複制代碼
=====out=====
預設協程:<greenlet.greenlet object at 0x10f4b9c00> 8協程t1:<greenlet.greenlet object at 0x10f4b9e10> 12協程t2:<greenlet.greenlet object at 0x10f4b9ec0> 21協程開辟獨立空間存儲的資料:
<greenlet.greenlet object at 0x10f4b9c00>=={'m': 8, 'M': 9}
<greenlet.greenlet object at 0x10f4b9e10>=={'a': 12, 'A': 34, 'la': 'greenlet'}
<greenlet.greenlet object at 0x10f4b9ec0>=={'b': 21, 'B': 43}複制代碼
local内部實作了( Local, LocalStack )來實作資料的隔離,本質上是字典,隻是字典的鍵為協程的id。
try:from greenlet import getcurrent as get_identexcept ImportError:try:from thread import get_identexcept ImportError:from _thread import get_identclass Local(object):__slots__ = ("__storage__", "__ident_func__")def __init__(self):object.__setattr__(self, "__storage__", {})object.__setattr__(self, "__ident_func__", get_ident) def __release_local__(self): #協程執行完釋放資料存儲空間self.__storage__.pop(self.__ident_func__(), None)複制代碼
LocalManager是對local對象進行管理,其主要的功能就是存儲請求資料,及完成請求後,對存儲空間進行資源釋放。
from werkzeug.local import Local, LocalManagerfrom werkzeug.wrappers import Request, Response
local = Local()
local_manager = LocalManager(local)def app(env, start_response):local.request = request = Request(env)
resp = Response('hello werkzeug from ' + str(request.environ))for k, v in local:
print(k, v)return resp(env, start_response)
app = local_manager.middleware(app)複制代碼
3.4 routing
from werkzeug.routing import Map, Rulefrom werkzeug.wrappers import Request, Responsedef get_message(request):return Response('路由'+ str(request.environ))def get_number(request):return Response('123')
rule1 = Rule('/message', endpoint=get_message) #建立路徑到視圖的映射rule2 = Rule('/number', endpoint=get_number)
url_map = Map([rule1, rule2]) #存儲路由映射def app(env, start_response):request = Request(env)
adapter = url_map.bind_to_environ(env) #1endpoint, values = adapter.match() #2resp = endpoint(request)return resp(env, start_response)複制代碼
- 是完成請求環境的綁定,用于擷取請求的方法、協定、請求參數等資訊,并将擷取的請求相關資訊作為參數傳遞給MapAdapter。
- 是完成請求參數和路由映射的比對(本質上執行了rule的match方法),并對請求方法進行檢查
for rule in self.map._rules:try:
rv = rule.match(path, method) #3except RequestSlash :#内部請求錯誤.......if rv is None:continueif rule.methods is not None and method not in rule.methods:
have_match_for.update(rule.methods)return rule.endpoint, rvif have_match_for: #路由疊代完成後,如果存在have_match_for,會報MethodNotAllowed錯誤raise MethodNotAllowed(valid_methods=list(have_match_for))raise NotFound()複制代碼
- 是對請求的路徑和路由進行一個正則比對,并傳回攜帶的請求路徑動态參數,若沒有動态參數,則傳回空字典;如果傳回的rv為None,則表示Path_info路徑沒有和目前的路由規則比對上。
#rule.match核心代碼 m = self._regex.search(path)if m is not None:
groups = m.groupdict() # 動态路徑字典..........複制代碼