Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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

作者:
Phillip J. Eby <pje at telecommunity.com>
讨论至:
Web-SIG 列表
状态:
最终版
类型:
信息性
创建日期:
2010 年 9 月 26 日
发布历史:
2010 年 9 月 26 日,2010 年 10 月 4 日
取代:
333

目录

PEP 333 读者前言

这是 PEP 333 的更新版本,经过少量修改以改善在 Python 3 下的可用性,并整合了对 WSGI 协议的几个长期事实修正。(其代码示例也已移植到 Python 3。)

虽然由于程序原因 [6],这必须是一个不同的 PEP,但所做的修改并未使先前符合 PEP 333 的 Python 2.x 服务器或应用程序失效。如果您的 2.x 应用程序或服务器符合 PEP 333,那么它也符合此 PEP。

然而,在 Python 3 下,您的应用程序或服务器还必须遵循下面题为关于字符串类型的一点说明Unicode 问题的章节中概述的规则。

有关本文档和 PEP 333 之间的详细逐行差异,您可以查看其 SVN 修订历史记录 [7],从修订版 84854 开始。

摘要

本文档规定了一个提议的标准接口,用于 Web 服务器和 Python Web 应用程序或框架之间,以促进 Web 应用程序在各种 Web 服务器上的可移植性。

最初的理由和目标(摘自 PEP 333)

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

相比之下,尽管 Java 也提供了同样多的 Web 应用程序框架,但 Java 的“servlet”API 使得用任何 Java Web 应用程序框架编写的应用程序都可以在任何支持 servlet API 的 Web 服务器中运行。

在 Python 的 Web 服务器中,无论这些服务器是用 Python 编写的(例如 Medusa)、嵌入 Python 的(例如 mod_python),还是通过网关协议(例如 CGI、FastCGI 等)调用 Python 的,如果存在并广泛使用这样的 API,将把框架的选择与 Web 服务器的选择分离,使用户可以自由选择适合他们的组合,同时让框架和服务器开发人员能够专注于他们首选的专业领域。

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

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

然而,由于没有现有服务器或框架支持 WSGI,因此实现 WSGI 支持的作者几乎没有直接的好处。因此,WSGI 必须易于实现,以便作者在接口上的初始投资可以合理地低。

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

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

另请注意,此目标排除了 WSGI 要求任何在已部署的 Python 版本中尚未可用的东西。因此,本规范未提议或要求新的标准库模块,WSGI 中的任何内容都不要求 Python 版本高于 2.2.2。(然而,对于 Python 的未来版本来说,在标准库提供的 Web 服务器中包含对该接口的支持会是一个好主意。)

除了易于为现有和未来的框架和服务器实现之外,还应该易于创建请求预处理器、响应后处理器和其他基于 WSGI 的“中间件”组件,这些组件对其包含的服务器来说就像一个应用程序,同时对其包含的应用程序来说又像一个服务器。

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

当然,截至本文撰写之时,那一天无疑还很遥远。在此期间,WSGI 的一个足够短期的目标是实现任何框架与任何服务器的使用。

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

规范概述

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

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

在本规范中,我们使用术语“可调用对象”来表示“函数、方法、类,或具有 __call__ 方法的实例”。由实现可调用对象的服务器、网关或应用程序来选择适合其需求的实现技术。反之,调用可调用对象的服务器、网关或应用程序**不得**依赖于所提供的可调用对象的类型。可调用对象只能被调用,不能被自省。

关于字符串类型的一点说明

通常,HTTP 处理字节,这意味着本规范主要涉及字节处理。

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

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

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

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

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

简而言之:本文档中您看到“字符串”一词的地方,它指的是“原生”字符串,即 str 类型的对象,无论其内部实现是字节还是 Unicode。您看到“字节字符串”的引用时,应将其理解为“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):
    """Simplest possible application object"""
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [HELLO_WORLD]

class AppClass:
    """Produce the same output, but using a class

    (Note: 'AppClass' is the "application" here, so calling it
    returns an instance of 'AppClass', which is then the iterable
    return value of the "application callable" as required by
    the spec.

    If we wanted to use *instances* of 'AppClass' as application
    objects instead, we would have to implement a '__call__'
    method, which would be invoked to execute the application,
    and we would need to create an instance for use by the
    server or gateway.
    """

    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):
    # Convert an environment variable to a WSGI "bytes-as-unicode" string
    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:
             # Before the first output, send the stored headers
             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:
                    # Re-raise original exception if headers sent
                    raise exc_info[1].with_traceback(exc_info[2])
            finally:
                exc_info = None     # avoid dangling circular ref
        elif headers_set:
            raise AssertionError("Headers already set!")

        headers_set[:] = [status, response_headers]

        # Note: error checking on the headers should happen here,
        # *after* the headers are set.  That way, if an error
        # occurs, start_response can only be re-called with
        # exc_info set.

        return write

    result = application(environ, start_response)
    try:
        for data in result:
            if data:    # don't send headers until body appears
                write(data)
        if not headers_sent:
            write(b'')   # send headers now if body was empty
    finally:
        if hasattr(result, 'close'):
            result.close()

中间件:两端兼顾的组件

请注意,一个对象可以在相对于某些应用程序时扮演服务器的角色,同时也可以在相对于某些服务器时扮演应用程序的角色。此类“中间件”组件可以执行以下功能:

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

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

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

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

from piglatin import piglatin

class LatinIter:

    """Transform iterated output to piglatin, if it's okay to do so

    Note that the "okayness" can change until the application yields
    its first non-empty bytestring, so 'transform_ok' has to be a mutable
    truth value.
    """

    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)   # call must be byte-safe on Py3
        else:
            return data

class Latinator:

    # by default, don't transform output
    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):

            # Reset ok flag, in case this is a repeat call
            del transform_ok[:]

            for name, value in response_headers:
                if name.lower() == 'content-type' and value == 'text/plain':
                    transform_ok.append(True)
                    # Strip content-length if present, else it'll be wrong
                    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))   # call must be byte-safe on Py3
                return write_latin
            else:
                return write

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


# Run foo_app under a Latinator's control, using the example CGI gateway
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 是描述 HTTP 响应头的 (header_name, header_value) 元组列表。可选的 exc_info 参数在下面关于start_response() 可调用对象错误处理的章节中描述。它仅在应用程序捕获到错误并尝试向浏览器显示错误消息时使用。

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

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

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

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

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

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

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

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

最后,服务器和网关**不得**直接使用应用程序返回的任何其他可迭代对象的属性,除非它是特定于该服务器或网关的类型实例,例如由 wsgi.file_wrapper 返回的“文件包装器”(参见可选的平台特定文件处理)。在一般情况下,只有此处指定的属性,或通过例如 PEP 234 迭代 API 访问的属性是可接受的。

environ 变量

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

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_NAMESERVER_PORT
HTTP_HOST 未设置时,这些变量可以组合起来确定默认值。有关详细信息,请参阅下面的URL 重构部分。SERVER_NAMESERVER_PORT 是必需的字符串,绝不能为空。
SERVER_PROTOCOL
客户端用于发送请求的协议版本。通常,这将是类似 "HTTP/1.0""HTTP/1.1" 的内容,应用程序可以使用它来确定如何处理任何 HTTP 请求头。(这个变量可能应该叫做 REQUEST_PROTOCOL,因为它表示请求中使用的协议,而不一定是服务器响应中使用的协议。然而,为了与 CGI 兼容,我们必须保留现有名称。)
HTTP_ 变量
与客户端提供的 HTTP 请求头对应的变量(即名称以 "HTTP_" 开头的变量)。这些变量的存在与否应与请求中相应 HTTP 头的存在与否相对应。

服务器或网关**应该**尝试提供尽可能多的其他适用 CGI 变量。此外,如果使用 SSL,服务器或网关**应该**提供尽可能多的 Apache SSL 环境变量 [5],例如 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 的“方案”部分。通常,其值将是 "http""https",视情况而定。
wsgi.input 一个输入流(类文件对象),HTTP 请求正文的字节可以从中读取。(服务器或网关可以根据应用程序的请求按需执行读取,也可以预读客户端的请求正文并将其缓冲在内存中或磁盘上,或者根据其偏好使用任何其他技术来提供此类输入流。)
wsgi.errors 一个输出流(类文件对象),错误输出可以写入其中,以便在标准化且可能集中的位置记录程序或其他错误。这应该是一个“文本模式”流;即,应用程序应该使用 "\n" 作为行结束符,并假定它将由服务器/网关转换为正确的行结束符。

(在 str 类型为 Unicode 的平台上,错误流**应该**接受并记录任意 Unicode 而不会引发错误;但是,允许替换无法以流的编码呈现的字符。)

对于许多服务器,wsgi.errors 将是服务器的主要错误日志。或者,这可能是 sys.stderr,或某种日志文件。服务器的文档应包含如何配置此项或在哪里找到记录的输出的说明。如果需要,服务器或网关可以向不同的应用程序提供不同的错误流。

wsgi.multithread 如果应用程序对象可能由同一进程中的另一个线程同时调用,则此值应评估为真;否则应评估为假。
wsgi.multiprocess 如果等效的应用程序对象可能由另一个进程同时调用,则此值应评估为真;否则应评估为假。
wsgi.run_once 如果服务器或网关期望(但不保证!)应用程序在其包含进程的生命周期中只被调用一次,则此值应评估为真。通常,这仅适用于基于 CGI(或类似)的网关。

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

输入和错误流

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

方法 备注
read(size) 输入 1
readline() 输入 1, 2
readlines(hint) 输入 1, 3
__iter__() 输入
flush() 错误 4
write(str) 错误
writelines(seq) 错误

除了上表列出的这些注意事项外,每个方法的语义都按照 Python 库参考中的说明:

  1. 服务器不需要读取超出客户端指定的 Content-Length 的内容,如果应用程序尝试读取超出该点,则**应该**模拟文件结束条件。应用程序**不应该**尝试读取超出 CONTENT_LENGTH 变量指定的数据。

    服务器**应该**允许在没有参数的情况下调用 read(),并返回客户端输入流的其余部分。

    服务器**应该**在尝试从空或已耗尽的输入流中读取时返回空字节字符串。

  2. 服务器**应该**支持 readline() 的可选“size”参数,但与 WSGI 1.0 中一样,它们可以省略对它的支持。

    (在 WSGI 1.0 中,不支持 size 参数,理由是实现起来可能很复杂,并且在实践中不常使用……但后来 cgi 模块开始使用它,因此实际的服务器不得不开始支持它!)

  3. 请注意,readlines()hint 参数对于调用者和实现者都是可选的。应用程序可以自由地不提供它,服务器或网关可以自由地忽略它。
  4. 由于 errors 流不能倒回,服务器和网关可以自由地立即转发写入操作,而无需缓冲。在这种情况下,flush() 方法可能是空操作。但是,可移植应用程序不能假设输出是无缓冲的,或者 flush() 是空操作。如果它们需要确保输出确实已被写入,则必须调用 flush()。(例如,为了最大限度地减少多个进程写入同一错误日志时数据的混杂。)

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

start_response() 可调用对象

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

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“逐跳”功能或标头,HTTP/1.0 中的任何等效功能,或任何会影响客户端与 Web 服务器连接持久性的标头。这些功能是实际 Web 服务器的专属领域,服务器或网关**应**认为应用程序尝试发送它们是致命错误,如果它们提供给 start_response(),则应引发错误。(有关“逐跳”功能和标头的更多详细信息,请参阅下面的其他 HTTP 功能部分。)

服务器**应该**在调用 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 响应头部,从而允许应用程序在发生错误时“改变主意”关于输出。

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

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

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

应用程序**可以**多次调用 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:
             # do stuff w/exc_info here
         finally:
             exc_info = None    # Avoid circular ref.

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

处理 Content-Length 标头

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

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

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

而且,如果服务器和客户端都支持 HTTP/1.1 “分块编码”,那么服务器**可以**使用分块编码为每次 write() 调用或可迭代对象产生的每个字节字符串发送一个块,从而为每个块生成一个 Content-Length 标头。这允许服务器在需要时保持客户端连接活动。请注意,服务器**必须**完全遵守 RFC 2616,否则应退回到其他策略来处理 Content-Length 的缺失。

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

缓冲与流式传输

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

WSGI 中相应的方法是应用程序简单地返回一个包含响应正文作为单个字节字符串的单元素可迭代对象(例如列表)。这是绝大多数应用程序函数的推荐方法,它们渲染的 HTML 页面文本可以轻松地容纳在内存中。

然而,对于大型文件或 HTTP 流的特殊用途(例如多部分“服务器推送”),应用程序可能需要以较小的块提供输出(例如,避免将大型文件加载到内存中)。有时还会出现响应的某些部分可能需要花费时间生成,但提前发送其前面的部分会很有用。

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

WSGI 服务器、网关和中间件**不得**延迟任何块的传输;它们**必须**要么将块完全传输到客户端,要么保证即使应用程序正在生成下一个块,它们也会继续传输。服务器/网关或中间件可以通过以下三种方式之一提供此保证:

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

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

中间件处理块边界

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

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

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

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

write() 可调用对象

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

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

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

如果可能避免使用 write() 可调用对象,新的 WSGI 应用程序和框架**不应**使用它。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 编码。

在 Python 平台上,如果 strStringType 类型实际上是基于 Unicode 的(例如 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:
    # regular application code here
    status = "200 Froody"
    response_headers = [("content-type", "text/plain")]
    start_response(status, response_headers)
    return ["normal body goes here"]
except:
    # XXX should trap runtime issues like MemoryError, KeyboardInterrupt
    #     in a separate handler before this bare 'except:'...
    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()),从而允许中间件捕获和修改错误输出。只要应用程序作者遵守以下规则,这些技术将奏效:

  1. 在开始错误响应时始终提供 exc_info
  2. 在提供 exc_info 时,绝不捕获由 start_response 引发的错误

HTTP 1.1 Expect/Continue

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

  1. 对于包含 Expect: 100-continue 请求的请求,立即以“100 Continue”响应,并正常进行。
  2. 正常处理请求,但向应用程序提供一个 wsgi.input 流,当应用程序首次尝试从输入流读取时,该流将发送“100 Continue”响应。然后,读取请求必须保持阻塞,直到客户端响应。
  3. 等待客户端决定服务器不支持 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 中所谓的“逐跳”标头不适用于 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 可调用对象。如果传入的对象与服务器/网关最初提供给应用程序的对象不同,它不能保证正确操作,并且必须拒绝向应用程序提供扩展服务。

这些指导方针也适用于将解析的 cookie、表单变量、会话等信息添加到 environ 的中间件。具体来说,此类中间件应将这些功能作为操作 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 的生产就绪版本尚未发布。

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

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

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

  • 您不能返回文件对象并期望它作为可迭代对象工作,因为在 Python 2.2 之前,文件是不可迭代的。(通常,您无论如何都不应该这样做,因为它在大多数情况下性能会非常差!)请使用 wsgi.file_wrapper 或应用程序特定的文件包装器类。(有关 wsgi.file_wrapper 的更多信息,以及可用于将文件包装为可迭代对象的示例类,请参见可选的特定平台文件处理。)
  • 如果您返回自定义可迭代对象,它**必须**实现 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,它必须是一个可调用对象,接受一个必需的位置参数和一个可选的位置参数。第一个参数是要发送的类文件对象,第二个参数是可选的块大小“建议”(服务器/网关无需使用)。该可调用对象**必须**返回一个可迭代对象,并且**不得**执行任何数据传输,除非服务器/网关实际接收到该可迭代对象作为应用程序的返回值。(否则将阻止中间件解释或覆盖响应数据。)

为了被认为是“类文件对象”,应用程序提供的对象必须有一个接受可选 size 参数的 read() 方法。它**可以**有一个 close() 方法,如果它有,则由 wsgi.file_wrapper 返回的可迭代对象**必须**有一个调用原始类文件对象的 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):
        # check if result.filelike is usable w/platform-specific
        # API, and if so, use that API to transmit the result.
        # If not, fall through to normal iterable handling
        # loop below.

    for data in result:
        # etc.

finally:
    if hasattr(result, 'close'):
        result.close()

问答

  1. 为什么 environ 必须是字典?使用子类有什么问题?

    要求字典的理由是为了最大限度地提高服务器之间的可移植性。另一种方法是定义字典方法的某个子集作为标准和可移植接口。然而,在实践中,大多数服务器可能会发现字典足以满足其需求,因此框架作者会期望完整的一套字典功能可用,因为它们更常出现。但是,如果某些服务器选择*不*使用字典,那么尽管该服务器“符合”规范,但仍将存在互操作性问题。因此,强制使用字典简化了规范并保证了互操作性。

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

  2. 为什么可以调用 write() 生成字节字符串/返回一个可迭代对象?我们不应该只选择一种方式吗?

    如果我们只支持迭代方法,那么当前假设“推送”可用的框架将受到影响。但是,如果我们只支持通过 write() 进行推送,那么服务器性能会因传输例如大文件而受到影响(如果工作线程在所有输出发送完毕之前不能开始处理新的请求)。因此,这种折衷方案允许应用程序框架根据需要支持两种方法,但与仅推送方法相比,服务器实现者只需承担一点点额外的负担。

  3. close() 是做什么用的?

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

  4. 为什么这个接口如此底层?我想要功能 X!(例如 cookies、会话、持久化等)

    这并不是又一个 Python Web 框架。它只是框架与 Web 服务器之间相互通信的一种方式。如果您需要这些功能,您需要选择一个提供您所需功能的 Web 框架。如果该框架允许您创建 WSGI 应用程序,您应该能够在大多数支持 WSGI 的服务器中运行它。此外,一些 WSGI 服务器可能会通过其 environ 字典中提供的对象提供附加服务;详情请参阅适用的服务器文档。(当然,使用此类扩展的应用程序将无法移植到其他基于 WSGI 的服务器。)

  5. 为什么使用 CGI 变量而不是传统的 HTTP 头?为什么要把它们和 WSGI 定义的变量混在一起?

    许多现有的 Web 框架大量建立在 CGI 规范之上,并且现有的 Web 服务器知道如何生成 CGI 变量。相比之下,表示入站 HTTP 信息的替代方法是碎片化的,并且缺乏市场份额。因此,使用 CGI“标准”似乎是利用现有实现的好方法。至于将它们与 WSGI 变量混合,分离它们只会需要传递两个字典参数,而没有提供任何实际好处。

  6. 状态字符串呢?我们能不能只使用数字,传入 200 而不是 "200 OK"

    这样做会使服务器或网关变得复杂,因为它需要有一个数字状态及其对应消息的表格。相比之下,应用程序或框架作者很容易为他们使用的特定响应代码键入额外的文本,并且现有框架通常已经有一个包含所需消息的表格。因此,总的来说,让应用程序/框架负责似乎更好,而不是服务器或网关。

  7. 为什么 wsgi.run_once 不保证只运行应用程序一次?

    因为它仅仅是向应用程序发出的一个建议,即它应该“为不频繁运行做好准备”。这适用于具有多种操作模式(用于缓存、会话等)的应用程序框架。在“多次运行”模式下,此类框架可能会预加载缓存,并且可能不会在每次请求后将例如日志或会话数据写入磁盘。在“单次运行”模式下,此类框架会避免预加载并在每次请求后刷新所有必要的写入。

    然而,为了测试应用程序或框架以验证在后一种模式下的正确操作,可能需要(或者至少是权宜之计)多次调用它。因此,应用程序不应仅仅因为以 wsgi.run_once 设置为 True 来调用它,就认为它肯定不会再次运行。

  8. 功能 X(字典、可调用对象等)在应用程序代码中使用起来很麻烦;为什么我们不用对象代替呢?

    WSGI 的所有这些实现选择都旨在 解耦 各个功能;将这些功能重新组合成封装对象会使编写服务器或网关变得有些困难,并且编写只替换或修改整体功能一小部分的中间件的难度会增加一个数量级。

    本质上,中间件希望采用“责任链”模式,它可以充当某些函数的“处理器”,同时允许其他函数保持不变。如果接口要保持可扩展性,用普通的 Python 对象很难做到这一点。例如,必须使用 __getattr____getattribute__ 重写,以确保扩展(例如未来 WSGI 版本定义的属性)能够通过。

    这类代码以难以 100% 正确著称,很少有人会愿意自己编写。因此,他们会复制别人的实现,但当他们复制的人纠正了另一个边缘情况时,却没有及时更新。

    此外,这种必要的样板代码将是纯粹的额外负担,是中间件开发人员为支持应用程序框架开发人员稍微美观一些的 API 而支付的“开发税”。但是,应用程序框架开发人员通常只会更新 一个 框架来支持 WSGI,并且只会在整个框架的非常有限的部分进行。这很可能是他们第一个(也可能是唯一一个)WSGI 实现,因此他们很可能会随时参照此规范进行实现。因此,为使 API 通过对象属性等方式“更漂亮”而付出的努力,对这类受众来说很可能被浪费掉。

    我们鼓励那些希望在直接 Web 应用程序编程(而非 Web 框架开发)中使用更美观(或以其他方式改进)的 WSGI 接口的人,开发能够封装 WSGI 以方便应用程序开发人员使用的 API 或框架。通过这种方式,WSGI 可以对服务器和中间件作者保持方便的底层特性,同时对应用程序开发人员来说又不至于“丑陋”。

提议中/讨论中

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

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

致谢

感谢 Web-SIG 邮件列表中的许多人,他们的深思熟虑的反馈使这份修订草案得以完成。特别感谢:

  • mod_python 的作者 Gregory “Grisha” Trubetskoy,他抨击了初稿,认为它没有提供比“普通 CGI”更多的优势,从而鼓励我寻找更好的方法。
  • Ian Bicking,他帮助我正确地指定了多线程和多进程选项,并敦促我提供一种机制,让服务器向应用程序提供自定义扩展数据。
  • Tony Lownds,他提出了 start_response 函数的概念,该函数接收状态和头信息,并返回一个 write 函数。他的输入也指导了异常处理机制的设计,尤其是在允许中间件覆盖应用程序错误消息方面。
  • Alan Kennedy,他勇敢地尝试在 Jython 上实现 WSGI(早在规范最终确定之前),这有助于塑造“支持旧版 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

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