WSGI:Python 的服务器-应用程序接口

已发表: 2022-03-11

1993 年,网络仍处于起步阶段,拥有大约 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 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

虽然我们在这种情况下使用了一个函数,但任何可调用的都可以。 此处应用程序对象的规则是:

  • 必须是带有environstart_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.inputwsgi.errors

大多数environ键都很简单,但其中两个需要进一步说明: wsgi.input ,它必须包含带有来自客户端的请求正文的流,以及wsgi.errors ,应用程序报告它遇到的任何错误。 从应用程序发送到wsgi.errors的错误通常会发送到服务器错误日志。

这两个键必须包含类似文件的对象; 也就是说,提供接口以作为流读取或写入的对象,就像我们在 Python 中打开文件或套接字时获得的对象一样。 起初这可能看起来很棘手,但幸运的是,Python 为我们提供了很好的工具来处理这个问题。

首先,我们在谈论什么样的流? 根据 WSGI 定义, wsgi.inputwsgi.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参数必须是一个可调用的,带有两个必需参数,即statusheaders ,以及一个可选参数exc_info 。 在将正文的任何​​部分发送回 Web 服务器之前,应用程序必须调用它。

在本文开头的第一个应用示例中,我们将响应的主体作为列表返回,因此我们无法控制何时迭代列表。 因此,我们必须在返回列表之前调用start_response

在第二个中,我们在产生响应正文的第一个(并且在本例中是唯一一个)片段之前调用了start_response 。 无论哪种方式在 WSGI 规范中都是有效的。

从 Web 服务器端,调用start_response实际上不应将标头发送到客户端,而是将其延迟到响应正文中至少有一个非空字节串要发送回客户端。 这种架构允许在应用程序执行的最后一刻之前正确报告错误。

start_responsestatus参数

传递给start_response回调的status参数必须是一个由 HTTP 状态代码和描述组成的字符串,由一个空格分隔。 有效示例是: '200 OK''404 Not Found'

start_responseheaders参数

传递给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_responseexc_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 字符串转换为bytesstr是一项简单的任务:

  • 蟒蛇 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 是几乎所有 Python Web 框架的关键部分。

在本文中,我们没有讨论 WSGI 如何处理文件上传,因为这可能被认为是更“高级”的功能,不适合作为介绍性文章。 如果您想了解更多信息,请查看有关文件处理的 PEP-3333 部分。

我希望这篇文章有助于更好地理解 Python 如何与 Web 服务器通信,并允许开发人员以有趣和创造性的方式使用这个接口。

致谢

我要感谢我的编辑 Nick McCrea 帮助我完成这篇文章。 由于他的工作,原文变得更加清晰,并且有几个错误没有得到纠正。