Python Web開發:開發wsgi中間件

本文參考了:html

上篇文章簡要提到:wsgi 規範中的 app 是一個可調用對象,能夠經過嵌套調用的方式實現中間件的功能。這篇文章就來親自動手實現一下。python

此文的重點在於 app 端,因此 wsgi 服務器將使用python 內置module wsgiref.simple_server 中的make_servergit

建立 app

新建文件 app.py :github

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 headers
    status = '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()。web

這個 app 作的事情很是簡單,把傳過來的 environ 原樣返回。在開始返回body 以前,調用server傳過來的start_response函數。ajax

簡要說明一下爲何是 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 application

print("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 的詳細內容。其中比較重要的我用紅框框圈了起來。bash

第一個中間件: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 = app
        self.whitelist = whitelist

    def validate_origin(self, origin):
        """Validate that the origin of the request is whitelisted."""
        return origin and origin in self.whitelist

    def cors_response_factory(self, origin, start_response):
        """Create a start_response method that includes a CORS header for the specified 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_response

    def cors_options_app(self, origin, environ, start_response):
        """A small wsgi app that responds to preflight requests for the specified 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__方法說明這是一個可調用對象(類也能夠是可調用的),同樣接收兩個參數:environstart_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:9000http://localhost:9001http://localhost:9002,能夠發現http://localhost:9000http://localhost:9001成功發出了請求,而http://localhost:9002失敗了。

第二個中間件:請求耗時

這個比上一個要簡單不少,相信如今你已經徹底可以理解了:

import time

class ResponseTimingMiddleware(object):
    """A wrapper around an app to print out the response time for each request."""

    def __init__(self, app):
        self.app = app

    def __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) * 1000
        timing_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 ~

若是你像我同樣真正熱愛計算機科學,喜歡研究底層邏輯,歡迎關注個人微信公衆號:

相關文章
相關標籤/搜索