请注意:本文编写于 2020-06-24,其中某些信息可能已经失去时效性。
如果你也想阅读 WSGI 相关的 PEP 规范,建议直接阅读 PEP 3333,因为 PEP 3333 对 PEP 333 是向下兼容的,也可以说 PEP 3333 是对 PEP 333 的补充。
This document specifies a proposed standard interface between web servers and Python web applications or frameworks, to promote web application portability across a variety of web servers. 本文档详细描述了一个建议用在 Web 服务器和 Python Web 应用或框架之间的标准接口,以提升 Web 应用在各类 Web 服务器之间的可移植性。 from PEP 3333
从 PEP 3333 的这段总结来看,WSGI 就是一个 Python 官方建议用在 Web 服务器和 Python Web 应用框架之间的标准接口。
首先,什么是服务器(server)? 一般来说,server 有两重意思:
作为开发者,一般提到 server 时指的都是后者,即一个长时间运行的软件程序。
所以,什么是 Web Server? 通俗的来讲 Web Server 就是一个提供 Web 服务的应用程序。 常见的符合 WSGI 规范的 Web Server 有 uWSGI、gunicorn 等等。
Web 框架在如今是比较常见的,比较知名的 Python Web 框架有:Django、Flask、Pyramid等等。反倒是 Web 应用不太常见,(个人理解)一般情况下只有在本地测试的时候会写一些简单的 Python Web 应用,平时的开发大多还是使用开源(或公司内部)的 Web 框架。
作为一个近两年刚接触到 Python Web 编程的新手,在日常的编程过程中完全没有见过所谓的 WSGI,但是我依然可以写好一个完整的 Web 应用,这是为什么?WSGI 有存在的必要嘛?
答案肯定是:有存在的必要。
首先解释一下为什么我在过去两年的过程中没有见过 WSGI 却依旧可以进行 Web 编程:因为现在的大多数框架都已经帮我们将 WSGI 标准封装在框架底层。甚至,我用的 Django REST Framework 框架连 HTTP Request 和 HTTP Response 都帮我封装好了。所以,就算我完全不了解 WSGI 这种偏底层的协议也能够进行日常的 Web 开发。
那 WSGI 到底解决了什么问题?这个在 PEP 3333 中有详细的解释,简单的说一下我的理解:在 WSGI 诞生之前,就已经存在了大量使用 Python 编写的 Web 应用框架,相应的也存在很多 Web 服务器。但是,各个 Python Web 框架和 Python Web 服务器之间不能互相兼容。夸张一点说,在当时如果想要开发一个 Web 框架说不定还得单独为这个框架开发一个 Web 服务器(而且这个服务器别的框架还不能用)。为了解决这一现象 Python 社区提交了 PEP 333,正式提出了 WSGI 这个概念。
简单的理解:只要是兼容 WSGI 的 Web 服务器和 Web 框架就能配套使用。开发服务器的程序员只需要考虑在兼容 WSGI 的情况下如何更好的提升服务器程序的性能;开发框架的程序员只需要考虑在兼容 WSGI 的情况下如何适应尽可能多业务开发逻辑(以上只是举例并非真的这样)。
WSGI 解放了 Web 开发者的精力让他们可以专注于自己需要关注的事情。
注:为了简练而写成了 WSGI 做了什么事情,实际上 WSGI 只是一个规范并不是实际的代码,准确的来说应该是「符合 WSGI 规范的 Web 体系做了什么事情?」
上面已经提到,WSGI 通过规范化 Web 框架和 Web 服务器之间的接口,让兼容了 WSGI 的框架和服务器能够自由组合使用……
所以,WSGI 究竟做了什么,让一切变得如此简单?
在 PEP 3333 中对 WSGI 进行了一段简单的概述,这里我结合看过的 一篇博文 进行简单的概括:
(简单来说)WSGI 将 Web 分成了三个部分,从上到下分别是:Application/Framework, Middleware 和 Server/Grageway,各个部分之间高度解耦尽可能的做到不互相依赖。
Middleware 属于三个部分中最为特别的一个,对于 Server 他是一个 Application,对于 Application 它是一个 Server。通俗的来说就是 Middleware 面对 Server 时能够展现出 Application 应有的特性,而面对 Application 时能够展现出 Server 应有的特性,由于这一特点 Middleware 在整个协议中起到了承上启下的功能。在现实开发过程中,还可以通过嵌套 Middleware 以实现更强大的功能。
通过上一小节能够大概的了解到 WSGI 在一次完整的请求中究竟做了什么。下面再来介绍一下一个完整的 WSGI Web 体系是如何工作的。
为了方便展示先来构建一个符合 WSGI 规范的 Python Web 项目示例:
注:示例基于 Python3
# 本示例代码改自参考文章 5:
# Huang Huang 的博客-翻译项目系列-让我们一起来构建一个 Web 服务器
# /path_to_code/server.py
# Examples of wsgi server
import sys
import socket
# 根据系统导入响应的 StringIO 模块
# StringIO:用于文本 I/O 的内存数据流
try:
from io import StringIO
except ImportError:
from cStringIO import StringIO
class WSGIServer(object):
request_queue_size = 1 # 请求队列长度
address_family = socket.AF_INET # 设置地址簇
socket_type = socket.SOCK_STREAM # 设置 socket 类型
def __init__(self, server_address):
# Server 初始化方法(构造函数)
# Create a listening socket
self.listen_socket = listen_socket = socket.socket(
self.address_family,
self.socket_type
)
# 设置 socket 允许重复使用 address
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Bind 绑定端口
listen_socket.bind(server_address)
# Activate 激活
listen_socket.listen(self.request_queue_size)
# 获取并记录 server host 和 port
host, port = self.listen_socket.getsockname()[:2]
self.server_name = socket.getfqdn(host)
self.server_port = port
# Return headers set by Web framework/application
self.headers_set = []
def set_app(self, application):
# 将传入的 application 设置为实例属性
self.application = application
def server_forever(self):
# 开启 server 循环函数
listen_socket = self.listen_socket
while True:
# 获取 client socket 参数 | client_connection 是 client socket 实例
# 这里会创建一个阻塞,直到接受到 client 连接为止
self.client_connection, client_address = listen_socket.accept()
# 调用 handle_one_request 方法处理一次请求并关闭 client 连接然后继续等待新的连接进入
self.handle_one_request()
def handle_one_request(self):
# 处理请求的入口方法 | 用来处理一次请求
# 从 client socket 中获取 request data
self.request_data = request_data = self.client_connection.recv(1024)
# 调用 parse_request 方法, 传入接收到的 request_data 并对其进行解析
self.parse_request(request_data)
# 通过已有数据构造环境变量字典
environ = self.get_environ()
# 调用 application,传入已经生成好的 environ 和 start_response,返回一个可迭代的 Response 对象
result = self.application(environ, self.start_response)
# 调用 finish_response 方法,构造一个响应并返回给客户端
self.finish_response(result)
def parse_request(self, text):
# 取行
request_line = text.splitlines()[0]
# 打碎请求行到组件中
(self.request_method,
self.path,
self.request_version
) = request_line.split()
def get_environ(self):
env = {}
env["wsgi.version"] = (1, 0)
env["wsgi.url_scheme"] = "http"
env["wsgi.input"] = StringIO(self.request_data.decode("utf-8"))
env["wsgi.errors"] = sys.stderr
env["wsgi.multithread"] = False
env["wsgi.multiprocess"] = False
env["wsgi.run_once"] = False
# Required CGI variables
env["REQUEST_METHOD"] = self.request_method
env["PATH_INFO"] = self.path.decode("utf-8")
env["SERVER_NAME"] = self.server_name
env["SERVER_PORT"] = str(self.server_port)
return env
def start_response(self, status, response_headers, exc_info=None):
# 按照 WSGI 规范提供一个 start_response 给 application
# Add necessary必要的 server headers
server_headers = [
("Date", "Tue, 31 Mar 2020 12:51:48 GMT"),
("Server", "WSGIServer 0.2")
]
self.headers_set = [status, response_headers + server_headers]
# 按照 WSGI 协议,应该在这里返回一个 write(),但这里为了简便就省略了
# 会在后续分析 wsgiref 源码时提及此处
def finish_response(self, result):
# 通过现有参数整理出一个响应体
try:
status, response_headers = self.headers_set
# 响应第一部分:HTTP 协议以及状态码
response = f"HTTP/1.1 {status}\r\n"
# 响应第二部分:将生成好的响应头递归式的传入响应体内
for header in response_headers:
response += "{0}: {1}\r\n".format(*header)
# 通过 \r\n 进行空行
response += "\r\n"
# 响应第三部分:将响应主题信息追加到响应体内
for data in result:
response += data
# 通过 senall 将响应发送给客户端
# 注意:在 Python3 下,如果你构建的响应体为 str 类型,需要进行 encode 转换为 bytes
self.client_connection.sendall(response.encode())
finally:
# 关闭连接
self.client_connection.close()
# /path_to_code/middleware.py
# Examples of wsgi middleware
class TestMiddleware(object):
def __init__(self, application):
self.application = application
def core(self, environ, start_response):
old_response = self.application(environ, start_response)
new_response = old_response + ["middleware add this message\n"]
return new_response
def __call__(self, environ, start_response):
return self.core(environ, start_response)
# /path_to_code/application.py
# Examples of wsgi application
def application(environ, start_response):
status = "200 OK"
response_headers = [("Content-Type", "text/plain")]
start_response(status, response_headers)
return ["hello world from a simple WSGI application!\n"]
# /path_to_code/run.py
# running Example
from server import WSGIServer
from application import application
from middleware import TestMiddleware
# 规定 server host 和 server port
server_address = (host, port) = "", 8888
# 创建 server 实例
server = WSGIServer(server_address)
# 设置本 server 对应的 middleware 以及 application
server.set_app(TestMiddleware(application))
# 输出提示性语句
print(f"WSGIServer: Serving HTTP on port: {port}...\n")
# 进入 server socket 监听循环
server.server_forever()
将四段代码分别复制到同一目录的四个文件(如果没有按照示例给出的命名记得更改一下 run 模块中相应的 import 的模块名)中。
注:以下操作默认你完全按照示例代码中给出的命名进行文件命名
python /path_to_code/run.py
127.0.0.1:8888
查看效果curl -v http://127.0.0.1:8888
查看完整输出curl -v https://baidu.com
的输出查看区别上面我根据 WSGI 协议编写了三个文件(模块):server.py middleware.py application.py,分别对应 WSGI 里 server middleware application 这三个概念。然后通过 run.py 引入三个模块组成了一个完整的 server-middleware-application Web 程序并监听本地 8888 端口。
通过 run.py 中的代码我们能够清晰的看到一个 WSGI 类型的 Web 程序的运行流程:
server.__init__
方法)server.set_app
方法)server.server_forever
方法)通过 server.py 中的代码能够清晰的看到一个 WSGI 类型的 Web 程序是如何处理 HTTP 请求的:
server_forever
监听到客户端请求并记录请求信息handle_one_request
方法处理此请求parse_request
方法将请求数据解析成所需格式get_environ
方法利用现有数据构造环境变量字典finish_response
方法构造一个可迭代的响应对象返回给客户端并结束本次请求通过 middleware.py 中的代码就能够理解一个 WSGI 中间件是如何工作的:
__init__
方法中接收一个 application 将自己伪装成一个 server__call__
方法中接收 environ 和 start_response 参数将自己伪装成一个 application
通过这两点伪装 middleware 能够很好的粘合在 server 和 application 之间完成中间逻辑处理,在 PEP 3333 中指明了中间件的几点常见用途。至于 application.py 在这里就真的只是一个简单的单文件 WSGI 应用。当然也可以尝试用写好的 server.py 和 middleware.py 对接像 Django 这样的框架,但需要对代码做一些修改,这里就不展开讨论了,有兴趣可以自己尝试。
在运行 run.py 之后使用浏览器浏览 127.0.0.1:8888
并查看结果如下:
通过控制台可以清晰地看到响应头和响应主体的内容是符合我们预期的
通过 curl http://127.0.0.1:8888
可以看到响应主体:
通过 curl -v http://127.0.0.1:8888
可以看到详细的请求和响应内容:
通过 curl -v https://baidu.com
获取百度首页的响应内容以作比较:
可以看到目前浏览网页常用的正常请求要比自己构建的测试示例要复杂的多,这也是为什么经常使用 Web 框架而非单文件应用来处理这些请求的原因。
PEP 3333 我只读到了 Buffering and Streaming 章节,并且没能很好的理解此章节所描述的东西,因此在下面的细节分析中大都是此章节之前的一些内容。
可迭代对象(callable)和可迭代对象(iterable)在 PEP 3333 中最常见的两个词汇,在 WSGI 规范中它们分别代表:实现了 __call__
的对象和实现了 __iter__
的对象。
这是一组比较基础的概念:
Python3 中字符串的默认类型是 str,在内存中以 Unicode 表示。如果要在网络中传输或保存为磁盘文件,需要将 str 转换为 bytes 类型。
Python3 里面的 str 是在内存中对文本数据进行使用的,bytes 是对二进制数据使用的。 str 可以 encode 为 bytes,但是 bytes 不一定可以 decode 为 tr。实际上
bytes.decode(‘latin1’)
可以称为 str,也就是说 decode 使用的编码决定了decode()
的成败,同样的,UTF-8 编码的 bytes 字符串用 GBK 去decode()
也会出错。 bytes一般来自网络读取的数据、从二进制文件(图片等)读取的数据、以二进制模式读取的文本文件(.txt, .html, .py, .cpp等) from 知乎-猿人学-Python 3 中str 和 bytes 的区别
WSGI 中规定了两种 String:
在 PEP 3333 中有对这部分的详细说明。
了解了以上基础概念之后再具体的看一下 WSGI 的三个主要组成部件:
application(environ, start_response)
。而且这两个参数只能以位置参数的形式被传入。start_response(status, response_headers, exc_info=None)
。"200 OK"
__iter__
的对象。无论如何,application 必须返回一个能够产生零个或多个字符串 iterable。len(iterable)
能够被成功执行(这里的 iterable 指的是第 10 条中的 iterable)则其返回的必须是一个 server 能够信赖的结果。也就是说 application 返回的 iterable 如果提供了一个有效的 __len__
方法就必须能够获得准确值。可以参考我的开源库 read-python 中 practices/for_wsgiref 目录下的 server.py 文件。
在这个文件中我提取了 Python wsgiref 官方库的必要代码汇聚成一个文件实现了一个和 wsgiref.WSGIServer
大致同样功能的 WSGIServer
类。
Python wsgiref 官方库对 WSGI 规范的实现更加抽象,加上一些历史原因使得代码分布在多个官方库中,我在抽离代码的过程中学到了很多但是同样也产生了很多困惑,我在源码中使用 TODO 疑惑 XXX
的形式将我的困惑表达出来了,如果你感兴趣并且恰好知道解决我疑惑的方法,欢迎直接给我的代码仓库提交 Issues。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有