今天花了點時間把看了web.py的程式碼分析了一遍,稍稍的總結成一個圖片,供有興趣的人蔘考。
原因
在開始之前先來說下分析它程式碼的原因,昨天是打算給wechat這個專案加上異常處理,可是發現在伺服器返回400錯誤之後,客戶端獲取到得responseText和我伺服器端定義的不一樣,我伺服器端是這麼返回錯誤的: return web.BadRequest(message="使用者名稱或密碼錯誤") ,於是去檢視了下這個BadRequest是怎麼處理這個message的,發現web.py的所有http狀態的返回都是基於Exception來做的(包括你想直接返回200,可以通過 web.OK 來返回。
研究到最後發現,既然是基於Exception來做的,那麼就直接return是不對的,應該用raise才行——raise web.OK 或者 raise web.BadRequest(message="blabla..") 。
這部分對應的處理程式碼在application.py#287行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
try: # allow uppercase methods only if web.ctx.method.upper() != web.ctx.method: raise web.nomethod() result = self.handle_with_processors() if is_generator(result): result = peep(result) else: result = [result] except web.HTTPError, e: # 就在這裡了,會把data取出來,如果不使用raise的話,只能去改造result物件了 result = [e.data] result = web.safestr(iter(result)) # 不用raise就改造safestr |
意識到這個問題的時候,我已經通過自己的方式改造成功了,就是在web.safestr中對物件的data進行判斷,當然有什麼副作用不知道,這麼改造:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
def safestr(obj, encoding='utf-8'): r""" Converts any given object to utf-8 encoded string. >>> safestr('hello') 'hello' >>> safestr(u'\u1234') '\xe1\x88\xb4' >>> safestr(2) '2' """ if isinstance(obj, unicode): return obj.encode(encoding) elif isinstance(obj, str): return obj elif hasattr(obj, 'next'): # iterator return itertools.imap(safestr, obj) elif hasattr(obj, 'data'): # the5fire 新增的這個 return str(obj.data) else: return str(obj) |
當然,最佳的方式還是要用raise來做。
原始碼分析開始
不管目的是什麼,反正最後還是把關鍵程式碼通讀了下,整理成下面這個圖,不是很詳細,但對於想分析的人來說應該會有些幫助:
整個流程是從專案的啟動開始的。
application.py
先從application.py開始執行,這是使用webpy開發是簡單執行專案的入口,簡單的例子就是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import web urls = ( '/(.*)', 'hello' ) app = web.application(urls, globals()) class hello: def GET(self, name): if not name: name = 'World' return 'Hello, ' + name + '!' if __name__ == "__main__": app.run() |
這裡的application就是圖中最上面的那個類,我把類的關鍵屬性和方法都寫了出來。
app.run() 中會執行 wsgifunc(self, *middleware) 這個方法,在這個方法裡定義了一個 wsgi 的方法(上面貼得那段異常處理的程式碼就是wsgi中的),最後被傳遞出去給到 wsgi.runwsgi` 的引數中。
httpserver.py
這個 runwsgi 是在wsgi.py中定義的,作用僅僅是呼叫httpserver的runsimple方法,並把前面的那個 wsgi 傳遞進去。
這時執行到了httpserver.py的runsimple這個函式中,在此函式中,首先是呼叫WSGIServer這個函式生成一個server例項,然後在啟動這個server。
在WSGIServer中主要工作是例項化wsgiserver包中的 CherryPyWSGIServer 這個類。順著圖再往下看 CherryPyWSGIServer 是繼承自HTTPServer的。在CherryPyWSGIServer的初始化中首先是建立了一個self.requests的執行緒池,用來存放所有的請求連線。然後是把上面的 wsgi 賦給屬性 wsgi_app ,還有就是宣告閘道器 gateway 這個用來把應用生成的資料最終返回給客戶端的元件。
wsgiserver/__init__.py
在CherryPyWSGIServer初始化是會建立一個self.requests的執行緒池,這個執行緒池在初始化時會持有這個CherryPyWSGIServer物件的例項。在上面說的啟動server時——server.start(),主要工作是繫結要監聽的地址和埠,然後啟動self.request這個執行緒池。執行緒池在啟動的時候會根據在建立requests這個執行緒池時指明執行緒池的大小來建立用來具體幹活的 WorkerThread (這個每個Worker也都會持有Server的例項),並啟動這些WorkerThread。
這些WorkerThead啟動之後會等待,從server的執行緒池的佇列中(也就是 self.server.requests.get() )獲取來自客戶端的連線 conn 。在上面的HTTPServer的定義中還有一個tick的方法,這個方法就是用來接收來自客戶端的請求,然後建立連線——HTTPConnection,最後把例項conn放到requests執行緒池中的佇列中。
在WorkerThead啟動之後會呼叫conn——HTTPConnection的 communicate 方法。在這個方法中會生成一個HTTPRequest物件,做完一些驗證和資料轉換之後(根據HTTP協議,把資料放到物件中),會呼叫這個HTTPRequest例項的respond方法,這個respond方法中,關鍵的一句是: self.server.gateway(self).respond() ——呼叫server中gateway的方法(注意:全域性只有一個CherryPyWSGIServer的例項server)。
WSGIGateway_10
上面的那句呼叫gateway中respond方法的程式碼邏輯是:例項化gateway——WSGIGateway_10,呼叫respond。在WSGIGateway_10的respond中,又通過request呼叫server上的wsgi_app方法:
1 |
response = self.req.server.wsgi_app(self.env, self.start_response) |
通過這個生成response,最後寫回到客戶端(瀏覽器)。
到此為止就是上面圖片中要介紹的所有流程了。
最後補充下那個被傳遞到最後的wsgi_app程式碼,我一直覺得一個類或者函式被傳遞這麼多層最後來執行有點彆扭,就像是在寫JavaScript中時有很深的回撥一樣難以理解。
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 |
def wsgi(env, start_resp): # clear threadlocal to avoid inteference of previous requests self._cleanup() self.load(env) try: # allow uppercase methods only if web.ctx.method.upper() != web.ctx.method: raise web.nomethod() result = self.handle_with_processors() if is_generator(result): result = peep(result) else: result = [result] except web.HTTPError, e: result = [e.data] result = web.safestr(iter(result)) status, headers = web.ctx.status, web.ctx.headers start_resp(status, headers) def cleanup(): self._cleanup() yield '' # force this function to be a generator #import pdb;pdb.set_trace() return itertools.chain(result, cleanup()) for m in middleware: wsgi = m(wsgi) return wsgi |
這麼一個函式翻山涉水被傳遞到WSGI中實屬不易,理解了這個函式的傳遞過程,和這個函式的作用,基本上也就理解了webpy了。