WSGI
WSGI是python容器服務(wù)器和python web app通信協(xié)議標準
server負責去進行http層的處理,外部看來,server接受client端請求
返回http response
而中間處理過程則是調(diào)用web app
WSGI就是調(diào)用標準,規(guī)定了app暴露的接口標準和返回標準以及服務(wù)器傳參標準
這樣一來不同的app和server之間能夠相互統(tǒng)一交互
其中對app的要求是
- callable
- 結(jié)果iterable
- 接受server的 兩個參數(shù) : environ環(huán)境參數(shù), start_response 生成標準http包頭的函數(shù)
callable好理解,實際上的app可以理解為server處理http request 的邏輯處理函數(shù),所以要求一定是可以被服務(wù)器調(diào)用的,需要暴露調(diào)用接口
iterable 要和start_response結(jié)合起來理解, iterable 的結(jié)果 我們將其用data簡寫 data[], 實際上這個data[]是http body, server 不停迭代,data[] 然后傳輸內(nèi)容
和start_response相結(jié)合,start_response也是一個callable筛谚,接受兩個必須的參數(shù),status(HTTP狀態(tài))和response_headers(響應(yīng)消息的頭).實際上返回的就是http header
在wsgi app 內(nèi)部, return data[]之前要先調(diào)用,但是不會立刻返回包頭給 server, 而一定要等到迭代 data[]到第一個非空對象以后才會傳
換句話說,如果這時候迭代data[]出錯了,你先傳過去包頭狀態(tài)里寫的是200那不就完犢子了
所以一定要先等到迭代到第一個非空對象以后,這個頭才會被傳過去
這里表述有問題,是一定在傳data[]給server之前,先傳start_response,也就是先傳頭,但是這時候server沒有直接把這個header發(fā)給client,而是等body內(nèi)容
body一定不能為空,不能只給一個頭
這里start_response還有一個可選參數(shù)也就是exc_info,當處理請求的過程遇到錯誤時停忿,這個參數(shù)會被設(shè)置驾讲,同時調(diào)用 start_response.如果這時候headers 還在 start_response內(nèi)沒出來呢,可以用這個參數(shù)直接去設(shè)置header 頭, 也就是把status code改為其他錯誤代碼
如果已經(jīng)輸出到server了,則需要raise 一個 error 讓 服務(wù)器 去處理,跳出應(yīng)用.
為了避免循環(huán)引用,start_response實現(xiàn)時需要保證 exc_info在函數(shù)調(diào)用后不再包含引用席赂。 也就是說start_response用完 exc_info后吮铭,需要保證執(zhí)行一句
exc_info = None
釋放掉引用.
下面來一個示例簡陋的WSGI程序
def application(environ, start_response):
status = '200 OK'
output = 'World!'
response_headers = [('Content-type', 'text/plain'),
('Content-Length', str(12)]
write = start_response(status, response_headers)
write('Hello ')
return [output]
關(guān)于這個write,我看了一下start_response的返回對象也就是理論上的header,實際也是一個callable,
形式為write(body_data)
不過理論上,下面這么寫應(yīng)該更合理
def application(environ, start_response):
status = '200 OK'
output = 'Hello,World!'
response_headers = [('Content-type', 'text/plain'),
('Content-Length', str(12)]
start_response(status, response_headers)
return [output]
output就是http body
startresponse就是header,這樣子更易接受
以上參考:
wsgiref 源代碼分析 --start_response()
進一步理解WSGI對app端的定義
- callable
- return iterable
- 接受environ,start_response
callable一定是function么? 類或是對象實現(xiàn)了callable不也可以?
iterable 一定要是[],{}這種基本數(shù)據(jù)結(jié)構(gòu)么?實現(xiàn)iter不也可以?
至于接收參數(shù)就更簡單了
所以從wsgi規(guī)則上,我們可以定義出的不止是app可以是對象,或是直接是類如下
# 1. 可調(diào)用對象是一個函數(shù)
def application(environ, start_response):
response_body = 'The request method was %s' % environ['REQUEST_METHOD']
# HTTP response code and message
status = '200 OK'
# 應(yīng)答的頭部是一個列表,每對鍵值都必須是一個 tuple颅停。
response_headers = [('Content-Type', 'text/plain'),
('Content-Length', str(len(response_body)))]
# 調(diào)用服務(wù)器程序提供的 start_response谓晌,填入兩個參數(shù)
start_response(status, response_headers)
# 返回必須是 iterable
return [response_body]
# 2. 可調(diào)用對象是一個類
class AppClass:
"""這里的可調(diào)用對象就是 AppClass 這個類,調(diào)用它就能生成可以迭代的結(jié)果癞揉。
使用方法類似于:
for result in AppClass(env, start_response):
do_somthing(result)
"""
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
def __iter__(self):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
self.start(status, response_headers)
yield "Hello world!\n"
# 3. 可調(diào)用對象是一個實例
class AppClass:
"""這里的可調(diào)用對象就是 AppClass 的實例纸肉,使用方法類似于:
app = AppClass()
for result in app(environ, start_response):
do_somthing(result)
"""
def __init__(self):
pass
def __call__(self, environ, start_response):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
self.start(status, response_headers)
yield "Hello world!\n"
嚴格意義講,這個標準是允許嵌套的,可以更進一步
也就是server調(diào)用一個app,但是app繼續(xù)往下調(diào)用app.
從結(jié)果來看,中間這個app是一個中間件,對服務(wù)器來說是app,對app來說是服務(wù)器.
PEP333給這個嵌套就定義為中間件,并給出了假設(shè)的場景
- Routing a request to different application objects based on the target URL, after rewriting the environ accordingly.
- Allowing multiple applications or frameworks to run side by side in the same process
- Load balancing and remote processing, by forwarding requests and responses over a network
- Perform content postprocessing, such as applying XSL stylesheets
直接翻譯一下
- 根據(jù) url 把請求給到不同的客戶端程序(url routing),在把environ 改寫以后
- 允許多個客戶端程序/web 框架同時運行,就是把接到的同一個請求傳遞給多個程序喊熟。
- 負載均衡和遠程處理:把請求在網(wǎng)絡(luò)上傳輸
- 應(yīng)答的過濾處理
PEP333直接給了一個中間件使用例子,但是不是那么多直觀
大概意思是先實現(xiàn)了一個迭代器類LatinIter,然后實現(xiàn)了一個類Latinator,在這個類中實現(xiàn)了callable,設(shè)置了對environ和start_response的處理
并且指定了接下來要調(diào)用的可能的self.app
最后實驗調(diào)用這個Lationtor作為中間件,處理foo_app
服務(wù)器環(huán)境是cgi
PEP333
這里面同時演示說明了,處理response頭的過程中如果出意外了該怎么辦.
from piglatin import piglatin
class LatinIter:
"""Transform iterated output to piglatin, if it's okay to do so
Note that the "okayness" can change until the application yields
its first non-empty string, so 'transform_ok' has to be a mutable
truth value.
"""
def __init__(self, result, transform_ok):
if hasattr(result, 'close'):
self.close = result.close
self._next = iter(result).next
self.transform_ok = transform_ok
def __iter__(self):
return self
def next(self):
if self.transform_ok:
return piglatin(self._next())
else:
return self._next()
class Latinator:
# by default, don't transform output
transform = False
def __init__(self, application):
self.application = application
def __call__(self, environ, start_response):
transform_ok = []
def start_latin(status, response_headers, exc_info=None):
# Reset ok flag, in case this is a repeat call
del transform_ok[:]
for name, value in response_headers:
if name.lower() == 'content-type' and value == 'text/plain':
transform_ok.append(True)
# Strip content-length if present, else it'll be wrong
response_headers = [(name, value)
for name, value in response_headers
if name.lower() != 'content-length'
]
break
write = start_response(status, response_headers, exc_info)
if transform_ok:
def write_latin(data):
write(piglatin(data))
return write_latin
else:
return write
return LatinIter(self.application(environ, start_latin), transform_ok)
# Run foo_app under a Latinator's control, using the example CGI gateway
from foo_app import foo_app
run_with_cgi(Latinator(foo_app))
我覺得上面這個例子不夠直觀,同時驗證了好幾條,我們看下面這個例子就是將路由設(shè)置為中間件,更容易理解
class Router(object):
def __init__(self):
self.path_info = {}
def route(self, environ, start_response):
application = self.path_info[environ['PATH_INFO']]
return application(environ, start_response)
def __call__(self, path):
def wrapper(application):
self.path_info[path] = application
return wrapper
router = Router()
## 上面是中間件router,實際是一個wsgi app
#here is the application
@router('/hello') #調(diào)用 route 實例柏肪,把函數(shù)注冊到 paht_info 字典
def hello(environ, start_response):
status = '200 OK'
output = 'Hello'
response_headers = [('Content-type', 'text/plain'),
('Content-Length', str(len(output)))]
write = start_response(status, response_headers)
return [output]
@router('/world')
def world(environ, start_response):
status = '200 OK'
output = 'World!'
response_headers = [('Content-type', 'text/plain'),
('Content-Length', str(len(output)))]
write = start_response(status, response_headers)
return [output]
#here run the application
result = router.route(environ, start_response)
for value in result:
write(value)
以上來自博客python wsgi簡介
我覺得更容易理解一些
這里還有一點,我看到這個路由實現(xiàn)的時候驚呆了,懷疑是否flask里的路由也是這么實現(xiàn)的,hh但不是,
那么為什么不這么做呢?
這就是一個問題了,我需要問問老師,
效率問題么?還是這么一來解耦太厲害了?
但確實前后端分離中,后端只負責開發(fā)RESTfulAPI的話,連路由都不用寫的,只需要處理數(shù)據(jù)庫和暴露API給前段就好,node和vue自然會處理好路由.
我們下一步開始分析流程