由内建库wsgiref理解wsgi

WSGI

WSGI是 Python Web Server Gateway Interface ,是为 Python 语言定义的 Web 服务器和 Web 应用程序或框架之间的一种简单而通用的接口。自从 WSGI 被开发出来以后,许多其它语言中也出现了类似接口

WSGI 是作为 Web 服务器(server)与 Web 应用程序或应用框架(application)之间的一种低级别的接口,以提升可移植 Web 应用开发的共同点。WSGI 是基于现存的 CGI 标准而设计的

  • WSGI是一套接口标准协议/规范
  • 通信(作用)区间是Web服务器和Python Web应用程序之间
  • 制定标准,以保证不同Web服务器可以和不同的Python程序之间相互通信

WSGI基本原理

wsgi
  1. 浏览器发送请求包到wsgi server后server解包并封装成environ
    • environ:http请求的参数以键值对字典的形式封装在environ中
  2. wsgi server传递environ和start_response函数给wsgi app
    • app:后台处理逻辑的程序,需要environ和start_response作为参数
  3. wsgi app 调用,在返回可迭代对象给 wsgi server之前,必须先调用 start_response 函数
    • start_response函数返回http status code、 headers和其他信息(response head)如Content-Type等给wsgi server
    • app返回值(可迭代对象)作为http body(response body) 返回给wsgi server
  4. wsgi server组装从app获取到的所有信息作为response返回给浏览器

wsgiref

wsgiref是Python提供的一个WSGI参考实现库,注意不适用于生产环境

  • wsgiref.simple_server 模块实现一个简单的WSGI HTTP服务器
    • wsgiref.simple_server.make_server(host, port, app, server_class=WSGIServer,
      handler_class=WSGIRequestHandler) 启动一个WSGI服务器
    • wsgiref.simple_server.demo_app(environ, start_response) 两参数函数demo_app为WSGI的应用程序
from wsgiref.simple_server import make_server, demo_app

host = '127.0.0.1'
port = 9999

server = make_server(host, port, demo_app)
server.serve_forever()

启动后,通过浏览器访问127.0.0.1:9999则可看到返回响应数据

WSGI 服务器作用:

  • 监听HTTP服务端口(TCPServer,默认端口80)
  • 接收浏览器端的HTTP请求并解析封装成environ环境数据
  • 负责调用应用程序,将environ数据和start_response方法两个实参传入给Application
  • 将应用程序响应的正文封装成HTTP响应报文返回浏览器端

WSGI App

先来看下wsgiref库中demo_app的源码:

def demo_app(environ, start_response):
    from io import StringIO
    stdout = StringIO() # stringio
    print("Hello world!", file=stdout) # 正如我们在浏览器中看到的第一行
    print(file=stdout) # 打印空行
    h = sorted(environ.items()) # 排序封装的environ中的kv
    for k, v in h:
        print(k, '=', repr(v), file=stdout) # 遍历kv
    start_response("200 OK", [('Content-Type', 'text/plain; charset=utf-8')]) # 在返回之前先调用start_response(返回http status code、 headers和其他信息)
    return [stdout.getvalue().encode("utf-8")] # 返回可迭代对象

通过以上对demo_app的分析,可以看出:

  1. make_server(host, port, app)中的参数app需要满足app是一个可调用对象
  2. app必须返回一个可迭代对象

environ

改写如上demo_app来看看我们关心的一些请求参数

from wsgiref.simple_server import make_server

host = '127.0.0.1'
port = 9999

def app(environ, start_response):
    from io import StringIO
    stdout = StringIO()
    for k, v in environ.items():
        if k.startswith('HTTP') or k.startswith('SERVER') or k in ("QUERY_STRING", "REQUEST_METHOD", "PATH_INFO", "CONTENT_TYPE"):
            print(k, '=', v, file=stdout)
    start_response("200 OK", [('Content-Type', 'text/plain; charset=utf-8')])
    return [stdout.getvalue().encode("utf-8"), b'abc~~~~']

server = make_server(host, port, app)
server.serve_forever()

浏览器访问127.0.0.1:9999,我们可以看到类似如下信息:

SERVER_NAME = 1.0.0.127.in-addr.arpa
SERVER_PORT = 9999
SERVER_PROTOCOL = HTTP/1.1
SERVER_SOFTWARE = WSGIServer/0.2
REQUEST_METHOD = GET
PATH_INFO = /
QUERY_STRING = 
CONTENT_TYPE = text/plain
HTTP_HOST = 127.0.0.1:9999
HTTP_CONNECTION = keep-alive
HTTP_SEC_CH_UA = "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
HTTP_SEC_CH_UA_MOBILE = ?0
HTTP_DNT = 1
HTTP_UPGRADE_INSECURE_REQUESTS = 1
HTTP_USER_AGENT = Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
HTTP_ACCEPT = text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
HTTP_SEC_FETCH_SITE = none
HTTP_SEC_FETCH_MODE = navigate
HTTP_SEC_FETCH_USER = ?1
HTTP_SEC_FETCH_DEST = document
HTTP_ACCEPT_ENCODING = gzip, deflate, br
HTTP_ACCEPT_LANGUAGE = zh-CN,zh;q=0.9,en;q=0.8
abc~~~~

修改访问地址为127.0.0.1:9999/abc?d=100,再比较下变化

start_response

  • start_response(status, response_headers, exc_info=None)
    • status:状态码和状态描述,例如 200 OK
    • response_headers:一个元素为二元组的列表,例如[('Content-Type', 'text/plain;charset=utf-8')],可修改Content-Type即MIME类型来决定浏览器响应结果的展示形式
    • exc_info:在错误处理的时候使用

可调用对象

通过以上的分析已经有如下的结论,app必须满足如下条件

  1. app必须是可调用对象,且接受environ和start_response两个参数
  2. 在返回响应body之前必须调用start_response
  3. app返回的是一个可迭代对象
    可调用对象可以是函数、类、实现了 __call__ 方法的类的实例,关于可调用对象可参看可调用对象,接下来我们来看看除函数以外的app实现
  • 函数
    如上demo_app和demo_app,这里不再过多介绍

from wsgiref.simple_server import make_server

host = '127.0.0.1'
port = 9999

class Application:
    def __init__(self, environ, start_response):
        self.environ = environ
        self.start_response = start_response
        self.html = '<h1>欢迎来到 monkeyjerry 的个人小站</h1>'.encode("utf-8")

    def __iter__(self):
        # __iter__必须返回迭代器iterator
        self.start_response("200 OK", [('Content-Type', 'text/html; charset=utf-8')])
        # yield self.html
        return iter([self.html])

server = make_server(host, port, Application)
server.serve_forever()

更多关于类容器化可参看魔术方法iter

  • 实现了__call__ 方法的类实例
from wsgiref.simple_server import make_server

host = '127.0.0.1'
port = 9999

class App:
    def __init__(self):
        self.html = b'Hello MonkeyJerry\n'

    def __call__(self, environ, start_response):
        start_response("200 OK", [('Content-Type', 'text/plain; charset=utf-8')])
        return [self.html]

server = make_server(host, port, App())   # App()类实例是可调用对象
server.serve_forever()

可进一步参看可调用对象__call__

Web框架

Web框架如Django,Flask等可以看作是比较复杂的App

有了WSGI接口标准协议,则允许开发者互不干扰地选择 Web 框架及 Web 服务器的类型。Web 服务器必须实现 WSGI 接口的服务器部分,而现代的 Python Web 框架均已实现了 WSGI 接口的框架部分。现在,则可以真正将 Web 服务器及框架任意搭配,比如,你可以使用 Django,Flask 或者 Pyramid,与 Gunicorn,Nginx/uWSGI 或 Waitress 进行结合

这样我们就只需专注于其擅长的部分来进行开发,而不需要触及另一部分的代码。其它语言也拥有类似的接口,比如:Java 拥有 Servlet API,而 Ruby 拥有 Rack

参考及扩展阅读