WSGI:Python 的服务器-应用程序接口
已发表: 2022-03-111993 年,网络仍处于起步阶段,拥有大约 1400 万用户和 100 个网站。 页面是静态的,但已经需要生成动态内容,例如最新的新闻和数据。 对此,Rob McCool 和其他贡献者在国家超级计算应用中心 (NCSA) HTTPd Web 服务器(Apache 的前身)中实现了通用网关接口 (CGI)。 这是第一个可以提供由单独应用程序生成的内容的 Web 服务器。
此后,互联网上的用户数量呈爆炸式增长,动态网站无处不在。 当第一次学习一门新语言甚至第一次学习编码时,开发人员很快就会想知道如何将他们的代码连接到网络中。
Web 上的 Python 和 WSGI 的兴起
自 CGI 创建以来,发生了很大变化。 CGI 方法变得不切实际,因为它需要在每个请求时创建一个新进程,从而浪费内存和 CPU。 出现了一些其他低级方法,如 FastCGI](http://www.fastcgi.com/) (1996) 和 mod_python (2000),在 Python Web 框架和 Web 服务器之间提供不同的接口。 随着不同方法的激增,开发人员对框架的选择最终限制了 Web 服务器的选择,反之亦然。
为了解决这个问题,2003 年 Phillip J. Eby 提出了 PEP-0333,即 Python Web 服务器网关接口 (WSGI)。 这个想法是在 Python 应用程序和 Web 服务器之间提供一个高级的通用接口。
2003 年,PEP-3333 更新了 WSGI 接口以添加 Python 3 支持。 如今,几乎所有 Python 框架都使用 WSGI 作为与 Web 服务器进行通信的一种手段,如果不是唯一手段的话。 这就是 Django、Flask 和许多其他流行框架的做法。
本文旨在让读者了解 WSGI 的工作原理,并允许读者构建一个简单的 WSGI 应用程序或服务器。 不过,这并不意味着详尽无遗,打算实现生产就绪服务器或应用程序的开发人员应该更彻底地研究 WSGI 规范。
Python WSGI 接口
WSGI 指定了服务器和应用程序必须遵守的简单规则。 让我们从回顾这个整体模式开始。
应用程序接口
在 Python 3.5 中,应用程序接口如下所示:
def application(environ, start_response): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [body]
在 Python 2.7 中,这个接口不会有太大的不同。 唯一的变化是主体由一个str
对象表示,而不是一个bytes
。
虽然我们在这种情况下使用了一个函数,但任何可调用的都可以。 此处应用程序对象的规则是:
- 必须是带有
environ
和start_response
参数的可调用对象。 - 必须在发送正文之前调用
start_response
回调。 - 必须返回带有文档正文部分的可迭代对象。
满足这些规则并产生相同效果的对象的另一个示例是:
class Application: def __init__(self, environ, start_response): self.environ = environ self.start_response = start_response def __iter__(self): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] self.start_response(status, headers) yield body
服务器接口
一个 WSGI 服务器可以像这样与这个应用程序交互::
def write(chunk): '''Write data back to client''' ... def send_status(status): '''Send HTTP status code''' ... def send_headers(headers): '''Send HTTP headers''' ... def start_response(status, headers): '''WSGI start_response callable''' send_status(status) send_headers(headers) return write # Make request to application response = application(environ, start_response) try: for chunk in response: write(chunk) finally: if hasattr(response, 'close'): response.close()
您可能已经注意到, start_response
可调用对象返回了一个write
可调用对象,应用程序可以使用该可调用对象将数据发送回客户端,但我们的应用程序代码示例并未使用该可调用对象。 这个write
接口已被弃用,我们现在可以忽略它。 稍后将在文章中简要讨论。
服务器职责的另一个特点是调用响应迭代器上的可选close
方法(如果存在)。 正如 Graham Dumpleton 在此处的文章中所指出的,它是 WSGI 的一个经常被忽视的特性。 调用此方法(如果存在)允许应用程序释放它可能仍持有的任何资源。
应用程序可调用的environ
参数
environ
参数应该是一个字典对象。 它用于将请求和服务器信息传递给应用程序,与 CGI 的方式非常相似。 事实上,所有 CGI 环境变量在 WSGI 中都是有效的,服务器应该将所有适用于应用程序的变量传递给应用程序。
虽然可以传递许多可选键,但有几个是强制性的。 以下面的GET
请求为例:
$ curl 'http://localhost:8000/auth?user=obiwan&token=123'
这些是服务器必须提供的键,以及它们将采用的值:
钥匙 | 价值 | 注释 |
---|---|---|
REQUEST_METHOD | "GET" | |
SCRIPT_NAME | "" | 服务器设置依赖 |
PATH_INFO | "/auth" | |
QUERY_STRING | "token=123" | |
CONTENT_TYPE | "" | |
CONTENT_LENGTH | "" | |
SERVER_NAME | "127.0.0.1" | 服务器设置依赖 |
SERVER_PORT | "8000" | |
SERVER_PROTOCOL | "HTTP/1.1" | |
HTTP_(...) | 客户端提供的 HTTP 标头 | |
wsgi.version | (1, 0) | 带有 WSGI 版本的元组 |
wsgi.url_scheme | "http" | |
wsgi.input | 类文件对象 | |
wsgi.errors | 类文件对象 | |
wsgi.multithread | False | 如果服务器是多线程的,则为True |
wsgi.multiprocess | False | 如果服务器运行多个进程,则为True |
wsgi.run_once | False | 如果服务器希望此脚本只运行一次,则为True (例如:在 CGI 环境中) |
此规则的例外是,如果这些键之一为空(如上表中的CONTENT_TYPE
),则可以从字典中省略它们,并假定它们对应于空字符串。
wsgi.input
和wsgi.errors
大多数environ
键都很简单,但其中两个需要进一步说明: wsgi.input
,它必须包含带有来自客户端的请求正文的流,以及wsgi.errors
,应用程序报告它遇到的任何错误。 从应用程序发送到wsgi.errors
的错误通常会发送到服务器错误日志。
这两个键必须包含类似文件的对象; 也就是说,提供接口以作为流读取或写入的对象,就像我们在 Python 中打开文件或套接字时获得的对象一样。 起初这可能看起来很棘手,但幸运的是,Python 为我们提供了很好的工具来处理这个问题。

首先,我们在谈论什么样的流? 根据 WSGI 定义, wsgi.input
和wsgi.errors
必须处理 Python 3 中的bytes
对象和 Python 2 中的str
对象。无论哪种情况,如果我们想使用内存缓冲区通过 WSGI 传递或获取数据接口,我们可以使用类io.BytesIO
。
例如,如果我们正在编写一个 WSGI 服务器,我们可以像这样向应用程序提供请求正文:
- 对于 Python 2.7
import io ... request_data = 'some request body' environ['wsgi.input'] = io.BytesIO(request_data)
- 对于 Python 3.5
import io ... request_data = 'some request body'.encode('utf-8') # bytes object environ['wsgi.input'] = io.BytesIO(request_data)
在应用程序方面,如果我们想将接收到的流输入转换为字符串,我们需要编写如下内容:
- 对于 Python 2.7
readstr = environ['wsgi.input'].read() # returns str object
- 对于 Python 3.5
readbytes = environ['wsgi.input'].read() # returns bytes object readstr = readbytes.decode('utf-8') # returns str object
wsgi.errors
流应该用于向服务器报告应用程序错误,并且行应该以\n
结尾。 Web 服务器应注意根据系统转换为不同的行尾。
Application Callable 的start_response
参数
start_response
参数必须是一个可调用的,带有两个必需参数,即status
和headers
,以及一个可选参数exc_info
。 在将正文的任何部分发送回 Web 服务器之前,应用程序必须调用它。
在本文开头的第一个应用示例中,我们将响应的主体作为列表返回,因此我们无法控制何时迭代列表。 因此,我们必须在返回列表之前调用start_response
。
在第二个中,我们在产生响应正文的第一个(并且在本例中是唯一一个)片段之前调用了start_response
。 无论哪种方式在 WSGI 规范中都是有效的。
从 Web 服务器端,调用start_response
实际上不应将标头发送到客户端,而是将其延迟到响应正文中至少有一个非空字节串要发送回客户端。 这种架构允许在应用程序执行的最后一刻之前正确报告错误。
start_response
的status
参数
传递给start_response
回调的status
参数必须是一个由 HTTP 状态代码和描述组成的字符串,由一个空格分隔。 有效示例是: '200 OK'
或'404 Not Found'
。
start_response
的headers
参数
传递给start_response
回调的headers
参数必须是一个 Python tuple
list
,每个元组由(header_name, header_value)
组成。 每个标头的名称和值都必须是字符串(无论 Python 版本如何)。 这是一个罕见的类型很重要的例子,因为这确实是 WSGI 规范所要求的。
下面是一个header
参数可能是什么样子的有效示例:
response_body = json.dumps(data).encode('utf-8') headers = [('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))]
HTTP 标头不区分大小写,如果我们正在编写符合 WSGI 的 Web 服务器,则在检查这些标头时需要注意这一点。 此外,应用程序提供的标头列表不应该是详尽的。 服务器有责任确保在将响应发送回客户端之前存在所有必需的 HTTP 标头,并填写应用程序未提供的任何标头。
start_response
的exc_info
参数
start_response
回调应该支持第三个参数exc_info
,用于错误处理。 该参数的正确使用和实现对于生产 Web 服务器和应用程序至关重要,但超出了本文的范围。
可以在 WSGI 规范中获得有关它的更多信息,here。
start_response
返回值—— write
回调
出于向后兼容的目的,实现 WSGI 的 Web 服务器应该返回一个write
callable。 此回调应允许应用程序将主体响应数据直接写回客户端,而不是通过迭代器将其交给服务器。
尽管它存在,但这是一个已弃用的界面,新应用程序应避免使用它。
生成响应体
实现 WSGI 的应用程序应该通过返回一个可迭代对象来生成响应体。 对于大多数应用程序,响应体不是很大,很容易放入服务器的内存中。 在这种情况下,最有效的发送方式是一次发送,使用单元素可迭代。 在特殊情况下,将整个主体加载到内存中是不可行的,应用程序可以通过这个可迭代的接口逐部分返回它。
Python 2 和 Python 3 的 WSGI 之间只有很小的区别:在 Python 3 中,响应体由bytes
对象表示; 在 Python 2 中,正确的类型是str
。
将 UTF-8 字符串转换为bytes
或str
是一项简单的任务:
- 蟒蛇 3.5:
body = 'unicode stuff'.encode('utf-8')
- 蟒蛇 2.7:
body = u'unicode stuff'.encode('utf-8')
如果您想了解更多关于 Python 2 的 unicode 和字节串处理的信息,YouTube 上有一个不错的教程。
如上所述,实现 WSGI 的 Web 服务器还应该支持write
回调以实现向后兼容性。
在没有 Web 服务器的情况下测试您的应用程序
通过了解这个简单的界面,我们可以轻松地创建脚本来测试我们的应用程序,而无需实际启动服务器。
以这个小脚本为例:
from io import BytesIO def get(app, path = '/', query = ''): response_status = [] response_headers = [] def start_response(status, headers): status = status.split(' ', 1) response_status.append((int(status[0]), status[1])) response_headers.append(dict(headers)) environ = { 'HTTP_ACCEPT': '*/*', 'HTTP_HOST': '127.0.0.1:8000', 'HTTP_USER_AGENT': 'TestAgent/1.0', 'PATH_INFO': path, 'QUERY_STRING': query, 'REQUEST_METHOD': 'GET', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'TestServer/1.0', 'wsgi.errors': BytesIO(b''), 'wsgi.input': BytesIO(b''), 'wsgi.multiprocess': False, 'wsgi.multithread': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0), } response_body = app(environ, start_response) merged_body = ''.join((x.decode('utf-8') for x in response_body)) if hasattr(response_body, 'close'): response_body.close() return {'status': response_status[0], 'headers': response_headers[0], 'body': merged_body}
通过这种方式,例如,我们可以将一些测试数据和模拟模块初始化到我们的应用程序中,并进行GET
调用以测试它是否相应地响应。 我们可以看到它不是一个实际的 Web 服务器,而是通过为应用程序提供一个start_response
回调和一个包含我们环境变量的字典,以类似的方式与我们的应用程序交互。 在请求结束时,它使用响应主体迭代器并返回一个包含其所有内容的字符串。 可以为不同类型的 HTTP 请求创建类似的方法(或通用方法)。
包起来
在本文中,我们没有讨论 WSGI 如何处理文件上传,因为这可能被认为是更“高级”的功能,不适合作为介绍性文章。 如果您想了解更多信息,请查看有关文件处理的 PEP-3333 部分。
我希望这篇文章有助于更好地理解 Python 如何与 Web 服务器通信,并允许开发人员以有趣和创造性的方式使用这个接口。
致谢
我要感谢我的编辑 Nick McCrea 帮助我完成这篇文章。 由于他的工作,原文变得更加清晰,并且有几个错误没有得到纠正。