本文參考了:
- github.com/alanctkc/ws…
- Youtube : Creating WSGI Middleware
上篇文章簡要提到:wsgi 規范中的 app 是一個可調用對象,可以通過嵌套調用的方式實現中間件的功能。這篇文章就來親自動手實現一下。
此文的重點在于 app 端,所以 wsgi 服務器將使用python 內置module wsgiref.simple_server
中的make_server
。
創建 app
新建文件 app.py
:
def application(environ, start_response):"""The web application."""response_body = ""for key, value in environ.items():response_body += "<p>{} : {}\n</p>".format(key, value)# Set up the response status and headersstatus = '200 OK'response_headers = [('Content-Type', 'text/html; charset=utf-8'),('Content-Length', str(len(response_body))),]start_response(status, response_headers)return [response_body.encode('utf-8')]復制代碼
注意:python3中要求
response_body
是 bytes,所以需要 encode()一下。在 python2中是 str,不需要 encode()。
這個 app 做的事情非常簡單,把傳過來的 environ 原樣返回。在開始返回body 之前,調用server
傳過來的start_response
函數。
簡要說明一下為什么是 retuen [response_body]
而不是 return response_body
或者 return response_body.split("\n")
或者return response_body.split("")
?
- 首先 wsgi 規范說明了
app
返回的是一個可迭代對象,列表是可迭代的。 - 其次,對于大多數 app 來說,response_body都不會太長,服務器的內存完成足以一次性裝下,所以最高效的方法就是一次性把
response_body
全傳過去。
創建 server
新建文件server.py
from wsgiref.simple_server import make_server
from app import applicationprint("Server is running at http://localhost:8888 . Press Ctrl+C to stop.")
server = make_server('localhost', 8888, application)
server.serve_forever()復制代碼
用瀏覽器打開 http://localhost:8888,就可以看到 environ 的詳細內容。其中比較重要的我用紅框框圈了起來。
第一個中間件:cors
先簡要了解一下 cors 的機制(詳細的要比這個復雜點):
如果一個ajax請求(XMLHttpRequest)是跨域的,比如說在 http://localhost:9000
頁面上向運行在http://localhost:8888
的服務器發起請求,瀏覽器就會往請求頭上面加上一個ORIGIN
字段,這個字段的值就是localhost:9000
。(對應在app 的 environ 參數中,就是 HTTP_ORIGIN
)
同時,瀏覽器會先發出OPTIONS
請求,服務器要實現這樣的功能:如果想要接收這個請求的話,需要在response 的 headers里面添加一個Access-Control-Allow-Origin
字段,值就是請求傳過來的那個ORIGIN
。
瀏覽器發出OPTIONS
請求并發現返回數據的 headers 里面有Access-Control-Allow-Origin
,才會進行下一步發出真正的請求:GET,POST,WAHTERVER。
所以,CORS 是瀏覽器和 Server共同協作來完成的。
看一下代碼:
class CORSMiddleware(object):def __init__(self, app, whitelist=None):"""Initialize the middleware for the specified app."""if whitelist is None:whitelist = []self.app = appself.whitelist = whitelistdef validate_origin(self, origin):"""Validate that the origin of the request is whitelisted."""return origin and origin in self.whitelistdef cors_response_factory(self, origin, start_response):"""Create a start_response method that includes a CORS header for thespecified origin."""def cors_allowed_response(status, response_headers, exc_info=None):"""This wraps the start_response behavior to add some headers."""response_headers.extend([('Access-Control-Allow-Origin', origin)])return start_response(status, response_headers, exc_info)return cors_allowed_responsedef cors_options_app(self, origin, environ, start_response):"""A small wsgi app that responds to preflight requests for thespecified origin."""response_body = 'ok'status = '200 OK'response_headers = [('Content-Type', 'text/plain'),('Content-Length', str(len(response_body))),('Access-Control-Allow-Origin', origin),('Access-Control-Allow-Headers', 'Content-Type'),]start_response(status, response_headers)return [response_body.encode('utf-8')]def cors_reject_app(self, origin, environ, start_response):response_body = 'rejected'status = '200 OK'response_headers = [('Content-Type', 'text/plain'),('Content-Length', str(len(response_body))),]start_response(status, response_headers)return [response_body.encode('utf-8')]def __call__(self, environ, start_response):"""Handle an individual request."""origin = environ.get('HTTP_ORIGIN')if origin:if self.validate_origin(origin):method = environ.get('REQUEST_METHOD')if method == 'OPTIONS':return self.cors_options_app(origin, environ, start_response)return self.app(environ, self.cors_response_factory(origin, start_response))else:return self.cors_reject_app(origin, environ, start_response)else:return self.app(environ, start_response)復制代碼
__init__
方法傳入的參數有:下一層的 app(回顧一下前面說的 app 是一層一層的,所以能夠實現中間件)和 client 白名單,只允許來自這個白名單內的ajax 請求。
__call__
方法說明這是一個可調用對象(類也可以是可調用的),一樣接收兩個參數:environ
和start_response
。首先判斷一下 environ 中有沒有HTTP_ORIGIN
,有的話就表明屬于跨域請求。如果是跨域,判斷一下 origin 在不咋白名單。如果在白名單里面,如果是 OPTIONS
請求,返回cors_options_app
里面的對應內容(加上了Access-Control-Allow-Origin
header);如果不是OPTIONS
請求,調用下一層的 app。如果不在白名單,返回的是cors_reject_app
。
修改一下server.py
:
app = CORSMiddleware(app=application,whitelist=['http://localhost:9000','http://localhost:9001']
)
server = make_server('localhost', 8000, app)
復制代碼
測試 cors app
這里在運行三個客戶端,[代碼在此]。(github.com/liaochangji…)
運行python client.py
:
在瀏覽器打開http://localhost:9000
、http://localhost:9001
和http://localhost:9002
,可以發現http://localhost:9000
和http://localhost:9001
成功發出了請求,而http://localhost:9002
失敗了。
第二個中間件:請求耗時
這個比上一個要簡單很多,相信現在你已經完全能夠理解了:
import timeclass ResponseTimingMiddleware(object):"""A wrapper around an app to print out the response time for eachrequest."""def __init__(self, app):self.app = appdef __call__(self, environ, start_response):"""Meaure the time spent in the application."""start_time = time.time()response = self.app(environ, start_response)response_time = (time.time() - start_time) * 1000timing_text = "總共耗時: {:.10f}ms \n".format(response_time)response = [timing_text.encode('utf-8') + response[0]]return response
復制代碼
再修改一下server.py
:
app = ResponseTimingMiddleware(CORSMiddleware(app=application,whitelist=['http://localhost:9000','http://localhost:9001'])
)
復制代碼
再次訪問http://localhost:8000
,會看到最前面打印出了此次請求的耗時:
總結一下
我手畫了一個請求圖,希望對你有所幫助:
本文的所有源代碼開源在 github 上:github.com/liaochangji…
希望能點個 star ~
如果你像我一樣真正熱愛計算機科學,喜歡研究底層邏輯,歡迎關注我的微信公眾號: