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.html
本文档详细描述了一个建议用在 Web 服务器和 Python Web 应用或框架之间的标准接口,以提高 Web 应用在各种 Web 服务器之间的可移植性。python
from PEP 3333git
从 PEP 3333 的这段总结来看,WSGI 就是一个 Python 官方建议用在 Web 服务器和 Python Web 应用框架之间的标准接口。程序员
首先,什么是服务器(server)? 通常来讲,server 有两重意思:github
做为开发者,通常提到 server 时指的都是后者,即一个长时间运行的软件程序。web
因此,什么是 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_queeu_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_queeu_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等)
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。