Flask源碼閱讀以及WSGI理解

首先要明白web服務就是一個解析請求 然后返回響應的過程。

然后明白wsgi是什么?

一套接口標準,用來實現在服務器和python程序之間的轉換。使得服務器和python程序之間能夠交互。

常見的python 的web程序的架構一般是這樣子

Nginx-->(wsgi)gunicorn-->framework-->application
wsgi工作在wsgi服務器和web服務器的中間,一般是使用nginx來進行反向代理。然后使用gunicorn來
當wsgi服務器。

wsgi的接口是這樣子的:

wsgi_app(environ, start_response)

environ 包含符合wsgi 標準的一個字典 如下所示:

{'wsgi.url_scheme': 'http', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'SCRIPT_NAME': '', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 'HTTP_CONNECTION': 'keep-alive', 'REQUEST_METHOD': 'GET', 'SERVER_SOFTWARE': 'Werkzeug/0.12.1', 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 'SERVER_PROTOCOL': 'HTTP/1.1', 'wsgi.multiprocess': False, 'REMOTE_ADDR': '127.0.0.1', 'QUERY_STRING': 'next=%2F', 'wsgi.run_once': False, 'SERVER_NAME': '127.0.0.1', 'werkzeug.server.shutdown': <function WSGIRequestHandler.make_environ.<locals>.shutdown_server at 0x000000198E7241E0>, 'wsgi.input': <_io.BufferedReader name=828>, 'wsgi.version': (1, 0), 'HTTP_COOKIE': 'UM_distinctid=15c7b3a049547-08fef361539c6f-323f5e0f-144000-15c7b3a049666a; CNZZDATA1262121305=568492071-1496716091-%7C1496716211; csrftoken=ovtMsXELMG8ISgFhdDzqcYJKVParC8ajkCr8Kza6RKQ9reVO3wOiwXuJigTw6Iaa; __wzdbd8a16e4306732acca12=1504512971|1acb4ba715b7; test=tests', 'SERVER_PORT': '5000', 'PATH_INFO': '/login', 'HTTP_ACCEPT_LANGUAGE': 'zh-CN,zh;q=0.8', 'REMOTE_PORT': 64941, 'HTTP_HOST': '127.0.0.1:5000', 'HTTP_USER_AGENT': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36', 'wsgi.errors': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, 'werkzeug.request': <BaseRequest 'http://127.0.0.1:5000/login?next=%2F' [GET]>, 'HTTP_CACHE_CONTROL': 'max-age=0', 'wsgi.multithread': False}


這里的environ就是給了一個request 定義了一個標準。
start_response 是一個返回響應的函數,通過request來產生響應。

那么wsgi服務器具體做了什么 ,它究竟是干什么的呢?
其實主要就是將數據轉化成wsgi格式的 。具體整個wsgi后面發生了什么呢?

wsgi

下面來閱讀整個flask框架。首先要明白的是flask的框架不過是應用和wsgi 之間的一層抽象。
主要提供的功能就是路由綁定 即將路由和具體的執行函數綁定在一起。

def dispatch_request(self):
    try:
        endpoint, values = self.match_request()
        return self.view_functions[endpoint](**values)
    except HTTPException, e:
        handler = self.error_handlers.get(e.code)
        if handler is None:
            return e
        return handler(e)
    except Exception, e:
        handler = self.error_handlers.get(500)
        if self.debug or handler is None:
            raise
        return handler(e)

這個函數根據相應的路由返回對應的響應。

那么在這之前就需要一個在程序啟動一開始就把函數和路由綁定的方法。


def route(self, rule, **options):
    def decorator(f):
        self.add_url_rule(rule, f.__name__, **options)
        self.view_functions[f.__name__] = f
        return f
    return decorator

這就是flask中的 @app.route 的route,使用裝飾器,將函數名字和路由綁定在一起。
比如
@app.route('/test/<int:id>')
def test(id):
    do something 

這里將這個路由和 test這個函數綁定在一起。這里還有動態參數這個概念。即 <>中的部分。這個依靠
正則表達式來獲取值。

整個流程就是這樣子的:

例: 瀏覽器--> 請求/test/32--> 經過wsgi服務器,environ中有一個path,上面的那個dispatch-request函數來

從路由字典中尋找 發現 test函數對應的是 這個類型的路由。 解析出 動態參數,傳給test函數。

還有一個問題 有一些任務是需要在整個流程前執行的,那么需要有這么一種機制來進行執行。

flask 中有這么一個變量

self.before_request_funcs

用來保存需要先執行的函數。

def preprocess_request(self):
    for func in self.before_request_funcs:
        rv = func()
        if rv is not None:
            return rv

def wsgi_app(self, environ, start_response):
    with self.request_context(environ):
        rv = self.preprocess_request()
        if rv is None:
            rv = self.dispatch_request()
        response = self.make_response(rv)
        response = self.process_response(response)
        return response(environ, start_response)

可以看到首先是執行before_request_funcs這個字典中的函數。同理如果有需要在每次請求之后執行的

函數也是這樣子,變動的是 變成了在每次返回響應之前執行,即執行完具體的路由函數后。

這應該是框架最主要的功能?,F在使用這個就可以寫web程序了。

但是還有一個問題,http是無狀態的協議,如果有多個用戶登錄網站,如何判斷那個是那個呢?

這時就需要session 來實現了。 用session來維持會話,給每個請求一個cookie標識,服務端通過這個

cookie來判斷那個是那個用戶。flask自帶的session都是存在客戶端的,考慮到安全,使用了加密。

在服務端保存一個密鑰,每次需要這個密鑰來進行信息讀取。

flask_login 這個擴展模塊使用了這個機制來進行登錄管理,通過保存用戶的id, 可以很清楚的知道具體是那個

用戶在登錄,對一些必須登錄的界面也可以很方便的進行管理。使用裝飾器這個語法糖 寫起來是這樣子的:

@app.route('/mypost')
@login_reqired
def mypost():
   do something

這樣子就可以對一些頁面實現訪問控制了。

最后還有一個比較理解的地方,即flask中的上下文。

Flask中分為請求上下文 requestcontext 和 應用上下文 appcontext
這背后的主要實現是靠:
LocalStack(object):
LocalProxy(object):

這種上下文像threadlocal,各個線程的對象 面向其他線程是隔離的。

為什么要這樣子呢?

因為在并發的時候如果是多個線程共享一個上下文,很容易出現混亂。
這就是對一些全局的變量進行了隔離。

應用上下文的作用

首先 代碼在執行的時候處于兩種狀態,一種是已經壓入上下文的一種是沒壓入的。

所以在執行一些操作時經常遇到 不在上下文這樣子的錯誤。

上面使用棧實現這種結構的原因是因為Flask是支持在一個python進程中有多個應用的。

下面來看這幾類的實現
首先是聲明兩個實例 一個代表請求上下文 一個代表應用上下文

_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()

然后來翻看werkzeug的源碼 發現這幾個類實現的很巧妙。
這里面自己實現了類似 threadlocal 的東西。
如果有greenlet存在則首先使用greenlet
實現這種隔離就是通過不同的線程id 不同實現隔離。

__slots__ = ('__storage__', '__ident_func__')
def __setattr__(self, name, value):
     storage[ident][name] = value

最主要的就是通過上面幾行代碼來實現的。
通過不同的線程id 和 來保存不同的函數名字,和值對應起來。

LocalStack 使用local來實現棧

def push(self, obj):
    """Pushes a new item to the stack"""
    rv = getattr(self._local, 'stack', None)
    if rv is None:
        self._local.stack = rv = []
    rv.append(obj)
    return rv

local 和 lcoalstack方法都實現了 __call__ 調用localproxy。這里是一個代理模式的最佳實踐。

ls = Localstack()
ls.push(10) # 還是localstack對象

proxy = ls() # 變成了localproxy 對象

幾乎全部的操作都被代理。
使用
_get_current_object 獲取真正的對象。

現在最基本的就差不多完了。但是又出現了一個問題。當一個程序開始變得比較大,功能開始繁雜。開始多個人寫同一個app,

使用之前的方式在一個文件中進行開發是很痛苦的事情。這個時候就需要一些方式來對其進行解耦。使用一些手段來進行拆分。

這時藍圖就出現了,以一種模塊的方式來將代碼進行解耦。

官方文檔是這樣說明藍圖的應用場景的

* 把一個應用分解為一個藍圖的集合。這對大型應用是理想的。一個項目可以實例化一個應用對象,初始化幾個擴展,并注冊一集合的藍圖。
* 以 URL 前綴和/或子域名,在應用上注冊一個藍圖。 URL 前綴/子域名中的參數即成為這個藍圖下的所有視圖函數的共同的視圖參數(默認情況下)。
* 在一個應用中用不同的 URL 規則多次注冊一個藍圖。
* 通過藍圖提供模板過濾器、靜態文件、模板和其它功能。一個藍圖不一定要實現應用或者視圖函數。
* 初始化一個 Flask 擴展時,在這些情況中注冊一個藍圖。

比如一個網站 一般包含 -->登錄認證 api 主頁面 。。。各種功能
不使用藍圖的話就是一堆代碼耦合在一起,對控制復雜度很不利。 使用藍圖的話
認證模塊 使用auth api就使用api 主頁面使用main 開頭的方法。各自模塊的代碼放在
各自的python文件中,不同模塊的靜態文件也放在不同的文件中,管理起來很方便。

最簡單的實現藍圖的方法就是對注冊的藍圖直接加上前綴
大概如下

main = register(prefix,blueprint_folder)
main.add_url_route(prefix+rule, ...)。。。

總結 :
從計算機網絡中學習到的很重要的一個概念就是分層。使用合理的層次來把任務進行劃分。各層間保存相互獨立。
通過一個叫SAP(服務訪問點)的東西來實現各層間的訪問。如上圖,從最下層的Baseserver 到 tcpserver 到httpserver
到wsgiserver 每一層的功能都很明確,具體要在那一層進行任務只關心它的下一層就好了。還有很主要的一點,不管是
一個概念的出現,還是一個新技術的出現都是前人發現現有的技術無法滿足需求,有更好的方法來解決問題,從而出現
了新技術。大部分東西看其本質還是那些。理解好了最根基的那些東西,再出來什么新的東西不過是到了一定時間點正好
該出來的東西。有了根基 無非就是看看就能夠開始使用。

博客 https://www.97up.cn/post/148

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容