說說我對 WSGI 的理解

先說下 WSGI 的表面意思,Web Server Gateway Interface 的縮寫,即 Web 服務器網關接口。python

以前不知道 WSGI 意思的夥伴,看了上面的解釋後,我估計也仍是不清楚,因此下面結合實際場景說明,先讓你們有個大體的認識。最後咱們再本身實現一個,加深對 WSGI 的理解。git

咱們如今使用 Python 編寫 Web 應用,能夠用比較流行的 Flask、Django 框架,也能夠按本身的想法直接寫一個。可選的服務器軟件也特別多,好比常見的有 Apache、Nginx、IIS 等,除此外,也有不少小衆的軟件。可是,如今問題來了,我該怎麼部署?在沒有 WSGI 規範以前,一個服務器調度 Python 應用是用這種方式,另外一款服務器使用的是那種方式,這樣的話,編寫出來的應用部署時只能選擇侷限的某個或某些服務器,達不到通用的效果。github

注意:下文中的代碼基於 Python 3.6 編寫。flask

假若有這麼一個服務器後端

wsgi/server.pybash

# coding=utf-8

import socket

listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET,
                    socket.SO_REUSEADDR, 1)
listener.bind(('0.0.0.0', 8080))
listener.listen(1)
print('Serving HTTP on 0.0.0.0 port 8080 ...')

while True:
    client_connection, client_address = \
        listener.accept()
    print(f'Server received connection'
          f' from {client_address}')
    request = client_connection.recv(1024)
    print(f'request we received: {request}')

    response = b""" HTTP/1.1 200 OK Hello, World! """
    client_connection.sendall(response)
    client_connection.close()
複製代碼

實現比較簡單,就是監聽 8080 端口,若是有請求在終端進行打印,並返回 Hello, World! 的響應。服務器

終端中啓動服務器app

➜  wsgi python server.py
Serving HTTP on 0.0.0.0 port 8080 ...
複製代碼

再開一個終端,請求下框架

➜  ~ curl 127.0.0.1:8080
HTTP/1.1 200 OK

Hello, World!
複製代碼

說明服務器工做正常。curl

另外有一個 Web 應用

wsgi/app.py

# coding=utf-8


def simple_app():
    return b'Hello, World!\r\n'
複製代碼

如今要部署(也就是讓這個總體跑起來),簡單粗暴的作法就是在服務器裏面直接調用 app 中相應的方法。就像這樣

wsgi/server2.py

# coding=utf-8

import socket

listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET,
                    socket.SO_REUSEADDR, 1)
listener.bind(('0.0.0.0', 8080))
listener.listen(1)
print('Serving HTTP on 0.0.0.0 port 8080 ...')

while True:
    client_connection, client_address = \
        listener.accept()
    print(f'Server received connection'
          f' from {client_address}')
    request = client_connection.recv(1024)
    print(f'request we received: {request}')

    from app import simple_app
    response = 'HTTP/1.1 200 OK\r\n\r\n'
    response = response.encode('utf-8')
    response += simple_app()

    client_connection.sendall(response)
    client_connection.close()
複製代碼

運行腳本

注意:由於使用端口相同的緣故,請先關閉上次的腳本,而後再執行,否則會因爲端口衝突而報錯。

➜  wsgi python server2.py
Serving HTTP on 0.0.0.0 port 8080 ...
複製代碼

而後請求一下看看效果

➜  ~ curl 127.0.0.1:8080
Hello, World!
複製代碼

嗯,能夠了。可是,上面的服務器和應用總體是跑起來了,那麼我換一個服務器或者應用呢。因爲服務器與應用之間怎麼交互徹底沒有規範,好比服務器應該如何把請求信息傳給應用,應用處理完畢後又怎麼告訴服務器開始返回響應,若是都是各搞各的,服務器須要定製應用,應用也要定製服務器,這要一個應用能跑起來也太麻煩了點吧。

因此,WSGI 的出現就是爲了解決上面的問題,它規定了服務器怎麼把請求信息告訴給應用,應用怎麼把執行狀況回傳給服務器,這樣的話,服務器與應用都按一個標準辦事,只要實現了這個標準,服務器與應用隨意搭配就能夠,靈活度大大提升。

WSGI 規範了些什麼,下圖能很直觀的說明。

圖片來自 https://www.toptal.com

首先,應用必須是一個可調用對象,能夠是函數,也能夠是實現了 __call__() 方法的對象。

每收到一個請求,服務器會經過 application_callable(environ, start_response) 調用應用。

應用在處理完畢準備返回數據的時候,先調用服務傳給它的函數 start_response(status, headers, exec_info),最後再返回可迭代對象做爲數據。(不理解可迭代對象的夥伴能夠看下我以前的一篇文章《搞清楚Python的迭代器、可迭代對象、生成器》)

其中,environ 必須是一個字典,包括了請求的相關信息,好比請求方式、請求路徑等等,start_response 是應用處理完畢後,須要調用的函數,用於告訴服務設置響應的頭部信息或錯誤處理等等。

status 必須是 999 Message here 這樣的字符串,好比 200 OK404 Not Found 等,headers 是一個由 (header_name, header_value) 這樣的元祖組成的列表,最後一個 exec_info 是可選參數,通常在應用出現錯誤的時候會用到。

知道了 WSGI 的大體概念,下面咱們來實現一個。

首先是應用

wsgi/wsgi_app.py

# coding=utf-8


def simple_app(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [f'Request {environ["REQUEST_METHOD"]}'
            f' {environ["PATH_INFO"]} has been'
            f' processed\r\n'.encode('utf-8')]
複製代碼

這裏定義了一個函數(可調用對象),它可使用服務器傳給它的請求相關的內容 environ,這裏使用了 REQUEST_METHOD 和 PATH_INFO 信息。在返回以前調用了 start_response,方便服務器設置一些頭部信息。

而後是服務器

wsgi/wsgi_server.py

# coding=utf-8

import socket

listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET,
                    socket.SO_REUSEADDR, 1)
listener.bind(('0.0.0.0', 8080))
listener.listen(1)
print('Serving HTTP on 0.0.0.0 port 8080 ...')

while True:
    client_connection, client_address = \
        listener.accept()
    print(f'Server received connection'
          f' from {client_address}')
    request = client_connection.recv(1024)
    print(f'request we received: {request}')

    headers_set = None

    def start_response(status, headers):
        global headers_set
        headers_set = [status, headers]

    method, path, _ = request.split(b' ', 2)
    environ = {'REQUEST_METHOD': method.decode('utf-8'),
               'PATH_INFO': path.decode('utf-8')}
    from wsgi_app import simple_app
    app_result = simple_app(environ, start_response)

    response_status, response_headers = headers_set
    response = f'HTTP/1.1 {response_status}\r\n'
    for header in response_headers:
        response += f'{header[0]}: {header[1]}\r\n'
    response += '\r\n'
    response = response.encode('utf-8')
    for data in app_result:
        response += data

    client_connection.sendall(response)
    client_connection.close()
複製代碼

服務器監聽相關代碼沒怎麼變化,主要是處理請求的時候有些不一樣。

首先定義了 start_response(status, headers) 函數,自身並不會調用。

而後調用應用,將當前的請求信息 environ 和上面的 start_response 函數傳給它,讓其本身決定使用什麼請求信息以及在處理完成準備返回數據以前調用 start_response 設置頭部信息。

好了,啓動服務器後(即執行服務器代碼,和以前的相似,這裏不贅述),而後請求看看結果

➜  ~ curl 127.0.0.1:8080/user/1
Request GET /user/1 has been processed
複製代碼

嗯,程序是正常的。

上面爲了說明,代碼耦合性較大,若是服務器須要更換應用的話,還得修改服務器代碼,這顯然是有問題的。如今原理差很少說清楚了,咱們把代碼優化下

wsgi/wsgi_server_oop.py

# coding=utf-8

import socket
import sys


class WSGIServer:
    def __init__(self):
        self.listener = socket.socket()
        self.listener.setsockopt(socket.SOL_SOCKET,
                                 socket.SO_REUSEADDR, 1)
        self.listener.bind(('0.0.0.0', 8080))
        self.listener.listen(1)
        print('Serving HTTP on 0.0.0.0'
              ' port 8080 ...')
        self.app = None
        self.headers_set = None

    def set_app(self, application):
        self.app = application

    def start_response(self, status, headers):
        self.headers_set = [status, headers]

    def serve_forever(self):
        while True:
            listener = self.listener
            client_connection, client_address = \
                listener.accept()
            print(f'Server received connection'
                  f' from {client_address}')
            request = client_connection.recv(1024)
            print(f'request we received: {request}')

            method, path, _ = request.split(b' ', 2)
            # 爲簡潔的說明問題,這裏填充的內容有些隨意
            # 若是有須要,能夠自行完善
            environ = {
                'wsgi.version': (1, 0),
                'wsgi.url_scheme': 'http',
                'wsgi.input': request,
                'wsgi.errors': sys.stderr,
                'wsgi.multithread': False,
                'wsgi.multiprocess': False,
                'wsgi.run_once': False,
                'REQUEST_METHOD': method.decode('utf-8'),
                'PATH_INFO': path.decode('utf-8'),
                'SERVER_NAME': '127.0.0.1',
                'SERVER_PORT': '8080',
            }
            app_result = self.app(environ, self.start_response)

            response_status, response_headers = self.headers_set
            response = f'HTTP/1.1 {response_status}\r\n'
            for header in response_headers:
                response += f'{header[0]}: {header[1]}\r\n'
            response += '\r\n'
            response = response.encode('utf-8')
            for data in app_result:
                response += data

            client_connection.sendall(response)
            client_connection.close()


if __name__ == '__main__':
    if len(sys.argv) < 2:
        sys.exit('Argv Error')
    app_path = sys.argv[1]
    module, app = app_path.split(':')
    module = __import__(module)
    app = getattr(module, app)

    server = WSGIServer()
    server.set_app(app)
    server.serve_forever()
複製代碼

基本原理沒變,只是使用了面向對象的方式修改了下原來的代碼,同時 environ 添加了一些必要的環境信息。

可使用之前的應用

➜  wsgi python wsgi_server_oop.py wsgi_app:simple_app
Serving HTTP on 0.0.0.0 port 8080 ...
複製代碼

請求

➜  ~ curl 127.0.0.1:8080/user/1
Request GET /user/1 has been processed
複製代碼

獲得和以前相同的結果。

Flask 應用能行嗎?來試一試,先新建一個

wsgi/flask_app.py

# coding=utf-8

from flask import Flask
from flask import Response

flask_app = Flask(__name__)


@flask_app.route('/user/<int:user_id>',
                 methods=['GET'])
def hello_world(user_id):
    return Response(
        f'Get /user/{user_id} has been'
        f' processed in flask app\r\n',
        mimetype='text/plain'
    )
複製代碼

從新啓動服務器

➜  wsgi python wsgi_server_oop.py flask_app:flask_app
Serving HTTP on 0.0.0.0 port 8080 ...
複製代碼

請求

➜  ~ curl 127.0.0.1:8080/user/1
Get /user/1 has been processed in flask app
複製代碼

由於 Flask 也是遵照 WSGI 規範的,因此執行也沒有問題。

至此,一個粗略的 WSGI 規範就實現了,雖然說代碼不優雅,一些核心的東西仍是體現出來了。不過畢竟忽略了不少東西,好比錯誤處理等,要在生產環境中使用的話還遠遠不夠,想知道得更全面的夥伴能夠去看看 PEP 3333。

目前流行的 Web 應用框架好比 Django、Bottle 等,服務器 Apahce、Nginx、Gunicorn 等也都支持這個規範。所以,框架和應用隨意搭配基本沒什麼問題。

參考

關注公衆號「小小後端」閱讀更多精彩文章。

相關文章
相關標籤/搜索