基礎知識
Python 知識
- iterator 和 generator
- 函式的高階用法:巢狀函式,作為引數傳遞等等
- 瞭解 decorator 會對理解 wsgi 有很大的幫助
- python 的 callable 概念
- classmethod 和 staticmethod 的概念
- web 程式設計的基礎
HTTP 基礎
對於 web 應用程式來說,最基本的概念就是客戶端傳送請求(request),收到伺服器端的響應(response)。
下面是簡單的 HTTP 請求:
1 2 3 4 5 |
GET /Index.html HTTP/1.1\r\n Connection: Keep-Alive\r\n Accept: */*\r\n User-Agent: Sample Application\r\n Host: www.microsoft.com\r\n\r\n |
內容包括了 method、 url、 protocol version 以及頭部的資訊。而 HTTP 響應(不包括資料)可能是如下的內容:
1 2 3 4 5 6 7 8 |
HTTP/1.1 200 OK Server: Microsoft-IIS/5.0\r\n Content-Location: http://www.microsoft.com/default.htm\r\n Date: Tue, 25 Jun 2002 19:33:18 GMT\r\n Content-Type: text/html\r\n Accept-Ranges: bytes\r\n Last-Modified: Mon, 24 Jun 2002 20:27:23 GMT\r\n Content-Length: 26812\r\n |
實際生產中,python 程式是放在伺服器的 http server(比如 apache, nginx 等)上的。現在的問題是 伺服器程式怎麼把接受到的請求傳遞給 python 呢,怎麼在網路的資料流和 python 的結構體之間轉換呢?這就是 wsgi 做的事情:一套關於程式端和伺服器端的規範,或者說統一的介面。
WSGI
我們先看一下面向 http 的 python 程式需要關心哪些內容:
- 請求
- 請求的方法 method
- 請求的地址 url
- 請求的內容
- 請求的頭部 header
- 請求的環境資訊
- 響應
- 狀態碼 status_code
- 響應的資料
- 響應的頭部
WSGI(Web Server Gateway Interface) 的任務就是把上面的資料在 http server 和 python 程式之間簡單友好地傳遞。它是一個標準,被定義在PEP 333。需要 http server 和 python 程式都要遵守一定的規範,實現這個標準的約定內容,才能正常工作。
應用程式端
WSGI 規定每個 python 程式(Application)必須是一個可呼叫的物件(實現了__call__
函式的方法或者類),接受兩個引數 environ
(WSGI 的環境資訊) 和 start_response
(開始響應請求的函式),並且返回 iterable。幾點說明:
environ
和start_response
由 http server 提供並實現environ
變數是包含了環境資訊的字典Application
內部在返回前呼叫start_response
start_response
也是一個 callable,接受兩個必須的引數,status
(HTTP狀態)和response_headers
(響應訊息的頭)- 可呼叫物件要返回一個值,這個值是可迭代的。
說了這麼多的概念,再來看看程式碼的實現:
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# 1. 可呼叫物件是一個函式 def application(environ, start_response): response_body = 'The request method was %s' % environ['REQUEST_METHOD'] # HTTP response code and message status = '200 OK' # 應答的頭部是一個列表,每對鍵值都必須是一個 tuple。 response_headers = [('Content-Type', 'text/plain'), ('Content-Length', str(len(response_body)))] # 呼叫伺服器程式提供的 start_response,填入兩個引數 start_response(status, response_headers) # 返回必須是 iterable return [response_body] # 2. 可呼叫物件是一個類 class AppClass: """這裡的可呼叫物件就是 AppClass 這個類,呼叫它就能生成可以迭代的結果。 使用方法類似於: 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. 可呼叫物件是一個例項 class AppClass: """這裡的可呼叫物件就是 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" |
伺服器程式端
上面已經說過,標準要能夠確切地實行,必須要求程式端和伺服器端共同遵守。上面提到, envrion
和 start_response
都是伺服器端提供的。下面就看看,伺服器端要履行的義務。
- 準備
environ
引數 - 定義
start_response
函式 - 呼叫程式端的可呼叫物件
PEP 333 裡給出了一個 wsgi server 的簡單實現,我又簡化了一下——去除一些異常處理和判斷,新增了一點註釋:
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 37 38 |
import os, sys def run_with_cgi(application): # application 是程式端的可呼叫物件 # 準備 environ 引數,這是一個字典,裡面的內容是一次 HTTP 請求的環境變數 environ = dict(os.environ.items()) environ['wsgi.input'] = sys.stdin environ['wsgi.errors'] = sys.stderr environ['wsgi.version'] = (1, 0) environ['wsgi.multithread'] = False environ['wsgi.multiprocess'] = True environ['wsgi.run_once'] = True environ['wsgi.url_scheme'] = 'http' headers_set = [] headers_sent = [] # 把應答的結果輸出到終端 def write(data): sys.stdout.write(data) sys.stdout.flush() # 實現 start_response 函式,根據程式端傳過來的 status 和 response_headers 引數, # 設定狀態和頭部 def start_response(status, response_headers, exc_info=None): headers_set[:] = [status, response_headers] return write # 呼叫客戶端的可呼叫物件,把準備好的引數傳遞過去 result = application(environ, start_response) # 處理得到的結果,這裡簡單地把結果輸出到標準輸出。 try: for data in result: if data: # don't send headers until body appears write(data) finally: if hasattr(result, 'close'): result.close() |
中間層 middleware
有些程式可能處於伺服器端和程式端兩者之間:對於伺服器程式,它就是應用程式;而對於應用程式,它就是伺服器程式。這就是中間層 middleware。middleware 對伺服器程式和應用是透明的,它像一個代理/管道一樣,把接收到的請求進行一些處理,然後往後傳遞,一直傳遞到客戶端程式,最後把程式的客戶端處理的結果再返回。如下圖所示:
middleware 做了兩件事情:
- 被伺服器程式(有可能是其他 middleware)呼叫,返回結果回去
- 呼叫應用程式(有可能是其他 middleware),把引數傳遞過去
PEP 333 上面給出了 middleware 的可能使用場景:
- 根據 url 把請求給到不同的客戶端程式(url routing)
- 允許多個客戶端程式/web 框架同時執行,就是把接到的同一個請求傳遞給多個程式。
- 負載均衡和遠端處理:把請求在網路上傳輸
- 應答的過濾處理
那麼簡單地 middleware 實現是怎麼樣的呢?下面的程式碼實現的是一個簡單地 url routing 的 middleware:
1 2 3 4 5 6 7 8 9 10 11 12 |
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() |
怎麼在程式裡面使用呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#here is the application @router('/hello') #呼叫 route 例項,把函式註冊到 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) |
注:上面的程式碼來自這篇部落格。
瞭解更多?
對於普通的開發者來說,瞭解到上面的知識已經足夠,並不需要掌握每一個細節。
Only authors of web servers and programming frameworks need to know every detail and corner case of the WSGI design. You don’t need to understand every detail of WSGI just to install a WSGI application or to write a web application using an existing framework.
想要更多的話,就去看 PEP333,文件裡還有下面更多的知識:
- 錯誤處理
- environ 變數包含哪些值,都是什麼意思。
- 輸入和輸出的細節
- start_response 的更多規範
- content-length 等頭部規範
- 快取和文字流
- unicode 和多語言處理
- ……