Python WSGI 简析

2019-12-27

近期给自己挖了个新坑,打算着手写一个 Python Web 框架,名字叫做 dopamine。谈及 Python Web 框架,自然而然也就提到了 WSGI,于是在此写一篇博客,当作完成整个项目过程中的知识整理和学习。

何为 WSGI

WSGI 全称 Python Web Server Gateway Interface,最早在 2003 年提出于 PEP 333 中,之后为了适应 Python 3 的一些变化,又在 PEP 3333 中做出了一些调整和修订,现行 WSGI 的版本号为 v1.0.1。

WSGI 被提出的背景,源于其诞生前市面上主流的 Python Web 程序(或者说框架)缺乏统一的设计,导致每个 Web 应用都拥有过于 Specialization 的特性,适用的服务器场景往往有所限制。为了提高 Python Web 应用开发的可移植性和统一设计标准,WSGI 孕育而生,其主要将一次 HTTP 请求的处理过程分为两个部分,并在这两个部分间设计了一套交互/通信标准。

  • The Server/Gateway Side 服务器/网关
  • The Application/Framework Side 应用程序/框架

服务器率先收到一次请求后,会对请求的信息进行处理,为应用程序提供环境信息和一个可供调用的回调函数(Callback Function)。当应用程序收到服务器发来的环境信息和回调函数后,会对请求进行实际的业务侧处理,实现具体的处理逻辑(比如说处理一次前端发来的登录账号密码验证),并透过前述的回调函数,将结果回传给服务器。WSGI 还设计了介于两者之间中间件 Middleware 的行为,但作为简析,就不放在本文的讨论范围了(其实是因为我还没怎么研究这块)。

区别于网上的一些教程和比较程式化的 PEP 内容,接下来我希望从一次完整的 HTTP 请求处理过程出发,来简单分析 WSGI 在其中扮演的角色,讲一下我自己的理解。

服务器/网关

一次 HTTP 请求通过 TCP 到达了服务器,此时服务器使用 Socket 读到了该次 HTTP 的请求内容,并进行了解析,将相应的环境信息,例如此次 HTTP 请求的 Headers 内容和远端 TCP 连接的信息,存储到变量 env 中去,然后实现一个供应用程序/框架调用的回调函数 start_response,下面用一段伪代码来模拟这个过程。

# -*- WSGI 服务器伪代码 server.py -*-
from my_web_app import application

# 读取 HTTP 请求进行环境信息获取
env = parse_http_request(socket.out) 
headers = []

# 回调函数需要接受至少两个参数,至多三个参数,稍后阐述不同参数的具体意义
def start_response(status, response_headers, exc_info=None):
    if exc_info:
        try:
            if something_wrong:
                raise exc_info[1].with_traceback(exc_info[2])
        finally:
            exc_info = None
    
    headers[:] = [status, response_headers]

# application 是我们稍后要定义的应用程序,result 即为 HTTP 请求的具体相应结果
result = application(env, start_response)

socket.write(headers)
# result 必须为一个可迭代对象,存储着用于返回的 Body 数据
for data in result:
    socket.write(data)

整个过程中,服务器端要做的主要有四件事情

  1. 处理客户端发来的 HTTP 请求,获取相关环境信息
  2. 实现供应用程序使用的回调函数
  3. 调用应用程序实现的 WSGI 接口,此时将环境信息和回调函数一同作为调用参数传入
  4. 将应用程序返回的内容 headers 和 result 作为 HTTP 请求的响应回传给客户端

实现回调函数的时候,我们一共定义了三个参数 statusresponse_headers 以及 exc_info,它们的具体内容和意义如下:

  • status HTTP 相应的状态参数,形如 '404 Not Found' 的字符串
  • response_headers HTTP 的响应头,形如 [('Content-Type', 'text/html')] 的二元组列表,对应着 Headers 中的相应字段及其值
  • exc_info 错误处理信息,用于应用程序返回给服务端进行错误处理

以上为服务器端在 WSGI 中扮演的主要责任和义务,为了主要描述底层思想,我通过伪代码进行了行为过程的表达,因而屏蔽了一些细节,诸如 HTTP 写回内容必须为 ISO-8859-1 编码等要求,如果你想自己实现一个 WSGI 服务器,详细请参考 PEP 3333 中的服务器部分介绍

应用程序/框架

由于 Python Web 框架的主要目的是为了构建 Python Web 程序,所以接下来我仅使用“应用程序”来作为这一部分的名称。

对比服务器端,应用程序要实现 WSGI 接口要做的事情可以说非常少,只需要实现一个可供服务器端调用的函数,类,甚至只要是一个拥有 __call__ 方法的对象即可。由于前文的 server.py 伪代码中我使用了 application 这个名字,所以在此沿用,以保持上下文的统一。

# -*- WSGI 应用程序伪代码 my_web_app.py -*-
def application(env, start_response):
    status = '200 OK'
    response_headers = [('Content-Type', 'text/plain')]
    start_response(status, response_headers)
    return [b'Hello, world!']

Well,实际上我们已经完成了一个符合 WSGI 规定的应用程序,它满足了所有作为一个 WSGI 应用的要素:

  • 可供调用
  • 接受两个参数
  • 返回一个可迭代的对象,其中存储着返回的 HTTP Body 内容

这个例子可能过于简单了,但是用于展示其底层设计思想是极有帮助的。但为了进一步帮助理解,我们还是再实现一个不太一样的,但依旧满足 WSGI 设计标准的应用程序。

# -*- WSGI 应用程序伪代码 my_web_app.py -*-
class application():
    def __init__(self, env, start_response):
        self.env = env
        self.start = start_response

    def __iter__(self):
        status = '200 OK'
        response_headers = [('Content-type’, ‘text/plain')]
        self.start(status, response_headers)
        yield b'Hello,'
        yield b' world!'

这次我们实现了一个类,同样在初始化的时候接受两个参数,并作为一个可迭代对象,通过 yield 作为生成器供服务器进行迭代返回数据,而不是 return 一个 list。

一些补充说明

首先,虽然 WSGI 在设计时分为了两大部分,服务端和应用程序,但这一划分其实是基于概念上,大部分时候也许并不意味着我们需要编写两个独立运行的 Python 程序,一个作为服务端,一个作为应用端。从上面的例子我们其实可以看出这两部分的代码其实关系十分紧密,甚至就是同一个程序的不同模块负责不同功能的区别,只是这两个功能上有区别的概念间约定了相应的标准,允许我们组合不同的模块来写出可移植性高,通用程度更强的代码来。

落回实际,如果看到这里你还是不太能理解 WSGI 所发挥的作用,不妨考虑这样一个问题,同一个物理服务器内,Nginx,WSGI 以及 Flask 应用程序之间有怎样的关系?我们也许可以通过这样一个过程来进行解答:

  1. Nginx 收到了一个请求,通过负载均衡和反向代理,将请求传给了 WSGI 服务器。
  2. WSGI Server 收到请求内容,进行环境变量解析,传递和调用(其实这里已经到达了 Flask 手中,只不过是 Flask 框架底层的 WSGI Server 实现在进行处理)。
  3. Flask 应用收到 WSGI 调用,开始处理请求。
  4. WSGI Server 收到 Flask 应用的处理结果,并将其返回给 Nginx。
  5. Nginx 收到最终结果,将其回传给客户端。
Tagged in : Python WSGI PEP HTTP