PEP3333(中文版)

PEP 3333 – Python Web 服务器网关接口(v1.0.1)


作者 (Author):
Phillip J. Eby

讨论列表 (Discussions-To):
Web-SIG 邮件列表

状态 (Status):
最终版 (Final)

类型 (Type):
信息类 (Informational)

创建日期 (Created):
2010年9月26日

发布历史 (Post-History):
2010年9月26日, 2010年10月4日

替代 (Replaces):
PEP333

目录 (Table of Contents)


PEP 333 读者前言

这是 PEP 333 的更新版本,进行了轻微修改以提高在 Python 3 下的可用性,并纳入了一些长期存在的 WSGI 协议事实上的修正案。(其代码示例也已移植到 Python 3。)

虽然出于程序性原因 (#id9),这必须是一个独立的 PEP,但并未进行任何会使之前符合标准的 Python 2.x 服务器或应用失效的更改。如果您的 2.x 应用或服务器符合 PEP 333,那么它也符合本 PEP。

然而,在 Python 3 下,您的应用或服务器还必须遵循下文 titled 关于字符串类型的说明Unicode 问题 部分中概述的规则。

有关本文档与 PEP 333 之间详细的逐行差异,您可以查看其 SVN 修订历史 (#id10),从修订版 84854 开始。


摘要

本文档规定了 Web 服务器与 Python Web 应用程序或框架之间拟议的标准接口,旨在促进 Web 应用程序在各种 Web 服务器之间的可移植性。


原始原理和目标 (来自 PEP 333)

Python 目前拥有各种各样的 Web 应用程序框架,例如 Zope, Quixote, Webware, SkunkWeb, PSO 和 Twisted Web —— 仅举几例 (#id6)。对于新的 Python 用户来说,这种广泛的选择可能是一个问题,因为一般来说,他们选择的 Web 框架将限制他们可用的 Web 服务器的选择,反之亦然。

相比之下,虽然 Java 也有同样多的 Web 应用程序框架可用,但 Java 的 "servlet" API 使得用任何 Java Web 应用程序框架编写的应用程序都可以在任何支持 servlet API 的 Web 服务器中运行。

在 Python 的 Web 服务器中提供并使用此类 API(无论这些服务器是用 Python 编写的(如 Medusa),嵌入 Python(如 mod_python),还是通过网关协议调用 Python(如 CGI, FastCGI 等))—— 将使框架的选择与服务器的选择分离开来,让用户自由选择适合他们的组合,同时让框架和服务器开发者专注于他们喜欢的专业领域。

因此,本 PEP 提出了一个简单且通用的 Web 服务器与 Web 应用程序或框架之间的接口:Python Web 服务器网关接口 (WSGI)。

但是,仅仅存在 WSGI 规范并不能解决 Python Web 应用程序现有的服务器和框架状态问题。服务器和框架的作者及维护者必须实际实现 WSGI 才能产生任何效果。

然而,由于现有的服务器或框架都不支持 WSGI,因此对于实现 WSGI 支持的作者来说,几乎没有立即的回报。因此,WSGI 必须易于实现,以便作者在接口上的初始投入可以合理地低。

因此,在接口的服务器端和框架端实现的简单性对于 WSGI 接口的实用性绝对关键,因此是任何设计决策的主要标准。

然而,请注意,框架作者的实现简单性并不等同于 Web 应用程序作者的使用便利性。WSGI 向框架作者呈现了一个绝对“无装饰”的接口,因为像响应对象和 cookie 处理这样的花哨功能只会妨碍现有框架对这些问题的处理。再次强调,WSGI 的目标是促进现有服务器与应用程序或框架之间的轻松互连,而不是创建一个新的 Web 框架。

还要注意,这一目标排除了 WSGI 要求任何尚未在已部署的 Python 版本中可用的内容。因此,本规范不提议也不要求新的标准库模块,WSGI 中的任何内容都不需要大于 2.2.2 的 Python 版本。(不过,未来的 Python 版本在标准库提供的 Web 服务器中包含对此接口的支持将是个好主意。)

除了便于现有和未来框架及服务端的实现外,还应该容易创建请求预处理器、响应后处理器和其他基于 WSGI 的“中间件”组件,这些组件对其包含的服务器而言看起来像一个应用程序,而对其包含的应用程序而言则充当服务器。

如果中间件既简单又健壮,并且 WSGI 在服务器和框架中广泛可用,那么就有可能出现一种全新类型的 Python Web 应用程序框架:由松散耦合的 WSGI 中间件组件组成的框架。确实,现有的框架作者甚至可以选择将其框架的现有服务重构为以这种方式提供,变得更像与 WSGI 一起使用的库,而不那么像单体框架。这将允许应用程序开发者为特定功能选择“同类最佳”组件,而不必承诺接受单一框架的所有优缺点。

当然,截至撰写本文时,那一天无疑还相当遥远。与此同时,WSGI 的一个足够的短期目标是启用任何框架与任何服务器的配合使用。

最后,应该提到的是,当前版本的 WSGI 没有规定任何特定的机制来“部署”应用程序以供 Web 服务器或服务器网关使用。目前,这必然由服务器或网关具体实现定义。在足够多的服务器和框架实现了 WSGI 以提供不同部署需求的实地经验之后,创建另一个 PEP 来描述 WSGI 服务器和应用程序框架的部署标准可能是有意义的。


规范概述

WSGI 接口有两端:“服务器”或“网关”端,以及“应用程序”或“框架”端。服务器端调用由应用程序端提供的可调用对象。该对象如何提供的具体细节由服务器或网关决定。假设某些服务器或网关将要求应用程序的部署者编写一个简短的脚本来创建服务器或网关的实例,并向其提供应用程序对象。其他服务器和网关可能使用配置文件或其他机制来指定应从何处导入或以其他方式获取应用程序对象。

除了“纯”服务器/网关和应用程序/框架外,还可以创建实现本规范两端的“中间件”组件。这些组件对其包含的服务器而言充当应用程序,对其包含的应用程序而言充当服务器,可用于提供扩展 API、内容转换、导航和其他有用功能。

在整个规范中,我们将使用术语“可调用对象 (callable)”来表示“函数、方法、类或具有 __call__ 方法的实例”。由服务器、网关或应用程序实现可调用对象的一方选择适合其需求的适当实现技术。相反,调用可调用对象的服务器、网关或应用程序不得对其提供的可调用对象的类型有任何依赖。可调用对象仅用于被调用,而不用于内省。


关于字符串类型的说明

通常,HTTP 处理的是字节,这意味着本规范主要是关于处理字节。

然而,这些字节的内容通常具有某种文本解释,而在 Python 中,字符串是处理文本最方便的方式。

但在许多 Python 版本和实现中,字符串是 Unicode 而不是字节。这需要在可用的 API 和在 HTTP 上下文中字节与文本之间的正确转换之间取得谨慎的平衡……特别是为了支持在不同 str 类型的 Python 实现之间移植代码。

因此,WSGI 定义了两种“字符串”:

  • “原生”字符串 (Native strings)(始终使用名为 str 的类型实现),用于请求/响应头和元数据。
  • “字节串” (Bytestrings)(在 Python 3 中使用 bytes 类型实现,在其他版本中使用 str),用于请求和响应的主体(例如 POST/PUT 输入数据和 HTML 页面输出)。

但不要混淆:即使 Python 的 str 类型在“底层”实际上是 Unicode,“原生”字符串的内容也必须能够通过 Latin-1 编码转换为字节!(有关更多详细信息,请参阅本文档后面的 Unicode 问题 部分。)

简而言之:当你在本文档中看到单词“string”时,它指的是“原生”字符串,即 str 类型的对象,无论其内部是实现为字节还是 unicode。当你看到对“bytestring”的引用时,应理解为“Python 3 下的 bytes 类型对象,或 Python 2 下的 str 类型”。

因此,尽管 HTTP 在某种意义上“真的只是字节”,但使用 Python 默认的 str 类型可以获得许多 API 便利。


应用/框架端

应用程序对象只是一个接受两个参数的可调用对象。术语“对象”不应被误解为需要一个实际的对象实例:函数、方法、类或具有 __call__ 方法的实例都可用作应用程序对象。应用程序对象必须能够被多次调用,因为几乎所有服务器/网关(CGI 除外)都会发出这样的重复请求。

(注意:虽然我们称其为“应用程序”对象,但这不应被解释为意味着应用程序开发人员将使用 WSGI 作为 Web 编程 API!假设应用程序开发人员将继续使用现有的高级框架服务来开发他们的应用程序。WSGI 是供框架和服务器开发人员使用的工具,并非旨在直接支持应用程序开发人员。)

这里是两个应用程序对象的示例;一个是函数,另一个是类:

HELLO_WORLD = b"Hello world!\n"

def simple_app(environ, start_response):
    """最简单的可能应用程序对象"""
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [HELLO_WORLD]

class AppClass:
    """产生相同的输出,但使用类
    (注意:'AppClass' 是这里的“应用程序”,所以调用它会返回一个 'AppClass' 的实例,
    然后该实例成为规范要求的“应用程序可调用对象”的迭代返回值。
    如果我们想使用 'AppClass' 的*实例*作为应用程序对象,我们将必须实现一个 '__call__' 方法,
    该方法将被调用来执行应用程序,并且我们需要创建一个实例供服务器或网关使用。)
    """
    def __init__(self, environ, start_response):
        self.environ = environ
        self.start = start_response

    def __iter__(self):
        status = '200 OK'
        response_headers = [('Content-type', 'text/plain')]
        self.start(status, response_headers)
        yield HELLO_WORLD

服务器/网关端

服务器或网关针对接收到的每个指向该应用程序的 HTTP 客户端请求,调用一次应用程序可调用对象。为了说明这一点,这里是一个简单的 CGI 网关,实现为一个接受应用程序对象的函数。请注意,这个简单的例子错误处理能力有限,因为默认情况下,未捕获的异常将被转储到 sys.stderr 并由 Web 服务器记录。

import os, sys

enc, esc = sys.getfilesystemencoding(), 'surrogateescape'

def unicode_to_wsgi(u):
    # 将环境变量转换为 WSGI "bytes-as-unicode" 字符串
    return u.encode(enc, esc).decode('iso-8859-1')

def wsgi_to_bytes(s):
    return s.encode('iso-8859-1')

def run_with_cgi(application):
    environ = {k: unicode_to_wsgi(v) for k, v in os.environ.items()}
    environ['wsgi.input'] = sys.stdin.buffer
    environ['wsgi.errors'] = sys.stderr
    environ['wsgi.version'] = (1, 0)
    environ['wsgi.multithread'] = False
    environ['wsgi.multiprocess'] = True
    environ['wsgi.run_once'] = True

    if environ.get('HTTPS', 'off') in ('on', '1'):
        environ['wsgi.url_scheme'] = 'https'
    else:
        environ['wsgi.url_scheme'] = 'http'

    headers_set = []
    headers_sent = []

    def write(data):
        out = sys.stdout.buffer
        if not headers_set:
            raise AssertionError("write() before start_response()")
        elif not headers_sent:
            # 在第一次输出之前,发送存储的头部
            status, response_headers = headers_sent[:] = headers_set
            out.write(wsgi_to_bytes('Status: %s\r\n' % status))
            for header in response_headers:
                out.write(wsgi_to_bytes('%s: %s\r\n' % header))
            out.write(wsgi_to_bytes('\r\n'))
            out.write(data)
            out.flush()

    def start_response(status, response_headers, exc_info=None):
        if exc_info:
            try:
                if headers_sent:
                    # 如果头部已发送,重新抛出原始异常
                    raise exc_info[1].with_traceback(exc_info[2])
            finally:
                exc_info = None  # 避免悬空的循环引用
        elif headers_set:
            raise AssertionError("Headers already set!")
        
        headers_set[:] = [status, response_headers]
        # 注意:头部的错误检查应该在这里发生,*在*头部设置之后。
        # 这样,如果发生错误,start_response 只能在设置了 exc_info 的情况下被重新调用。
        return write

    result = application(environ, start_response)
    try:
        for data in result:
            if data:
                # 直到主体出现前不要发送头部
                write(data)
        if not headers_sent:
            write(b'')  # 如果主体为空,现在发送头部
    finally:
        if hasattr(result, 'close'):
            result.close()

中间件:扮演双重角色的组件

请注意,单个对象可以对某些应用程序扮演服务器的角色,同时对某些服务器扮演应用程序的角色。这样的“中间件”组件可以执行以下功能:

  • 根据目标 URL 将请求路由到不同的应用程序对象,并相应地重写 environ
  • 允许多个应用程序或框架在同一进程中并行运行。
  • 通过网络转发请求和响应,实现负载均衡和远程处理。
  • 执行内容后处理,例如应用 XSL 样式表。

中间件的存在通常对接口的“服务器/网关”端和“应用程序/框架”端都是透明的,不需要特殊支持。想要将中间件 incorporated 到应用程序中的用户,只需将中间件组件提供给服务器,就像它是一个应用程序一样,并配置中间件组件来调用应用程序,就像中间件组件是一个服务器一样。当然,中间件包装的“应用程序”实际上可能是另一个包装了另一个应用程序的中间件组件,以此类推,形成所谓的“中间件栈”。

在大多数情况下,中间件必须符合 WSGI 服务器端和应用程序端的限制和要求。但在某些情况下,中间件的要求比“纯”服务器或应用程序更严格,这些点将在规范中指出。

这里有一个(开玩笑的)中间件组件示例,它使用 Joe Strout 的 piglatin.pytext/plain 响应转换为猪拉丁语。(注意:一个“真正的”中间件组件可能会使用更稳健的方式来检查内容类型,并且还应该检查内容编码。此外,这个简单的例子忽略了单词可能被分割在块边界上的可能性。)

from piglatin import piglatin

class LatinIter:
    """如果可行,将迭代输出转换为猪拉丁语
    注意,“可行性”可能会改变,直到应用程序产生第一个非空字节串,
    所以 'transform_ok' 必须是一个可变真值。
    """
    def __init__(self, result, transform_ok):
        if hasattr(result, 'close'):
            self.close = result.close
        self._next = iter(result).__next__
        self.transform_ok = transform_ok

    def __iter__(self):
        return self

    def __next__(self):
        data = self._next()
        if self.transform_ok:
            return piglatin(data)  # 在 Py3 上调用必须是字节安全的
        else:
            return data

class Latinator:
    # 默认情况下,不转换输出
    transform = False

    def __init__(self, application):
        self.application = application

    def __call__(self, environ, start_response):
        transform_ok = []

        def start_latin(status, response_headers, exc_info=None):
            # 重置 ok 标志,以防这是重复调用
            del transform_ok[:]
            for name, value in response_headers:
                if name.lower() == 'content-type' and value == 'text/plain':
                    transform_ok.append(True)
                    # 如果存在则剥离 content-length,否则它会出错
                    response_headers = [(name, value) for name, value in response_headers if name.lower() != 'content-length']
                    break
            
            write = start_response(status, response_headers, exc_info)
            if transform_ok:
                def write_latin(data):
                    write(piglatin(data))  # 在 Py3 上调用必须是字节安全的
                return write_latin
            else:
                return write

        return LatinIter(self.application(environ, start_latin), transform_ok)

# 在 Latinator 的控制下运行 foo_app,使用来自 foo_app 的示例 CGI 网关
from foo_app import foo_app
run_with_cgi(Latinator(foo_app))

规范细节

应用程序对象必须接受两个位置参数。为了说明起见,我们将它们命名为 environstart_response,但它们不必具有这些名称。服务器或网关必须使用位置参数(而不是关键字参数)调用应用程序对象。(例如,通过调用 result=application(environ, start_response) 如上所示。)

environ 参数是一个字典对象,包含 CGI 风格的环境变量。该对象必须是内置的 Python 字典(不是子类、UserDict 或其他字典模拟),并且应用程序可以根据需要随意修改字典。字典还必须包含某些 WSGI 要求的变量(在稍后的部分描述),也可能包含服务器特定的扩展变量,命名遵循下面将描述的约定。

start_response 参数是一个可调用对象,接受两个必需的位置参数和一个可选参数。为了说明起见,我们将这些参数命名为 statusresponse_headersexc_info,但它们不必具有这些名称,并且应用程序必须使用位置参数调用 start_response 可调用对象(例如 start_response(status, response_headers))。

status 参数是一个形式为 "999 Message here" 的状态字符串,response_headers 是一个 (header_name, header_value) 元组列表,描述 HTTP 响应头。可选的 exc_info 参数在下文 The start_response() CallableError Handling 部分中描述。它仅在应用程序捕获了错误并试图向浏览器显示错误消息时使用。

start_response 可调用对象必须返回一个 write(body_data) 可调用对象,该对象接受一个位置参数:一个要作为 HTTP 响应主体一部分写入的字节串。(注意:write() 可调用对象仅提供用于支持某些现有框架的命令式输出 API;如果可以避免,新的应用程序或框架不应使用它。有关更多详细信息,请参阅 Buffering and Streaming 部分。)

当被服务器调用时,应用程序对象必须返回一个迭代器,产生零个或多个字节串。这可以通过多种方式完成,例如返回一个字节串列表,或者应用程序是一个产生字节串的生成器函数,或者应用程序是一个其实例可迭代的类。无论以何种方式完成,应用程序对象必须始终返回一个产生零个或多个字节串的迭代器。

服务器或网关必须以无缓冲的方式将产生的字节串传输给客户端,在完成每个字节串的传输后再请求下一个。(换句话说,应用程序应该执行自己的缓冲。有关如何处理应用程序输出的更多信息,请参阅下面的 Buffering and Streaming 部分。)

服务器或网关应将产生的字节串视为二进制字节序列:特别是,它应确保不更改行尾。应用程序负责确保要写入的字节串采用适合客户端的格式。(服务器或网关可以应用 HTTP 传输编码,或执行其他转换以实现 HTTP 功能,如字节范围传输。有关更多详细信息,请参阅下面的 Other HTTP Features。)

如果对 len(iterable) 的调用成功,服务器必须能够依赖结果是准确的。也就是说,如果应用程序返回的迭代器提供了有效的 __len__() 方法,它必须返回准确的结果。(有关通常如何使用此信息的详情,请参阅 Handling the Content-Length Header 部分。)

如果应用程序返回的迭代器具有 close() 方法,服务器或网关必须在当前请求完成时调用该方法,无论请求是正常完成,还是由于迭代期间发生应用程序错误或浏览器提前断开而提前终止。(close() 方法要求是为了支持应用程序释放资源。此协议旨在补充 PEP 342 的生成器支持以及其他具有 close() 方法的常见迭代器。)

返回生成器或其他自定义迭代器的应用程序不应假设整个迭代器将被消耗,因为它可能会被服务器提前关闭。

(注意:应用程序必须在迭代器产生第一个主体字节串之前调用 start_response() 可调用对象,以便服务器可以在任何主体内容之前发送头部。然而,此调用可以由迭代器的第一次迭代执行,因此服务器不得假设在开始迭代迭代器之前已经调用了 start_response()。)

最后,除非迭代器是该服务器或网关特定类型的实例(例如 wsgi.file_wrapper 返回的“文件包装器”,见 Optional Platform-Specific File Handling),否则服务器和网关不得直接使用应用程序返回的迭代器的任何其他属性。在一般情况下,只有此处指定的属性或通过例如 PEP 234 迭代 API 访问的属性是可以接受的。

environ 变量

environ 字典必须包含这些 CGI 环境变量,如通用网关接口规范 (#id7) 所定义。除非它们的值为空字符串(在这种情况下可以省略,除非另有说明),否则必须存在以下变量:

  • REQUEST_METHOD: HTTP 请求方法,如 "GET" 或 "POST"。这绝不能是空字符串,因此总是必需的。
  • SCRIPT_NAME: 请求 URL“路径”的初始部分,对应于应用程序对象,以便应用程序知道其虚拟“位置”。如果应用程序对应于服务器的“根”,这可能是空字符串。
  • PATH_INFO: 请求 URL“路径”的其余部分,指定请求目标在应用程序内的虚拟“位置”。如果请求 URL 指向应用程序根且没有尾随斜杠,这可能是空字符串。
  • QUERY_STRING: 请求 URL 中跟随 "?" 的部分(如果有)。可能为空或缺失。
  • CONTENT_TYPE: HTTP 请求中任何 Content-Type 字段的内容。可能为空或缺失。
  • CONTENT_LENGTH: HTTP 请求中任何 Content-Length 字段的内容。可能为空或缺失。
  • SERVER_NAME, SERVER_PORT: 当未设置 HTTP_HOST 时,这些变量可以组合以确定默认值。有关更多详细信息,请参阅下面的 URL Reconstruction 部分。SERVER_NAMESERVER_PORT 是必需的字符串,绝不能为空。
  • SERVER_PROTOCOL: 客户端发送请求所使用的协议版本。通常这将是类似 "HTTP/1.0" 或 "HTTP/1.1" 的内容,应用程序可使用它来确定如何处理任何 HTTP 请求头。(此变量本应称为 REQUEST_PROTOCOL,因为它表示请求中使用的协议,不一定是服务器响应中将使用的协议。然而,为了与 CGI 兼容,我们必须保留现有名称。)
  • HTTP_ 变量: 对应于客户端提供的 HTTP 请求头的变量(即名称以 "HTTP_" 开头的变量)。这些变量的存在与否应与请求中相应 HTTP 头的存在与否相对应。

服务器或网关应尽可能提供其他适用的 CGI 变量。此外,如果使用 SSL,服务器或网关还应提供尽可能多的 Apache SSL 环境变量 (#id8),如 HTTPS=onSSL_PROTOCOL。然而,请注意,使用上述列表之外的任何 CGI 变量的应用程序必然无法移植到不支持相关扩展的 Web 服务器。(例如,不发布文件的 Web 服务器将无法提供有意义的 DOCUMENT_ROOTPATH_TRANSLATED。)

符合 WSGI 的服务器或网关应记录它提供的变量及其定义(如适用)。应用程序应检查其所需的任何变量是否存在,并在变量缺失时有备用计划。

注意:缺失的变量(例如未发生身份验证时的 REMOTE_USER)应从 environ 字典中省略。还要注意,如果存在 CGI 定义的变量,它们必须是原生字符串。任何 CGI 变量的值是 str 以外的任何类型都违反本规范。

除了 CGI 定义的变量外,environ 字典还可能包含任意的操作系统“环境变量”,并且必须包含以下 WSGI 定义的变量:

变量
wsgi.version 元组 (1, 0),代表 WSGI 版本 1.0。
wsgi.url_scheme 一个字符串,代表调用应用程序时 URL 的“scheme”部分。通常,这将具有值 "http""https",视情况而定。
wsgi.input 一个输入流(类文件对象),可从中读取 HTTP 请求主体字节。(服务器或网关可以根据应用程序的请求按需执行读取,或者可以预读客户端的请求主体并将其缓冲在内存或磁盘上,或者根据其偏好使用任何其他技术来提供此类输入流。)
wsgi.errors 一个输出流(类文件对象),可向其中写入错误输出,以便在标准化且可能集中的位置记录程序或其他错误。这应该是一个“文本模式”流;即,应用程序应使用 "\n" 作为行尾,并假设它将由服务器/网关转换为正确的行尾。(在 str 类型为 unicode 的平台上,错误流应该接受并记录任意 unicode 而不引发错误;不过,允许替换流编码中无法渲染的字符。)对于许多服务器,wsgi.errors 将是服务器的主要错误日志。或者,这可能是 sys.stderr 或某种日志文件。服务器的文档应包括如何配置此选项或在哪里找到记录输出的说明。如果需要,服务器或网关可以向不同的应用程序提供不同的错误流。
wsgi.multithread 如果应用程序对象可能在同一进程中被另一个线程同时调用,则此值应评估为 true,否则为 false。
wsgi.multiprocess 如果等效的应用程序对象可能在另一个进程中被同时调用,则此值应评估为 true,否则为 false。
wsgi.run_once 如果服务器或网关期望(但不保证!)应用程序在其包含的进程生命周期中仅被调用这一次,则此值应评估为 true。通常,这只适用于基于 CGI(或类似物)的网关。

最后,environ 字典还可能包含服务器定义的变量。这些变量应仅使用小写字母、数字、点和下划线命名,并应以定义服务器或网关唯一的名称为前缀。例如,mod_python 可能会定义名为 mod_python.some_variable 的变量。

输入和错误流

服务器提供的输入和错误流必须支持以下方法:

方法 注释
read(size) input 1
readline() input 1, 2
readlines(hint) input 1, 3
__iter__() input
flush() errors 4
write(str) errors
writelines(seq) errors

每种方法的语义如 Python 库参考中所记录,除了上表中列出的这些注释:

  1. 服务器不需要读取超过客户端指定的 Content-Length 的数据,如果应用程序尝试读取超过该点,应模拟文件结束条件。应用程序不应尝试读取多于 CONTENT_LENGTH 变量指定的数据。
  2. 服务器应允许在不带参数的情况下调用 read(),并返回客户端输入流的剩余部分。
  3. 服务器应允许从空或耗尽的输入流进行的任何读取尝试返回空字节串。
  4. 服务器应支持 readline() 的可选 "size" 参数,但如在 WSGI 1.0 中一样,允许省略对它的支持。(在 WSGI 1.0 中,不支持 size 参数,理由是它可能难以实现,并且在实践中不常用……但后来 cgi 模块开始使用它,因此实际的服务器不得不开始支持它!)
  5. 注意,readlines()hint 参数对调用者和实现者都是可选的。应用程序可以自由不提供它,服务器或网关可以自由忽略它。
  6. 由于 errors 流可能无法倒回,服务器和网关可以自由地立即转发写入操作,而不进行缓冲。在这种情况下,flush() 方法可能是一个空操作。然而,便携式应用程序不能假设输出是无缓冲的或 flush() 是空操作。如果需要确保输出确实已写入,它们必须调用 flush()。(例如,为了最小化多个进程写入同一错误日志时的数据混合。)

符合本规范的所有服务器必须支持上表中列出的方法。符合本规范的应用程序不得使用 inputerrors 对象的任何其他方法或属性。特别是,应用程序不得尝试关闭这些流,即使它们拥有 close() 方法。

start_response() 可调用对象

传递给应用程序对象的第二个参数是一个形式为 start_response(status, response_headers, exc_info=None) 的可调用对象。(与所有 WSGI 可调用对象一样,参数必须按位置提供,而不是按关键字提供。)start_response 可调用对象用于开始 HTTP 响应,它必须返回一个 write(body_data) 可调用对象(见下面的 Buffering and Streaming 部分)。

status 参数是一个 HTTP“状态”字符串,如 "200 OK""404 Not Found"。也就是说,它是一个由状态码和原因短语组成的字符串,按此顺序排列,由单个空格分隔,周围没有空格或其他字符。(有关更多信息,请参阅 RFC 2616 第 6.1.1 节。)该字符串不得包含控制字符,并且不得以回车、换行或其组合终止。

response_headers 参数是一个 (header_name, header_value) 元组列表。它必须是一个 Python 列表;即 type(response_headers) is ListType,服务器可以以任何方式更改其内容。每个 header_name 必须是有效的 HTTP 头字段名(如 RFC 2616 第 4.2 节所定义),不带尾随冒号或其他标点符号。

每个 header_value 不得包含任何控制字符,包括嵌入的或末尾的回车或换行符。(这些要求是为了最小化服务器、网关和需要检查或修改响应头的中间响应处理器必须执行的任何解析的复杂性。)

通常,服务器或网关负责确保将正确的头发送给客户端:如果应用程序省略了 HTTP(或其他生效的相关规范)要求的头,服务器或网关必须添加它。例如,HTTP Date:Server: 头通常由服务器或网关提供。

(提醒服务器/网关作者:HTTP 头名称不区分大小写,因此在检查应用程序提供的头时务必考虑到这一点!)

应用程序和中间件禁止使用 HTTP/1.1“逐跳 (hop-by-hop)”功能或头,HTTP/1.0 中的任何等效功能,或任何会影响客户端与 Web 服务器连接持久性的头。这些功能是实际 Web 服务器的专属领域,服务器或网关应将应用程序尝试发送它们视为致命错误,并在它们被提供给 start_response() 时引发错误。(有关“逐跳”功能和头的更多具体信息,请参阅下面的 Other HTTP Features 部分。)

服务器应在调用 start_response 时检查头中的错误,以便在应用程序仍在运行时引发错误。

然而,start_response 可调用对象实际上不得传输响应头。相反,它必须存储它们,以便服务器或网关仅在应用程序返回值的第一次迭代产生非空字节串后,或在应用程序第一次调用 write() 可调用对象后才传输它们。换句话说,在实际有主体数据可用之前,或者直到应用程序返回的迭代器耗尽之前,不得发送响应头。(此规则的唯一可能例外是如果响应头明确包含零的 Content-Length。)

延迟发送响应头是为了确保缓冲和异步应用程序可以在最后一刻之前将其原本打算的输出替换为错误输出。例如,如果在应用程序缓冲区内生成主体时发生错误,应用程序可能需要将响应状态从 "200 OK" 更改为 "500 Internal Error"。

如果提供了 exc_info 参数,它必须是一个 Python sys.exc_info() 元组。仅当错误处理程序调用 start_response 时,应用程序才应提供此参数。如果提供了 exc_info 且尚未输出任何 HTTP 头,start_response 应用新提供的 HTTP 响应头替换当前存储的 HTTP 响应头,从而允许应用程序在发生错误时“改变主意”关于输出。

然而,如果提供了 exc_info 且 HTTP 头已经发送,start_response必须引发错误,并且应使用 exc_info 元组重新抛出。即:

raise exc_info[1].with_traceback(exc_info[2])

这将重新抛出应用程序捕获的异常,原则上应中止应用程序。(一旦 HTTP 头已经发送,应用程序尝试向浏览器输出错误是不安全的。)如果应用程序调用 start_response 时带有 exc_info,则不得捕获 start_response 引发的任何异常。相反,它应允许此类异常传播回服务器或网关。有关更多详细信息,请参阅下面的 Error Handling

应用程序可以多次调用 start_response,当且仅当提供了 exc_info 参数。更确切地说,如果在当前应用程序调用中已经调用了 start_response,则在没有 exc_info 参数的情况下调用 start_response 是致命错误。这包括第一次调用 start_response 引发错误的情况。(有关正确逻辑的说明,请参见上面的示例 CGI 网关。)

注意:实现 start_response 的服务器、网关或中间件应确保在函数执行期间之外不持有对 exc_info 参数的引用,以避免通过涉及的跟踪和帧创建循环引用。最简单的方法是类似这样:

def start_response(status, response_headers, exc_info=None):
    if exc_info:
        try:
            # 在这里处理 exc_info
            pass
        finally:
            exc_info = None  # 避免循环引用

上面的示例 CGI 网关提供了此技术的另一个说明。

处理 Content-Length

如果应用程序提供了 Content-Length 头,服务器传输给客户端的字节数不应超过头允许的数量,并且应在发送足够数据后停止迭代响应,或者如果应用程序尝试 write() 超过该点则引发错误。(当然,如果应用程序提供的数据不足以满足其声明的 Content-Length,服务器应关闭连接并记录或以其他方式报告错误。)

如果应用程序不提供 Content-Length 头,服务器或网关可以选择几种处理方法之一。其中最简单的是在响应完成时关闭客户端连接。

然而,在某些情况下,服务器或网关可能能够生成 Content-Length 头,或者至少避免需要关闭客户端连接。如果应用程序不调用 write() 可调用对象,并且返回一个 len() 为 1 的迭代器,那么服务器可以通过获取迭代器产生的第一个字节串的长度来自动确定 Content-Length

并且,如果服务器和客户端都支持 HTTP/1.1 “分块编码 (chunked encoding)”,那么服务器可以使用分块编码为每个 write() 调用或迭代器产生的字节串发送一个块,从而为每个块生成一个 Content-Length 头。这允许服务器保持客户端连接存活,如果它希望这样做的话。请注意,服务器在执行此操作时必须完全遵守 RFC 2616,否则退回到处理缺少 Content-Length 的其他策略之一。

(注意:应用程序和中间件不得对其输出应用任何种类的 Transfer-Encoding,如分块或 gzip;作为“逐跳”操作,这些编码是实际 Web 服务器/网关的领域。有关更多详细信息,请参阅下面的 Other HTTP Features。)

缓冲和流式传输

一般来说,应用程序将通过缓冲其(适度大小的)输出并一次性发送全部来实现最佳吞吐量。这是现有框架(如 Zope)中的常见方法:输出缓冲在 StringIO 或类似对象中,然后与响应头一起一次性传输。

WSGI 中对应的方法是应用程序简单地返回一个单元素迭代器(例如列表),其中包含作为单个字节串的响应主体。这是绝大多数应用程序函数的推荐方法,这些函数渲染文本轻松适应内存的 HTML 页面。

然而,对于大文件,或者对于 HTTP 流式传输的特殊用途(如多部分“服务器推送”),应用程序可能需要以较小的块提供输出(例如避免将整个大文件加载到内存中)。有时也可能是响应的一部分生产耗时,但提前发送其前面的部分会很有用。

在这些情况下,应用程序通常会返回一个迭代器(通常是生成器迭代器),以块为单位产生输出。这些块可以与多部分边界重合(用于“服务器推送”),或者就在耗时任务(如读取磁盘文件的另一块)之前中断。

WSGI 服务器、网关和中间件不得延迟任何块的传输;它们必须要么将块完全传输给客户端,要么保证即使在应用程序产生下一个块时也会继续传输。

服务器/网关或中间件可以通过以下三种方式之一提供此保证:

  1. 在将控制权返回给应用程序之前,将整个块发送到操作系统(并请求刷新任何 O/S 缓冲区),或者
  2. 使用不同的线程以确保在应用程序产生下一个块时块继续传输。
  3. (仅限中间件)将整个块发送给其父网关/服务器。

通过提供此保证,WSGI 允许应用程序确保传输不会在其输出数据的任意点停滞。这对于多部分“服务器推送”流式传输等的正常运行至关重要,其中多部分边界之间的数据应完整传输给客户端。

中间件对块边界的处理

为了更好地支持异步应用程序和服务器,中间件组件不得阻塞迭代以等待来自应用程序迭代器的多个值。如果中间件在产生任何输出之前需要从应用程序累积更多数据,它必须产生一个空字节串。

换句话说,每次底层应用程序产生一个值时,中间件组件必须至少产生一个值。如果中间件不能产生任何其他值,它必须产生一个空字节串。

此要求确保异步应用程序和服务器可以合谋减少同时运行给定数量应用程序实例所需的线程数。

还要注意,此要求意味着中间件必须在其底层应用程序返回迭代器后立即返回一个迭代器。中间件也禁止使用 write() 可调用对象来传输由底层应用程序产生的数据。中间件只能使用其父服务器的 write() 可调用对象来传输底层应用程序使用中间件提供的 write() 可调用对象发送的数据。

write() 可调用对象

某些现有的应用程序框架 API 以不同于 WSGI 的方式支持无缓冲输出。具体来说,它们提供某种形式的“write”函数或方法来写入一个无缓冲的数据块,或者它们提供一个缓冲的“write”函数和一个“flush”机制来刷新缓冲区。

不幸的是,除非使用线程或其他特殊机制,否则无法根据 WSGI 的“迭代器”应用程序返回值来实现此类 API。

因此,为了允许这些框架继续使用命令式 API,WSGI 包含一个特殊的 write() 可调用对象,由 start_response 可调用对象返回。

新的 WSGI 应用程序和框架如果可能应避免使用 write() 可调用对象。write() 可调用对象严格来说是一个支持命令式流式 API 的黑客。一般来说,应用程序应通过其返回的迭代器产生输出,因为这使得 Web 服务器可以在同一 Python 线程中交错执行其他任务,从而可能为整个服务器提供更好的吞吐量。

write() 可调用对象由 start_response() 可调用对象返回,它接受一个参数:一个要作为 HTTP 响应主体一部分写入的字节串,其处理方式完全如同它是由输出迭代器产生的一样。换句话说,在 write() 返回之前,它必须保证传入的字节串要么已完全发送给客户端,要么已缓冲以便在应用程序继续进行时传输。

即使应用程序使用 write() 来产生其响应主体的全部或部分,也必须返回一个迭代器对象。返回的迭代器可以是空的(即不产生任何非空字节串),但如果它确实产生非空字节串,则该输出必须由服务器或网关正常处理(即,必须立即发送或排队)。应用程序不得在其返回的迭代器内调用 write(),因此迭代器产生的任何字节串都在所有传递给 write() 的字节串发送给客户端之后传输。

Unicode 问题

HTTP 不直接支持 Unicode,此接口也不支持。所有编码/解码必须由应用程序处理;传递给服务器或从服务器返回的所有字符串必须是 strbytes 类型,绝不能是 unicode。在需要字符串对象的地方使用 unicode 对象的结果是未定义的。

还要注意,作为状态或响应头传递给 start_response() 的字符串必须遵循 RFC 2616 关于编码的规定。也就是说,它们必须是 ISO-8859-1 字符,或使用 RFC 2047 MIME 编码。

strStringType 类型实际上基于 Unicode 的 Python 平台(例如 Jython, IronPython, Python 3 等)上,本规范中提到的所有“字符串”必须仅包含可在 ISO-8859-1 编码中表示的代码点(\u0000\u00FF,含)。应用程序提供包含任何其他 Unicode 字符或代码点的字符串是致命错误。

同样,服务器和网关不得向应用程序提供包含任何其他 Unicode 字符的字符串。

再次强调,本规范中称为“字符串”的所有对象必须strStringType 类型,并且不得unicodeUnicodeType 类型。并且,即使给定平台允许 str/StringType 对象中每个字符超过 8 位,对于本规范中称为“字符串”的任何值,只能使用低 8 位。

对于本规范中称为“字节串”的值(即从 wsgi.input 读取、传递给 write() 或由应用程序产生的值),在 Python 3 下该值必须是 bytes 类型,在早期版本的 Python 中是 str 类型。

错误处理

一般来说,应用程序应尝试捕获其自身的内部错误,并在浏览器中显示有帮助的消息。(在此上下文中,“有帮助”的含义由应用程序决定。)

然而,要显示此类消息,应用程序不得实际向浏览器发送任何数据,否则它有破坏响应的风险。因此,WSGI 提供了一种机制,要么允许应用程序发送其错误消息,要么自动中止:start_responseexc_info 参数。以下是其用法的示例:

try:
    # 常规应用程序代码在这里
    status = "200 Froody"
    response_headers = [("content-type", "text/plain")]
    start_response(status, response_headers)
    return ["normal body goes here"]
except:
    # XXX 应该在此裸 'except:' 之前的单独处理程序中捕获运行时问题,如 MemoryError, KeyboardInterrupt
    status = "500 Oops"
    response_headers = [("content-type", "text/plain")]
    start_response(status, response_headers, sys.exc_info())
    return ["error body goes here"]

如果发生异常时尚未写入任何输出,对 start_response 的调用将正常返回,应用程序将返回一个错误主体发送给浏览器。然而,如果任何输出已经发送给浏览器,start_response 将重新抛出提供的异常。此异常不应被应用程序捕获,因此应用程序将中止。然后服务器或网关可以捕获此(致命)异常并中止响应。

服务器应捕获并记录任何中止应用程序或其返回值迭代的异常。如果发生应用程序错误时已向浏览器写入部分响应,如果已发送的头指示服务器知道如何干净修改的 text/* 内容类型,服务器或网关可以尝试向输出添加错误消息。

一些中间件可能希望提供额外的异常处理服务,或拦截并替换应用程序错误消息。在这种情况下,中间件可以选择不重新抛出提供给 start_responseexc_info,而是引发中间件特定的异常,或者在存储提供的参数后简单地返回而不引发异常。这将导致应用程序返回其错误主体迭代器(或调用 write()),允许中间件捕获并修改错误输出。只要应用程序作者遵循以下原则,这些技术就会起作用:

  • 在开始错误响应时始终提供 exc_info
  • 当提供 exc_info 时,从不捕获 start_response 引发的错误

HTTP 1.1 Expect/Continue

实现 HTTP 1.1 的服务器和网关必须提供对 HTTP 1.1“expect/continue”机制的透明支持。这可以通过以下几种方式完成:

  • 立即用“100 Continue”响应回复包含 Expect: 100-continue 请求的请求,并正常继续。
  • 正常继续请求,但向应用程序提供一个 wsgi.input 流,该流将在应用程序首次尝试从输入流读取时发送“100 Continue”响应。然后读取请求必须保持阻塞,直到客户端响应。
  • 等到客户端决定服务器不支持 expect/continue,并自行发送请求主体。(这是次优的,不推荐。)

请注意,这些行为限制不适用于 HTTP 1.0 请求,或不是定向到应用程序对象的请求。有关 HTTP 1.1 Expect/Continue 的更多信息,请参阅 RFC 2616 第 8.2.3 和 10.1.1 节。

其他 HTTP 特性

一般来说,服务器和网关应该“装傻”,允许应用程序完全控制其输出。它们只应进行不改变应用程序响应有效语义的更改。应用程序开发人员总是可以添加中间件组件来提供额外功能,因此服务器/网关开发人员在实现时应保守。在某种意义上,服务器应将自己视为 HTTP“网关服务器”,而应用程序是 HTTP“源服务器”。(有关这些术语的定义,请参阅 RFC 2616 第 1.3 节。)

然而,由于 WSGI 服务器和应用程序不通过 HTTP 通信,RFC 2616 所称的“逐跳 (hop-by-hop)”头不适用于 WSGI 内部通信。WSGI 应用程序不得生成任何 “逐跳”头,不得尝试使用需要生成此类头的 HTTP 功能,也不得依赖 environ 字典中任何传入“逐跳”头的内容。WSGI 服务器必须自行处理任何支持的入站“逐跳”头,例如解码任何入站 Transfer-Encoding,包括分块编码(如果适用)。

将这些原则应用于各种 HTTP 功能,很明显服务器可以通过 If-None-MatchIf-Modified-Since 请求头以及 Last-ModifiedETag 响应头来处理缓存验证。然而,这不是必须的,如果应用程序想要支持该功能,它应该执行自己的缓存验证,因为服务器/网关不需要进行此类验证。

同样,服务器可以重新编码或传输编码应用程序的响应,但应用程序应自行使用合适的内容编码,并且不得应用传输编码。如果客户端请求且应用程序本机不支持字节范围,服务器可以传输应用程序响应的字节范围。同样,如果需要,应用程序应自行执行此功能。

请注意,对应用程序的这些限制不一定意味着每个应用程序都必须重新实现每个 HTTP 功能;许多 HTTP 功能可以由中间件组件部分或完全实现,从而解放服务器和应用程序作者,无需一遍又一遍地实现相同的功能。

线程支持

线程支持(或缺乏支持)也取决于服务器。能够并行运行多个请求的服务器,也应提供以单线程方式运行应用程序的选项,以便非线程安全的应用程序或框架仍可与该服务器一起使用。


实现/应用说明

服务器扩展 API

一些服务器作者可能希望公开更高级的 API,供应用程序或框架作者用于特殊目的。例如,基于 mod_python 的网关可能希望将 Apache API 的一部分作为 WSGI 扩展公开。

在最简单的情况下,这只需要定义一个 environ 变量,例如 mod_python.some_api。但在许多情况下,中间件的可能存在会使这变得困难。例如,提供对 environ 变量中找到的相同 HTTP 头的访问权限的 API,如果 environ 已被中间件修改,可能会返回不同的数据。

一般来说,任何复制、取代或绕过部分 WSGI 功能的扩展 API 都有与中间件组件不兼容的风险。服务器/网关开发者不应假设没有人会使用中间件,因为一些框架开发者专门打算将其框架组织或重组为几乎完全作为各种类型的中间件运行。

因此,为了提供最大的兼容性,提供替换某些 WSGI 功能的扩展 API 的服务器和网关,必须设计这些 API,以便使用它们替换的那部分 API 来调用它们。例如,访问 HTTP 请求头的扩展 API 必须要求应用程序传入其当前的 environ,以便服务器/网关可以验证通过 API 访问的 HTTP 头未被中间件修改。如果扩展 API 不能保证它总是与 environ 关于 HTTP 头的内容一致,它必须拒绝为应用程序提供服务,例如通过引发错误、返回 None 而不是头集合,或适合该 API 的任何其他方式。

同样,如果扩展 API 提供了写入响应数据或头的替代方法,它应要求传入 start_response 可调用对象,然后应用程序才能获得扩展服务。如果传入的对象与服务器/网关最初提供给应用程序的对象不相同,它就不能保证正确操作,并且必须拒绝向应用程序提供扩展服务。

这些准则也适用于向 environ 添加信息(如解析的 cookie、表单变量、会话等)的中间件。具体来说,此类中间件应将这些功能提供为在 environ 上操作的函数,而不是简单地将值塞入 environ。这有助于确保信息是在任何中间件完成任何 URL 重写或其他 environ 修改之后environ 计算的。

服务器/网关和中间件开发者都非常重要的是遵循这些“安全扩展”规则,以避免未来中间件开发者被迫从 environ 中删除任何和所有扩展 API 以确保其中介不被使用这些扩展的应用程序绕过的情况!

应用配置

本规范未定义服务器如何选择或获取要调用的应用程序。这些和其他配置选项是高度特定于服务器的问题。预计服务器/网关作者将记录如何配置服务器以执行特定的应用程序对象,以及使用哪些选项(如线程选项)。

另一方面,框架作者应记录如何创建包装其框架功能的应用程序对象。选择了服务器和应用程序框架的用户必须将两者连接起来。然而,由于框架和服务器现在有一个共同的接口,这应该仅仅是一个机械问题,而不是为每个新的服务器/框架对进行重大的工程努力。

最后,一些应用程序、框架和中间件可能希望使用 environ 字典来接收简单的字符串配置选项。服务器和网关应通过允许应用程序的部署者指定要放置在 environ 中的名称 - 值对来支持这一点。在最简单的情况下,此支持可以仅包括将 os.environ 中的所有操作系统提供的环境变量复制到 environ 字典中,因为部署者原则上可以在服务器外部配置这些变量,或者在 CGI 情况下,它们可能能够通过服务器的配置文件进行设置。

应用程序应尝试将此类所需变量保持在最低限度,因为并非所有服务器都支持轻松配置它们。当然,即使在最坏的情况下,部署应用程序的人也可以创建一个脚本来提供必要的配置值:

from the_app import application

def new_app(environ, start_response):
    environ['the_app.configval1'] = 'something'
    return application(environ, start_response)

但是,大多数现有的应用程序和框架可能只需要来自 environ 的单个配置值,以指示其应用程序或框架特定配置文件的位置。(当然,应用程序应缓存此类配置,以避免在每次调用时重新读取它。)

URL 重构

如果应用程序希望重构请求的完整 URL,它可以使用以下算法,由 Ian Bicking 贡献:

from urllib.parse import quote

url = environ['wsgi.url_scheme'] + '://'
if environ.get('HTTP_HOST'):
    url += environ['HTTP_HOST']
else:
    url += environ['SERVER_NAME']
    if environ['wsgi.url_scheme'] == 'https':
        if environ['SERVER_PORT'] != '443':
            url += ':' + environ['SERVER_PORT']
    else:
        if environ['SERVER_PORT'] != '80':
            url += ':' + environ['SERVER_PORT']

url += quote(environ.get('SCRIPT_NAME', ''))
url += quote(environ.get('PATH_INFO', ''))
if environ.get('QUERY_STRING'):
    url += '?' + environ['QUERY_STRING']

请注意,这样重构的 URL 可能与客户端请求的 URI 不完全相同。例如,服务器重写规则可能已修改客户端最初请求的 URL 以将其置于规范形式。

支持较旧 (<2.2) 版本的 Python

一些服务器、网关或应用程序可能希望支持较旧 (<2.2) 版本的 Python。如果 Jython 是目标平台,这一点尤其重要,因为截至撰写本文时,生产就绪版本的 Jython 2.2 尚不可用。

对于服务器和网关,这相对简单:针对 Python 2.2 之前版本的服务器和网关必须仅限制自己使用标准的 "for" 循环来迭代应用程序返回的任何迭代器。这是确保与 2.2 之前的迭代器协议(如下进一步讨论)和“当今”的迭代器协议(见 PEP 234)在源代码级别兼容的唯一方法。

(请注意,此技术必然仅适用于用 Python 编写的服务器、网关或中间件。关于如何从其他语言正确使用迭代器协议的讨论不在本 PEP 范围内。)

对于应用程序,支持 2.2 之前的 Python 版本稍微复杂一些:

  • 您不能返回文件对象并期望它作为迭代器工作,因为在 Python 2.2 之前,文件是不可迭代的。(一般来说,无论如何都不应该这样做,因为它在大多数情况下表现很差!)使用 wsgi.file_wrapper 或特定于应用程序的文件包装器类。(有关 wsgi.file_wrapper 的更多信息以及可用于将文件包装为迭代器的示例类,请参阅 Optional Platform-Specific File Handling。)
  • 如果您返回自定义迭代器,它必须实现 2.2 之前的迭代器协议。也就是说,提供一个接受整数键的 __getitem__ 方法,并在耗尽时引发 IndexError。(请注意,内置序列类型也是可以接受的,因为它们也实现了此协议。)

最后,希望支持 2.2 之前 Python 版本并迭代应用程序返回值或自身返回迭代器(或两者兼有)的中间件,必须遵循上述适当的建议。

(注意:不言而喻,要支持 2.2 之前的 Python 版本,任何服务器、网关、应用程序或中间件还必须仅使用目标版本中可用的语言功能,使用 1 和 0 代替 TrueFalse 等。)

可选的平台特定文件处理

某些操作环境提供特殊的高性能文件传输设施,例如 Unix sendfile() 调用。服务器和网关可以通过 environ 中的可选 wsgi.file_wrapper 键公开此功能。应用程序可以使用此“文件包装器”将文件或类文件对象转换为迭代器,然后返回它,例如:

if 'wsgi.file_wrapper' in environ:
    return environ['wsgi.file_wrapper'](filelike, block_size)
else:
    return iter(lambda: filelike.read(block_size), '')

如果服务器或网关提供 wsgi.file_wrapper,它必须是一个可调用对象,接受一个必需的位置参数和一个可选的位置参数。第一个参数是要发送的类文件对象,第二个参数是可选的块大小“建议”(服务器/网关不必使用)。可调用对象必须返回一个迭代器对象,并且不得在服务器/网关实际从应用程序接收到迭代器作为返回值之前执行任何数据传输。(否则将阻止中间件解释或覆盖响应数据。)

要被视为“类文件”,应用程序提供的对象必须有一个接受可选大小参数的 read() 方法。它可能有一个 close() 方法,如果有,wsgi.file_wrapper 返回的迭代器必须有一个 close() 方法来调用原始类文件对象的 close() 方法。如果“类文件”对象具有与 Python 内置文件对象名称匹配的任何其他方法或属性(例如 fileno()),wsgi.file_wrapper 可以假设这些方法或属性具有与内置文件对象相同的语义。

任何平台特定文件处理的实际实现必须发生在应用程序返回之后,并且服务器或网关检查是否返回了包装器对象。(再次强调,由于中间件、错误处理程序等的存在,不保证创建的任何包装器实际上会被使用。)

除了 close() 的处理外,从应用程序返回文件包装器的语义应与应用程序返回 iter(filelike.read, '') 相同。换句话说,传输应从“文件”内的当前位置开始(在传输开始时),并继续直到到达末尾,或直到写了 Content-Length 字节。(如果应用程序不提供 Content-Length,服务器可以利用其对底层文件实现的了解从文件生成一个。)

当然,平台特定的文件传输 API 通常不接受任意的“类文件”对象。因此,wsgi.file_wrapper 必须内省提供的对象以查找诸如 fileno()(类 Unix 操作系统)或 java.nio.FileChannel(在 Jython 下)等内容,以确定类文件对象是否适合与其支持的平台特定 API 一起使用。

请注意,即使对象不适合平台 API,wsgi.file_wrapper仍然必须返回一个包装 read()close() 的迭代器,以便使用文件包装器的应用程序可以在平台间移植。这是一个简单的平台无关文件包装器类,适用于旧(2.2 之前)和新 Python:

class FileWrapper:
    def __init__(self, filelike, blksize=8192):
        self.filelike = filelike
        self.blksize = blksize
        if hasattr(filelike, 'close'):
            self.close = filelike.close

    def __getitem__(self, key):
        data = self.filelike.read(self.blksize)
        if data:
            return data
        raise IndexError

这是一个服务器/网关使用它来提供对平台特定 API 访问的代码片段:

environ['wsgi.file_wrapper'] = FileWrapper
result = application(environ, start_response)
try:
    if isinstance(result, FileWrapper):
        # 检查 result.filelike 是否可与平台特定 API 一起使用,
        # 如果是,则使用该 API 传输结果。
        # 如果不是,则回退到下面的正常迭代器处理循环。
        pass 
    for data in result:
        # 等等
finally:
    if hasattr(result, 'close'):
        result.close()

问答

为什么 environ 必须是字典?使用子类有什么问题?
要求使用字典的理由是为了最大化服务器之间的可移植性。替代方案是将字典方法的某个子集定义为标准和便携式接口。然而,在实践中,大多数服务器可能会发现字典足以满足他们的需求,因此框架作者将期望全套字典功能可用,因为它们大多数时候都在那里。但是,如果某些服务器选择不使用字典,那么尽管该服务器“符合”规范,也会出现互操作性问题。因此,强制使用字典简化了规范并保证了互操作性。

请注意,这并不妨碍服务器或框架开发者在 environ 字典内作为自定义变量提供专业服务。这是提供任何此类增值服务的推荐方法。

为什么你可以调用 write() 并产生字节串/返回迭代器?我们不应该只选一种方式吗?
如果我们只支持迭代方法,那么假设“推送”可用的当前框架将受苦。但是,如果我们只支持通过 write() 进行推送,那么服务器在传输大文件等时的性能将受到影响(如果工作线程在所有输出发送完毕之前无法开始处理新请求)。因此,这种妥协允许应用程序框架根据需要支持这两种方法,但对服务器实现者的负担仅比仅推送方法多一点。

close() 是做什么的?
当在应用程序对象执行期间完成写入时,应用程序可以使用 try/finally 块确保释放资源。但是,如果应用程序返回迭代器,任何使用的资源直到迭代器被垃圾回收之前都不会释放。close() 习惯用法允许应用程序在请求结束时释放关键资源,并且它与 PEP 325 提议的生成器中 try/finally 支持向前兼容。

为什么这个接口这么低级?我想要功能 X!(例如 cookies, sessions, persistence, …)
这不是又一个 Python Web 框架。它只是框架与 Web 服务器交谈以及 vice versa 的一种方式。如果你想要这些功能,你需要选择一个提供你想要功能的 Web 框架。如果该框架允许你创建 WSGI 应用程序,你应该能够在大多数支持 WSGI 的服务器中运行它。此外,一些 WSGI 服务器可能通过其 environ 字典中提供的对象提供额外服务;有关详细信息,请参阅适用的服务器文档。(当然,使用此类扩展的应用程序将无法移植到其他基于 WSGI 的服务器。)

为什么使用 CGI 变量而不是老式的 HTTP 头?为什么把它们和 WSGI 定义的变量混在一起?
许多现有的 Web 框架 heavily 建立在 CGI 规范之上,现有的 Web 服务器知道如何生成 CGI 变量。相比之下,表示入站 HTTP 信息的替代方式是碎片化的且缺乏市场份额。因此,使用 CGI“标准”似乎是利用现有实现的好方法。至于将它们与 WSGI 变量混合,将它们分开只需要传递两个字典参数,而没有提供真正的好处。

状态字符串呢?我们不能只用数字吗,传入 200 而不是 "200 OK"
这样做会使服务器或网关复杂化,因为它们必须有一个数字状态表和相应的消息。相比之下,应用程序或框架作者很容易输入与他们使用的特定响应代码配套的额外文本,并且现有框架通常已经包含所需消息的表。所以,总的来说,让应用程序/框架负责似乎比服务器或网关负责更好。

为什么 wsgi.run_once 不保证只运行一次应用?
因为它仅仅是向应用程序建议它应该“为不频繁运行做好准备”。这是针对具有多种操作模式(用于缓存、会话等)的应用程序框架的。在“多次运行”模式下,此类框架可以预加载缓存,并且可能不会在每次请求后将日志或会话数据写入磁盘。在“单次运行”模式下,此类框架避免预加载并在每次请求后刷新所有必要的写入。然而,为了测试应用程序或框架以验证在后一种模式下的正确操作,可能有必要(或至少权宜之计)调用它多次。因此,应用程序不应仅仅因为调用时 wsgi.run_once 设置为 True 就假设它肯定不会再次运行。

功能 X(字典、可调用对象等)在应用程序代码中使用很丑;为什么我们不使用对象 instead?
WSGI 的所有这些实现选择 specifically 旨在将功能彼此解耦;将这些功能重新组合成封装的对象使得编写服务器或网关有点困难,而编写仅替换或修改整体功能一小部分的中间件则困难一个数量级。

本质上,中间件想要拥有一个“责任链”模式, whereby 它可以充当某些功能的“处理程序”,同时允许其他功能保持不变。如果接口要保持可扩展性,这在普通 Python 对象中很难做到。例如,必须使用 __getattr____getattribute__ 覆盖,以确保扩展(如未来 WSGI 版本定义的属性)被传递下去。这类代码 notoriously 难以 100% 正确,很少有人愿意自己编写。他们将因此复制其他人的实现,但在他们复制的人纠正另一个角落案例时未能更新它们。

此外,这种必要的样板代码纯粹是多余的,是中间件开发者为了支持应用程序框架开发者稍微漂亮一点的 API 而支付的开发者税。但是,应用程序框架开发者通常只会更新一个框架以支持 WSGI,并且在其整个框架中非常有限的部分。这很可能是他们的第一次(也许是唯一一次)WSGI 实现,因此他们很可能会手边拿着本规范进行实现。因此,用对象属性等使 API“更漂亮”的努力对于这个受众来说很可能是浪费的。

我们鼓励那些希望在直接 Web 应用程序编程中(相对于 Web 框架开发)拥有更漂亮(或以其他方式改进)WSGI 接口的人开发 API 或框架,以包装 WSGI 供应用程序开发者方便使用。通过这种方式,WSGI 可以保持对服务器和中间件作者方便的低级,而对应用程序开发者来说并不“丑陋”。


提议/讨论中

这些项目目前正在 Web-SIG 和其他地方讨论,或在 PEP 作者的“待办事项”列表中:

  • wsgi.input 应该是迭代器而不是文件吗?这将有助于异步应用程序和分块编码输入流。
  • 正在讨论可选扩展,用于暂停应用程序输出的迭代,直到输入可用或发生回调。
  • 添加一节关于同步与异步应用程序和服务器、相关线程模型以及这些领域的议题/设计目标。

致谢

感谢 Web-SIG 邮件列表上的许多人的深思熟虑的反馈,使这份修订草案成为可能。特别是:

  • Gregory "Grisha" Trubetskoy,mod_python 的作者,他抨击初稿没有提供优于“普通旧 CGI”的任何优势,从而鼓励我寻找更好的方法。
  • Ian Bicking,他帮助督促我正确指定多线程和多进程选项,并纠缠我提供一种机制让服务器向应用程序提供自定义扩展数据。
  • Tony Lownds,他想出了 start_response 函数接受状态和头并返回 write 函数的概念。他的输入也指导了异常处理设施的设计,特别是在允许覆盖应用程序错误消息的中间件方面。
  • Alan Kennedy,他勇敢地尝试在 WSGI 规范最终确定之前实现 WSGI-on-Jython,这帮助塑造了“支持较旧版本的 Python”部分,以及可选的 wsgi.file_wrapper 设施,以及一些早期的字节/unicode 决策。
  • Mark Nottingham,他广泛审查了规范中关于 HTTP RFC 合规性的问题,特别是关于 HTTP/1.1 功能的问题,直到他指出我才知道它们存在。
  • Graham Dumpleton,他不知疲倦地工作(即使面对我的懒惰和愚蠢)以推出某种 Python 3 版本的 WSGI,他提出了“原生字符串”与“字节串”的概念,并深思熟虑地解决了大量的 HTTP、wsgi.input 和其他修正案。如果不是全部,这个新 PEP 的大部分功劳归于他。

参考文献


版权

本文档已放入公共领域。

来源:
https://github.com/python/peps/blob/main/peps/pep-3333.rst
https://peps.python.org/pep-3333/

最后修改: 2025-02-01 08:55:40 GMT

posted @ 2026-03-18 22:31  Ching_Fire  阅读(7)  评论(0)    收藏  举报