Web 開發規範 — WSGI

weixin_33858249發表於2016-07-26

目錄

WSGI 簡介

WSGI(Web Server Gateway Interface) Web 伺服器閘道器介面。從名稱上來看WSGI就是一個閘道器,作用就是在協議之間進行轉換。具體而言,WSGI 是一個規範,它遵循這種規範將一個Web元件抽象成三個部件層:Web Server + Web Middleware + Web Application。除此之外,它還定義了 Web Server 如何與 Python 寫的 Web Application進行互動,使得 Python 寫的 Web Application 可以和 Web Server 能夠對接起來。現在一般會使用 Apache + wsgi_mod 的組合來實現Web Services。

為什麼需要 WSGI 這個規範?

在 Web Services 處理方案中,有一個方案是目前應用最廣泛的:
- 部署一個 Web Server(Apache) 專門用來處理 HTTP 協議層面相關的事情,EG. 如何在一個 HOST 上提供多個不同的虛擬主機:單IP多域名,單IP多埠等。

  • 部署一個用各種語言開發( Java, PHP, Python, Ruby等 )出來的 Application,這個 Application 會接收從 Web Server 上傳遞過來的 Client Request,處理請求完成後,再 Return 給 Web Server,最後由 Web Server Response 給 Client 。

那麼,Web Server 和 Application 之間就就需要解決互動的問題。而為了解決這個問題,就定義 Web Server 和 Application 之間互動的規範。EG. Java 的 Servlet,Python 的 WSGI 等。定義這些規範的目的是為了定義統一的標準,提升程式的可移植性。

WSGI 如何工作?

WSGI 相當於 Web Server 和 Python Application 之間的橋樑。其存在的目的有兩個:
1. 讓 Web Server 知道如何呼叫 Python Application,並且將 Client Request 傳遞給 Application。
2. 讓 Python Application 能理解 Client Request 並執行對應操作,以及將執行結果返回給 Web Server,最終響應到Client。

當處理一個 WSGI 請求時,Server 為 Application 提供 上下文資訊 和一個 回撥函式, Application 處理完請求之後,使用 Server 所提供的回撥函式返回相對應請求的響應。

WSGI的角色

  • Web Server end(也稱之為 Server 或 Gateway)
  • Web Application end(也稱之為 Application 或 Framework),WSGI Application End 的規範一般是通過具體的框架(Django/Web.py)來實現的。

Server 首先會接收到User Request,然後根據規範呼叫 Application 並將預處理過的 Request 傳遞給 Application。由 Application 處理完 Request 之後將結果返回給 Web Server,最後 Web Server 將結果封裝成 HTTP Response 並響應給 User。 (重要的事情,已經是第三遍了)

Server 如何呼叫 Application?

每個 Application 的入口都只有一個,就是說 Server 只能通過這一個入口呼叫 Application 並將 Client Request 傳遞給 Application。所以 Server 需要知道如何找到這一個 Application 的入口:

這是由一個 Python Module(一般由框架提供) 決定的,並且這個 Python Module 中需要包含由一個名稱為 application 的可呼叫物件(該物件無論是 Function/Class 都可以)。而這一個可呼叫的 application 物件就是 Application 的唯一入口了。WSGI 定義了 application 物件的樣式:

def application(environ, start_response):    #該函式需要提供兩個形參
      pass

EXAMPLE:假設 Application 的入口檔案為 /var/www/index.py ,那麼首先我們需要在 Server 中配置好這一路徑(讓 Server 能夠找到這個 Python Module),然後在 index.py 這個檔案中寫入下面的 Application 入口實現程式碼:

#使用 web.py 框架時的樣式
import web   #由 web.py 框架提供了可呼叫物件 application() 作為 Application 的入口

urls = (
    '/.*', 'hello',
)

class hello(object):
    def GET(self):
        return "Hello, world."

app = web.application(urls, globals()).wsgifunc()

#Server 會根據配置找到這個檔案,然後呼叫這個 application 物件
#因為 application 物件是唯一的入口,所以不管 Client Request 的路徑和資料是什麼, Server 都會呼叫這個物件來處理,具體的 Client Request 的處理過程交由 application 物件來完成,框架的使用者一般不需要關心這個物件內部是如何工作的,框架已經對其做了封裝。

application 的兩個引數

Application 處理 Request 和 Response 都與 application 物件的兩個引數(environ、start_response)有關。

  • environ:其引用指向一個Dict型別物件,一般存放了下列型別的資料
    • 所有和 Client 相關的上下文 Informations,這樣 application 物件就可以通過 environ 引數知道 Client Request 希望訪問或操作的資源是什麼,還能夠知道 Requuest Client 中攜帶了什麼資料等等。
    • 一些 CGI(通用網管介面規範)中定義的環境變數
    • 至少包含了其他7個 WSGI 規範所定義的環境變數
    • 還可能包含了一些OS的環境變數以及 Web Server 相關的環境變數

CGI 規範中要求的資料成員

REQUEST_METHOD: 請求方法,String型別,'GET', 'POST','PUT','DELETE'等
SCRIPT_NAME: HTTP請求的URL_Path中的用於查詢到application物件的部分(EG. Web伺服器可以根據URL_Path的一部分來決定請求由哪個虛擬主機來處理這個請求)
PATH_INFO: HTTP請求的URL_Path中剩餘的部分,也就是application物件要處理的部分
QUERY_STRING: HTTP請求中的查詢字串,URL'?'後面緊跟的內容
CONTENT_TYPE: HTTP headers頭部中的 content-type 內容
CONTENT_LENGTH:HTTP headers中的 content-length 內容
SERVER_NAME 和 SERVER_PORT: 伺服器主機名和埠,這兩個值和前面的SCRIPT_NAME, PATH_INFO拼起來可以得到完整的URL路徑 EG. http://www.jmilkfan.com:80/virtual/private
SERVER_PROTOCOL: HTTP協議版本,HTTP/1.0或者HTTP/1.1
HTTP_: 和HTTP請求中的headers對應

WSGI 規範中要求的成員

wsgi.version: 表示WSGI版本,一個元組(1, 0),表示版本1.0
wsgi.url_scheme: http或者https
wsgi.input: 一個類檔案的輸入流,application可以通過這個獲取HTTP request body
wsgi.errors: 一個輸出流,當 Application 出錯時,可以將錯誤資訊寫入這裡
wsgi.multithread: 當application物件可能被多個執行緒同時呼叫時,這個值需要為True
wsgi.multiprocess: 當application物件可能被多個程式同時呼叫時,這個值需要為True
wsgi.run_once: 當server期望application物件在程式的生命週期內只被呼叫一次時,該值為True
  • start_resposne :start_resposne的引用指向一個回撥函式(在 Server 中定義),這個回撥函式會接受兩個必選引數(status,resposne_headers)和一個可選引數(exc_info)。EG. start_resposne(status,resposne_headers,exc_info=None)
    當 application 物件根據 environ 引數的 Info 執行完 Client Request 的業務邏輯之後,需要返回結果給 Server 。而且 HTTP Response 需要包含 status、headers、body 等元素,所以在 application 物件將 body 作為返回值 return 之前,需要先呼叫 start_response()將 status、headers 先返回給 Server,同時也是告訴Server,application 物件要開始返回 body 了。當Web Server 接受完這些元素之後,就可以將這些元素封裝成為 HTTP Response 響應給 Client。

    • status: 一個字串,表示HTTP響應狀態的字串
    • response_headers: 是一個包含了以元組(header_name, header_value)作為元素的列表型別物件,分別表示HTTP響應的 headers 及其內容。
    • exc_info(可選): 在出現錯誤時使用,返回相關錯誤資訊給瀏覽器

回撥函式:就是一個通過函式指標(變數的引用)呼叫的函式。如果你把函式的指標(引用)作為引數傳遞給另一個函式,當這個指標(引用)被用來呼叫其所指向的函式時,我們就說這是回撥函式。回撥函式不是由該函式的實現方直接呼叫,而是在特定的事件或條件發生時由另外的一方呼叫的,用於對該事件或條件進行響應。所以 application 物件會在執行完請求的操作之後才會使用引數 start_response ,回撥該變數指向的函式,以此來將 status、headers return 給 Server 。

body = []
status_headers == [None,None]

def status_response(status,headers):
    status_headers[:] = [status,headers]
    return body.append(status_headers)

application 物件的返回值

application物件的返回值 body 用於 HTTP 響應,如果沒有 body 返回,那麼可以返回 None。如果有 body 返回,那麼要求返回 body 必須是一個可迭代的物件。Server 通過遍歷這個可迭代物件可以獲得 body 的全部內容。
EXAMPLE:

def application(environ, start_response):  #
      status = '200 OK'
      response_headers = [('Content-type', 'text/plain')]
      start_response(status, response_headers)  #將 body == ['hello, world'] 作為返回值 return 之前,先呼叫 start_response(status, response_headers) 將 status、headers 先返回給 Server
      return ['hello, world']

再談Server如何呼叫application

Server 通過配置和 Python Module 來找到 Application 的入口,在找到 application 物件之後,Server 會通過將 Client Request 所有的 Info 和一些符合規範的引數資訊傳遞給 application 物件作為實參,以此來實現 Server 將客戶端請求傳遞給 Application。然後 application 物件執行業務邏輯,在返回 body 之前需要呼叫start_resposne()回撥函式將HTTP響應所需要的資訊返回。最後返回一個可迭代的物件 body ,Server 最後通過遍歷來獲取完整的 Body 資料。

注意:environ 和 start_response() 是需要在Server中的生成和定義的。

WSGI 中介軟體

這裡寫圖片描述

WSGI Middleware 也是 WSGI 規範的一部分。 Middleware 是一個執行在 Server 和 Application 之間的應用。它同時具備了 Server 和 Application 的角色功能,對於 Server 來說,它是一個 Application;對於 Application來說,它是一個 Server。*middleware並不修改 Server 端和 Application 端的規範,只是同時實現了這兩個角色的功能而已,因此它可以在兩端之間起協調作用。而且 Middleware 可以將 Cllient HTTP Request 路由給不同的 Application 。*

Middleware 是如何工作的:
1.Server 收到客戶端的 HTTP 請求後,生成了environ_s,並且已經定義了start_response_s
2.Server 呼叫 Middleware 的 application 物件,傳遞的引數是environ_sstart_response_s
3.Middleware 會根據 environ 執行業務邏輯,生成environ_m,並且已經定義了start_response_m
4.Middleware 再呼叫 Application 的 application 物件,傳遞引數是environ_mstart_response_m。Application 的 application 物件處理完成後,會呼叫 start_response_m 並且返回結果給 Middleware,存放在 result_m 中。
5.Middleware 處理 result_m,然後生成 result_s,接著呼叫 start_response_s,並返回結果 result_s給 Server 端。Server 端獲取到 result_s 後就可以傳送結果給客戶端了。

從上面的流程可以看出middleware應用的幾個特點:
1. Server認為middleware是一個application。
2. Application認為middleware是一個server。
3. Middleware可以有多層。

我們可以將 WSGI Middleware 了理解為 Server 和 Application 互動的一層包裝,經過不同的 Middleware ,便擁有了不同的功能,EG. URL 路由轉發、許可權認證。因為Middleware能過處理所有通過的 request 和 response,所以要做什麼都可以,沒有限制。比如可以檢查 request 是否有非法內容,檢查 response 是否有非法內容,為 request 加上特定的 HTTP header 等。這些不同的 Middleware 組合便形成了 WSGI 的框架,比如我們將要介紹的 Paste。

WSGI的實現和部署

要使用WSGI,需要分別實現 Server 角色和 Application 角色。

  • Application 端一般是由Python的各種框架(Django, web.py)來實現的,一般開發者不需要關心 WSGI 的實現,框架會提供介面讓開發者獲取 HTTP 請求的內容以及傳送 HTTP 響應。

  • Server 端的實現會比較複雜一點,這個主要是因為軟體架構的原因。一般常用的Web伺服器,如Apache和nginx,都不會內建WSGI的支援,而是通過擴充套件外掛來完成。EG. Apache + mod_wsgi來支援WSGI。 Apache 和 mod_wsgi 之間通過程式內部介面傳遞資訊,mod_wsgi 會實現 WSGI 的 Server端、程式管理以及對 application 的呼叫。Nginx + uWSGI,用 nginx 的協議將請求封裝好,傳送給應用伺服器,應用伺服器會實現 WSGI 的 Server 端、程式管理以及對 application 的呼叫。

參考資料

WSGI 簡介
《Openstack 設計與實現》

相關文章