Python WSGI 简析
近期给自己挖了个新坑,打算着手写一个 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)
整个过程中,服务器端要做的主要有四件事情
- 处理客户端发来的 HTTP 请求,获取相关环境信息
- 实现供应用程序使用的回调函数
- 调用应用程序实现的 WSGI 接口,此时将环境信息和回调函数一同作为调用参数传入
- 将应用程序返回的内容 headers 和 result 作为 HTTP 请求的响应回传给客户端
实现回调函数的时候,我们一共定义了三个参数 status
,response_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 应用程序之间有怎样的关系?我们也许可以通过这样一个过程来进行解答:
- Nginx 收到了一个请求,通过负载均衡和反向代理,将请求传给了 WSGI 服务器。
- WSGI Server 收到请求内容,进行环境变量解析,传递和调用(其实这里已经到达了 Flask 手中,只不过是 Flask 框架底层的 WSGI Server 实现在进行处理)。
- Flask 应用收到 WSGI 调用,开始处理请求。
- WSGI Server 收到 Flask 应用的处理结果,并将其返回给 Nginx。
- Nginx 收到最终结果,将其回传给客户端。