天天看點

Werkzeug源碼分析

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。

Werkzeug源碼分析

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)包含一些接收資料的方法

  1. 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複制代碼      
  1. 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)複制代碼      
  1. 是完成請求環境的綁定,用于擷取請求的方法、協定、請求參數等資訊,并将擷取的請求相關資訊作為參數傳遞給MapAdapter。
  2. 是完成請求參數和路由映射的比對(本質上執行了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()複制代碼      
  1. 是對請求的路徑和路由進行一個正則比對,并傳回攜帶的請求路徑動态參數,若沒有動态參數,則傳回空字典;如果傳回的rv為None,則表示Path_info路徑沒有和目前的路由規則比對上。
      #rule.match核心代碼  m = self._regex.search(path)if m is not None:
                groups = m.groupdict() # 動态路徑字典..........複制代碼