淺談flask源碼之請求過程

 

 更新時間:2018年07月26日 09:51:36   做者:Dear、   我要評論python

 
這篇文章主要介紹了淺談flask源碼之請求過程,小編以爲挺不錯的,如今分享給你們,也給你們作個參考。一塊兒跟隨小編過來看看吧
 

Flasknginx

Flask是什麼?web

Flask是一個使用 Python 編寫的輕量級 Web 應用框架, 讓咱們可使用Python語言快速搭建Web服務, Flask也被稱爲 "microframework" ,由於它使用簡單的核心, 用 extension 增長其餘功能數據庫

爲何選擇Flask?flask

咱們先來看看python如今比較流行的web框架服務器

  • Flask
  • Django
  • Tornado
  • Sanic

Flask: 輕, 組件間鬆耦合, 自由、靈活,可擴展性強,第三方庫的選擇面廣的同時也增長了組件間兼容問題session

Django: Django至關於一個全家桶, 幾乎包括了全部web開發用到的模塊(session管理、CSRF防僞造請求、Form表單處理、ORM數據庫對象化、模板語言), 可是相對應的會形成一個緊耦合的狀況, 對第三方插件不太友好app

Tornado: 底層經過eventloop來實現異步處理請求, 處理效率高, 學習難度大, 處理稍有不慎很容易阻塞主進程致使不能正常提供服務, 新版本也支持asyncio框架

Sanic: 一個類Flask框架, 可是底層使用uvloop進行異步處理, 可使用同步的方式編寫異步代碼, 並且運行效率十分高效.異步

WSGI

先來看看維基百科對WSGI的定義

Web服務器網關接口(Python Web Server Gateway Interface,縮寫爲WSGI)是爲Python語言定義的Web服務器和Web應用程序或框架之間的一種簡單而通用的接口.

何爲網關, 即從客戶端發出的每一個請求(數據包)第一個到達的地方, 而後再根據路由進行轉發處理. 而對於服務端發送過來的消息, 老是先經過網關層, 而後再轉發至客戶端

那麼可想而知, WSGI實際上是做爲一個網關接口, 來接受Server傳遞過來的信息, 而後經過這個接口調用後臺app裏的view function進行響應.

先看一段有趣的對話:

Nginx:Hey, WSGI, 我剛收到了一個請求,我須要你做些準備, 而後由Flask來處理這個請求.
WSGI:OK, Nginx. 我會設置好環境變量, 而後將這個請求傳遞給Flask處理.
Flask:Thanks. WSGI給我一些時間,我將會把請求的響應返回給你.
WSGI:Alright, 那我等你.
Flask:Okay, 我完成了, 這裏是請求的響應結果, 請求把結果傳遞給Nginx.
WSGI:Good job! Nginx, 這裏是響應結果, 已經按照要求給你傳遞回來了.
Nginx:Cool, 我收到了, 我把響應結果返回給客戶端.你們合做愉快~

對話裏面能夠清晰瞭解到WSGI、nginx、Flask三者的關係

下面來看看Flask中的wsgi接口(注意:每一個進入Flask的請求都會調用Flask.__call__)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 摘自Flask源碼 app.py
class Flask(_PackageBoundObject):
   # 中間省略
   def __call__( self , environ, start_response):
     return self .wsgi_app(environ, start_response)
       
   def wsgi_app( self , environ, start_response):
     # environ: 一個包含所有HTTP請求信息的字典, 由WSGI Server解包HTTP請求生成
     # start_response: WSGI Server提供的函數, 調用能夠發送響應的狀態碼和HTTP報文頭,
     # 函數在返回前必須調用一次.
     :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 response.
     # 建立上下文
     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)

wsgi_app中定義的就是Flask處理一個請求的基本流程,
1.建立上下文
2.把上下文入棧
3.分發請求
4.上下文出棧
5.返回結果

其中response = self.full_dispatch_request()請求分發的過程咱們須要關注一下

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 摘自Flask源碼 app.py
class Flask(_PackageBoundObject):
   # 中間省略
   def full_dispatch_request( self ):
     self .try_trigger_before_first_request_functions()
     try :
       request_started.send( self )
       rv = self .preprocess_request()
       if rv is None :
         rv = self .dispatch_request()
     except Exception as e:
       rv = self .handle_user_exception(e)
     return self .finalize_request(rv)
 
   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)
 
   def finalize_request( self , rv, from_error_handler = False ):
     response = self .make_response(rv)
     try :
       response = self .process_response(response)
       request_finished.send( self , response = response)
     except Exception:
       if not from_error_handler:
         raise
       self .logger.exception( 'Request finalizing failed with an '
                  'error while handling an error' )
     return response

咱們能夠看到, 請求分發的操做實際上是由dispatch_request來完成的, 而在請求進行分發的先後咱們能夠看到Flask進行了以下操做:
1.try_trigger_before_first_request_functions, 首次處理請求前的操做,經過@before_first_request定義,能夠進行數據庫鏈接
2.preprocess_request, 每次處理請求前進行的操做, 經過@before_request來定義, 能夠攔截請求
3.process_response, 每次正常處理請求後進行的操做, 經過@after_request來定義, 能夠統計接口訪問成功的數量
4.finalize_request, 把視圖函數的返回值轉換成一個真正的響應對象

以上的這些是Flask提供給咱們使用的鉤子(hook), 能夠根據自身需求來定義,
而hook中還有@teardown_request, 是在每次處理請求後執行(不管是否有異常), 因此它是在上下文出棧的時候被調用

若是同時定義了四種鉤子(hook), 那麼執行順序應該是

graph LR
before_first_request --> before_request
before_request --> after_request
after_request --> teardown_request

在請求函數和鉤子函數之間,通常經過全局變量g實現數據共享

如今的處理流程就變爲:

1.建立上下文
2.上下文入棧
3.執行before_first_request操做(若是是第一次處理請求)
4.執行before_request操做
5.分發請求
6.執行after_request操做
7.執行teardown_request操做
8.上下文出棧
9.返回結果

其中3-7就是須要咱們完成的部分.

如何使用Flask

上面咱們知道, Flask處理請求的步驟, 那麼咱們來試試

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from flask import Flask
app = Flask(__name__)
 
 
@app .before_first_request
def before_first_request():
   print ( 'before_first_request run' )
 
 
@app .before_request
def before_request():
   print ( 'before_request run' )
 
 
@app .after_request
def after_request(param):
   print ( 'after_request run' )
   return param
 
@app .teardown_request
def teardown_request(param):
   print ( 'teardown_request run' )
 
 
@app .route( '/' )
def hello_world():
   return 'Hello World!'
 
 
if __name__ = = '__main__' :
   app.run()

當運行flask進程時, 訪問127.0.0.1:5000, 程序輸出, 正好認證了咱們以前說的執行順序.

before_first_request run
before_request run
after_request run
teardown_request run
127.0.0.1 - - [03/May/2018 18:42:52] "GET / HTTP/1.1" 200 -

路由分發

看了上面的代碼, 咱們可能仍是會有疑問, 爲何咱們的請求就會跑到hello world 函數去處理呢?咱們先來普及幾個知識點:

  • url: 客戶端訪問的網址
  • view_func: 即咱們寫的視圖函數
  • rule: 定義的匹配路由的地址
  • url_map: 存放着rule與endpoint的映射關係
  • endpoint: 能夠看做爲每一個view_func的ID
  • view_functions: 一個字典, 以endpoint爲key, view_func 爲value

添加路由的方法:

1.@app.route
2.add_url_rule

咱們先來看看@app.route幹了什麼事情

?
1
2
3
4
5
6
7
8
9
# 摘自Flask源碼 app.py
class Flask(_PackageBoundObject):
   # 中間省略
   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

咱們能夠看到, route函數是一個裝飾器, 它在執行時會先獲取endpoint, 而後再經過調用add_url_rule來添加路由, 也就是說全部添加路由的操做其實都是經過add_url_rule來完成的. 下面咱們再來看看add_url_rule.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 摘自Flask源碼 app.py
class Flask(_PackageBoundObject):
   # 中間省略
   # 定義view_functions
   self .view_functions = {}
   # 定義url_map
   self .url_map = Map ()
   
   def add_url_rule( self , rule, endpoint = None , view_func = None ,
            provide_automatic_options = None , * * options):
     # 建立rule
     rule = self .url_rule_class(rule, methods = methods, * * options)
     rule.provide_automatic_options = provide_automatic_options
     # 把rule添加到url_map
     self .url_map.add(rule)
     if view_func is not None :
       old_func = self .view_functions.get(endpoint)
       if old_func is not None and old_func ! = view_func:
         raise AssertionError( 'View function mapping is overwriting an '
                    'existing endpoint function: %s' % endpoint)
       # 把view_func 添加到view_functions字典
       self .view_functions[endpoint] = view_func

能夠看到, 當咱們添加路由時, 會生成一個rule, 並把它存放到url_map裏頭, 而後把view_func與其對應的endpoint存到字典.

當一個請求進入時, Flask會先根據用戶訪問的Url到url_map裏邊根據rule來獲取到endpoint, 而後再利用view_functions獲取endpoint在裏邊所對應的視圖函數

graph LR
url1 -->url_map
url2 -->url_map
url3 -->url_map
urln -->url_map
url_map --> endpoint
endpoint --> view_functions

上下文管理

下面咱們再來看看以前一直忽略的上下文,什麼是上下文呢?

上下文即語境、語意,是一句話中的語境,也就是語言環境. 一句莫名其妙的話出現會讓人不理解什麼意思, 若是有語言環境的說明, 則會更好, 這就是語境對語意的影響. 而對應到程序裏每每就是程序中須要共享的信息,保存着程序運行或交互中須要保持或傳遞的信息.

Flask中有兩種上下文分別爲:應用上下文(AppContext)和請求上下文(RequestContext). 按照上面提到的咱們很容易就聯想到:應用上下文就是保存着應用運行或交互中須要保持或傳遞的信息, 如當前應用的應用名, 當前應用註冊了什麼路由, 又有什麼視圖函數等. 而請求上下文就保存着處理請求過程當中須要保持或傳遞的信息, 如此次請求的url是什麼, 參數又是什麼, 請求的method又是什麼等.

 

咱們只須要在須要用到這些信息的時候把它從上下文中取出來便可. 而上下文是有生命週期的, 不是全部時候都能獲取到.

上下文生命週期:

  • RequestContext: 生命週期在處理一次請求期間, 請求處理完成後生命週期也就結束了.
  • AppContext: 生命週期最長, 只要當前應用還在運行, 就一直存在. (應用未運行前並不存在)

那麼上下文是在何時建立的呢?咱們又要如何建立上下文: 剛纔咱們提到, 在wsgi_app處理請求的時候就會先建立上下文, 那個上下文實際上是請求上下文, 那應用上下文呢?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 摘自Flask源碼 ctx.py
class RequestContext( object ):
   # 中間省略
   def push( self ):
     top = _request_ctx_stack.top
     if top is not None and top.preserved:
       top.pop(top._preserved_exc)
     # 獲取應用上下文
     app_ctx = _app_ctx_stack.top
     # 判斷應用上下文是否存在並與當前應用一致
     if app_ctx is None or app_ctx.app ! = self .app:
       # 建立應用上下文併入棧
       app_ctx = self .app.app_context()
       app_ctx.push()
       self ._implicit_app_ctx_stack.append(app_ctx)
     else :
       self ._implicit_app_ctx_stack.append( None )
 
     if hasattr (sys, 'exc_clear' ):
       sys.exc_clear()
     # 把請求上下文入棧
     _request_ctx_stack.push( self )

咱們知道當有請求進入時, Flask會自動幫咱們來建立請求上下文. 而經過上述代碼咱們能夠看到,在建立請求上下文時會有一個判斷操做, 若是應用上下文爲空或與當前應用不匹配, 那麼會從新建立一個應用上下文. 因此說通常狀況下並不須要咱們手動去建立, 固然若是須要, 你也能夠顯式調用app_context與request_context來建立應用上下文與請求上下文.

那麼咱們應該如何使用上下文呢?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from flask import Flask, request, g, current_app
app = Flask(__name__)
 
@app .before_request
def before_request():
   print 'before_request run'
   g.name = "Tom"
   
@app .after_request
def after_request(response):
   print 'after_request run'
   print (g.name)
   return response
 
@app .route( '/' )
def index():
   print (request.url)
   g.name = 'Cat'
   print (current_app.name)
   
if __name__ = = '__main__' :
   app.run()

訪問127.0.0.1:5000時程序輸出

before_request run
http://127.0.0.1:5000/
flask_run
after_request run
Cat
127.0.0.1 - - [04/May/2018 18:05:13] "GET / HTTP/1.1" 200 -

代碼裏邊應用到的current_app和g都屬於應用上下文對象, 而request就是請求上下文.

  • current_app 表示當前運行程序文件的程序實例
  • g: 處理請求時用做臨時存儲的對象. 每次請求都會重設這個變量 生命週期同RequestContext
  • request 表明的是當前的請求

那麼隨之而來的問題是: 這些上下文的做用域是什麼?

線程有個叫作ThreadLocal的類,也就是一般實現線程隔離的類. 而werkzeug本身實現了它的線程隔離類: werkzeug.local.Local. 而LocalStack就是用Local實現的.

這個咱們能夠經過globals.py能夠看到

?
1
2
3
4
5
6
7
8
9
10
11
# 摘自Flask源碼 globals.py
from functools import partial
from werkzeug.local import LocalStack, LocalProxy
 
 
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request' ))
session = LocalProxy(partial(_lookup_req_object, 'session' ))
g = LocalProxy(partial(_lookup_app_object, 'g' ))

_lookup_app_object思就是說, 對於不一樣的線程, 它們訪問這兩個對象看到的結果是不同的、徹底隔離的. Flask經過這樣的方式來隔離每一個請求.

以上就是本文的所有內容,但願對你們的學習有所幫助,也但願你們多多支持腳本之家。

相關文章
相關標籤/搜索