Python Web 服务器网关接口

原文为 PEP 3333

目 录

前 言

本文档是 PEP 333 的升级版,针对 Python 3 进行了可用性方面的细微修改,采纳了几个针对 WSGI 协议的存在已久的事实修订(相关代码示例也已经移植到 Python 3)。

由于流程上的原因[1],本次修改是独立的,不会影响到 Python 2.x 下服务端或应用端的程序。如果你的程序遵守 PEP 333,则它必然符合此次升级后的要求。

但是如果你的程序运行在 Python 3 环境下,则必须注意下文中「字符串类型」和「Unicode 编码」两节中的要求。

文本上的具体差别可以查看 SVN 历史版本[2],版本号 84854 之前的是老版。

摘 要

本文档描述了 Web 服务器与 Web 应用或框架的标准交互接口,以提高 Python Web 应用在不同 Web 服务器之间具有可移植性。

基本原理和目标

Python 目前有很多 Web 框架,比如 Zope,Quixote,Webware,SkunkWeb,PSO 和 Twisted 等等[3]。新手面对如此多的选择十分纠结,因为一般来说,框架的选择会限制他们对 Web 服务器的选择,反之亦然。

相比之下,Java 也有非常多的 Web 框架,但是它的「servlet」API 使得用户无论选择哪种框架,都可以保证程序正常运行在支持该 API 的任何 Web 服务器上。

无论 Web 服务器是由 Python 写成,还是内嵌 Python,抑或使用 CGI 和 FastCGI 之类的网关协议调用 Python,在 Python 世界推广类似的 API 将使 Web 框架的选择与 Web 服务器无关,使框架和服务端的开发者能够专注于他们各自的领域。

因此本文档提出了一种简单且通用的接口以完成 Web 服务器与应用和框架的交互:Python Web 服务器网关接口(WSGI)。

不过仅仅一个 WSGI 的存在并不足以缓解当前 Python Web 开发中的困境,只有在服务端和框架中真的实现了 WSGI 才行。

由于现有的服务端和框架并不支持 WSGI,必须给哪些愿意支持 WSGI 的开发者们一点好处,故 WSGI 必须易于实现,以保证实现它的代价比较小。

因此,在服务端和框架中都容易实现对于 WSGI 的实用性至关重要,也是所有设计决策的主要考量。

在 Web 框架中易于实现并不代表对 Web 应用开发者也易用。WSGI 向 Web 框架开发者提供了绝对简洁的接口,因为类似响应对象(response objects)和 cookie 管理这样锦上添花的功能只会妨碍现有框架本身对应的功能。重申一遍,WSGI 的目标是使现有 Web 服务器与 Web 应用或框架的交互变得更容易,而不是发明一个新的 Web 框架。

这个目标要求 WSGI 不能依赖于任何在当前已部署的 Python 环境中不支持的功能。所以本次升级并没有产生新的标准库,且只要求用户的 Python 环境版本不低于 2.2.2(不过在未来的 Web 服务器标准库中集成新标准应该是一个不错的选择)。

除了在现有和未来出现的框架和服务端易于集成之外,WSGI 还应该能很容易地创建请求预处理器、响应处理程序和其他基于 WSGI 的中间件组件(对于 Web 服务器它们是应用,但对于它们包含的应用来说则是服务器)。

如果中间件能够既简洁又健壮,WSGI 又在服务端和框架中广泛应用,这将使一种全新的 Python Web 框架得以出现:一种由各种松耦合的 WSGI 中间件组成的框架。现有的框架开发者们甚至会重构他们的现有服务,使他们的框架更像一些和 WSGI 配合的库而不是一个独立的框架。这样 Web 应用开发者就可以针对特定的需求选择最好的组件,而不是只能接受某一个框架的所有优点和缺点。

当然,前途是光明的,道路是曲折的,WSGI 的短期目标是先让任意框架可以与任意 Web 服务器交互。

最后需要注意的是,当前版本的 WSGI 并没有规定一个应用具体以何种方式部署在 Web 服务器或网关服务器上,目前这由二者的具体实现决定。如果足够多实现了 WSGI 的服务器或网关在实践中产生了这个需求,也许可以另写一份 PEP 来描述 WSGI 服务器和应用框架的部署标准。

概 述

WSGI 接口有两种形式:服务端和应用端。服务端请求一个由应用端提供的可调用的对象,至于该对象应当如何被提供取决于服务端。有些服务端需要应用程序的部署人员编写一个简短的脚本来启动一个 Web 服务器或网关服务器的实例,以为此实例提供所需对象;而另一些服务端则需要配置文件或其他机制来指定从哪里导入或者得到所需对象。

除了 Web 服务器/网关服务器和 Web 应用/开发框架,还可以创建包含两种接口的中间件组件:对于 Web 服务器它们是应用,而对于应用来说他们是服务器。中间件可以用来提供扩展 API,内容转换,导航和其他有用的功能。

在本文档中,我们使用术语「可调用者」代表「一个函数,方法,类,或者拥有 __call__ 方法的一个实例」。实现「可调用者」的 Web 服务器,网关服务器或应用程序可以根据需要选择合适的实现方式;相反,请求「可调用者」的 Web 服务器,网关服务器或应用程序不可以依赖具体的实现方式。「可调用者」只能被调用,不能自省[4]

字符串类型

一般来说,HTTP 协议处理字节流,也就是说本文档主要面向字节流的处理。

不过字节流经常是文本意义上可读的,而在 Python 中,字符串类型是处理文本的趁手工具。

但是在很多 Python 的版本和实现中,字符串是 Unicode 编码的,而不是字节流。这要求我们在 HTTP 字节流与文本的相互转换和好用的 API 之间保持很好的平衡,尤其要注意支持基于不同版本的 Python 程序之间的可移植性,这些版本中字符串类型不尽相同。

因此 WSGI 定义了两种字符串类型:

1. 原生字符串(一般使用 str 类型实现)。这种字符串用在请求和响应的包头和元数据中。

2. 字节流字符串(在 Python 3 中使用 bytes 类型实现,其他版本中使用 str 类型实现)。这种字符串用在请求和响应的包内容中(比如POST 方法或 PUT 方法的输入数据以及 HTML 页面的输出)。

大家一定要注意不要搞混了:即使 Python 的 str 类型实质上是 Unicode 编码的,但是原生字符串的内容仍然将通过 Latin-1 编码转换为字节流(参见下文「Unicode 编码」一节可获得更多信息)。

简而言之,本文档中的「字符串」这个词都是指「原生字符串」,亦即一个 str 类型的对象,无论其实质上是字节流还是 Unicode 编码。任何地方出现的「字节流字符串」,都是指 Python 3 下 bytes 类型的一个实例,或者 Python 2 下 str 类型的一个实例。

因此,虽然 HTTP 某种意义上来说就是字节流,使用 Python 默认的字符串类型来解析会带来不少 API 使用上的好处。

应用端接口

应用对象是一个简单的接受两个参数的可调用对象。这里的对象并不是真的需要一个对象实例,一个函数、方法、类、或者带有 __call__ 方法的对象实例都可以当作应用对象。应用对象必须可以多次被调用,因为实际上所有的服务端(而非 CGI)都会产生这样的重复请求。

(注意:虽然我们称之为「应用」对象,但这并不是说程序员可以把 WSGI 当做 Web 编程 API 来调用!我们假定应用开发者仍然使用更高层面上的框架服务来开发应用,WSGI 是提供给框架和 Web 服务器开发者使用的工具,并不打算直接对应用开发者提供支持。)

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

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('')   # send headers now if body was empty
    finally:
        if hasattr(result, 'close'):
            result.close()

中间件:分饰两角的组件

同一个对象既可以作为服务端存在,也可以作为应用端存在。这样的中间件可以完成以下功能:

  • 重写上文代码中的 environ 之后,可以根据目标 URL 将请求转发到不同的应用程序对象
  • 允许多个应用程序或框架在一个进程中同时运行
  • 通过转发请求和响应,实现负载均衡和远程处理
  • 对内容进行后期处理,比如引入 XSL 样式表

中间件的存在对于服务端和应用端来说都应该是透明的,并且不需要特殊的支持。希望在应用程序中加入中间件的用户只需简单的把中间件当作应用提供给 Web 服务器,并配置中间件使其以服务器的身份与应用程序交互即可。当然,中间件包装后提供给服务器的应用也可以是另一个中间件,如此连锁下去便构成了所谓的「中间件栈」。

一般情况下,中间件要符合 WSGI 对应用端和服务端提出的一些限制和要求,有些时候这样的限制甚至比纯粹的服务端或应用端还要严格,这些地方我们会特别指出。

下面是一个非正式的中间件组件的示例,使用 Joe Strout 的 piglatin.py 将 text/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):
        if self.transform_ok:
            return piglatin(self._next())   # call must be byte-safe on Py3
        else:
            return self._next()

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 或其他对字典对象的模仿),应用程序可以以任意方式修改这个字典。environ 还应该包含一些特定的WSGI 所需的变量(在后面的小节里会详述),也可以包含一些服务器特定的扩展变量,通过下文的约定命名。

start_response 参数是一个接受两个固定参数和一个可选参数的可调用者。为了方便说明,我们分别命名为 status、response_headersexc_info,同样的,这样命名也不是强制的。应用程序必须用形式参数来调用 start_response(就像这样:start_response(status,response_headers))。

status 参数是一个形式上像「999 Message here」这样的状态字符串。而 response_headers 参数是一个 (header_name, header_value) 元组的列表,用来描述 HTTP 响应头。可选的 exc_info 参数会在下面的「start_response() 函数」和「错误处理」两节中详述,它只有在应用程序产生了错误并希望在浏览器上显示相关信息的时候才有用。

start_response 可调用者必须返回一个 write(body_data) 可调用者,它接受一个形式参数:一个将要被作为 HTTP 响应体的一部分输出的字节流字符串(注意:提供可调用者 write() 只是为了支持现有框架的必要的输出 API,新的应用程序或框架尽量避免使用,详细情况参见 缓冲区和流处理 一节)。

当被服务器调用的时候,应用对象必须返回一个生成 0 或多个字节流字符串的迭代器,这可以通过多种方法实现,比如返回一个字节流字符串的列表,或者应用程序本身是一个字节流字符串的生成器,或者应用程序是一个类而其实例是可迭代的.不管怎么实现,应用对象必须总是返回一个生成 0 或多个字节流字符串的迭代器。

服务器端必须将生成的字节流字符串以一种无缓冲的方式传送到客户端,每次传完一个再去获取下一个(换句话说,应用程序应该自己实现缓冲,更多关于应用程序如何处理输出的细节参见「缓冲区和流处理」小节)。

服务端应该把产生的字节流字符串当作字节流对待:尤其必须保证没修改行尾。应用程序负责确保字符串以与客户端匹配的编码输出(服务端可能会附加 HTTP 传送编码,或者为了实现一些 HTTP 特性而进行一些转换,比如字节范围转换(?),更多细节参见「其他 HTTP 特性」小节)

如果调用 len(iterable) 成功,服务器将认为结果是正确的。也就是说,应用程序返回的可迭代的字符串提供了一个靠谱的 __len__() 方法,那么肯定返回了正确的结果(关于这个方法正常情况下如何被使用参见「处理 Content-Length 头」)。

如果应用程序返回的迭代器有 close() 方法,则不管该请求是正常结束还是由于迭代错误或浏览器失去连接而终止,服务端都必须在结束该请求之前调用这个方法(这是用来支持应用程序对资源的释放,以完善 PEP 342 的生成器支持和其他含有 close() 方法的一般迭代器)。

应用程序返回的生成器或其他定制的迭代器并不一定会被使用,可能直接就被服务端关闭了。

(注意:应用程序必须在迭代器生成第一个字节流字符串之前调用 start_response() 可调用者,这样服务器才能在发送任何主体内容之前发送响应头。不过这一步也可以在迭代器第一次迭代的时候进行,所以服务器不能假定开始迭代之前 start_response() 已经被调用过了。)

除以上提及的之外,服务端不能直接使用任何应用程序返回的迭代器属性,除非该属性是针对服务端特定类型的一个实例,比如 wsgi.file_wrapper 返回的「文件包装器」(参见「(可选)特定平台上的文件处理」)。通常情况下,只有这里指定的属性,或者通过如 PEP 234 迭代 API 之类的途径获取的其他属性才是可以访问的。

environ 变量

environ 字典中要求包含下面这些在 CGI 规范中定义了的 CGI 环境变量。除非其值是空字符串(这种情况下如果下面没有特别指出的话它们可能会被忽略),下面这些变量必须存在:

REQUEST_METHOD

HTTP 请求的类型,比如「GET」或者「POST」。这个不可能是空字符串,所以是必须给出的。  

SCRIPT_NAME

URL 请求中路径的开始部分,对应应用程序对象(?),这样应用程序就知道它的虚拟位置。如果该应用程序对应服务器的根目录的话,它可能是空字符串。  

PATH_INFO

URL 请求中路径的剩余部分,指定请求的目标在应用程序内部的虚拟位置(?)。如果请求的目标是应用程序根目录并且没有末尾的斜杠的话,可能为空字符串。

QUERY_STRING

URL 请求中跟在「?」后面的那部分,可能为空或不存在。  

CONTENT_TYPE

HTTP 请求中任何 Content-Type 域的内容,可能为空或不存在。  

CONTENT_LENGTH

HTTP 请求中任何 Content-Length 域的内容,可能为空或不存在。  

SERVER_NAME,SERVER_PORT

这些变量可以和 SCRIPT_NAME、PATH_INFO 一起组成完整的URL。然而要注意的是,重建请求 URL 的时候应该优先使用 HTTP_HOST 而非 SERVER_NAME 。详细内容参见「URL 重建」。SERVER_NAMESERVER_PORT 永远不能为空字符串,也总是必须存在的。

SERVER_PROTOCOL

客户端发送请求所使用协议的版本。通常是类似「HTTP/1.0」或「HTTP/1.1」的东西,可以被用来判断如何处理请求包头。(既然这个变量表示的是请求中使用的协议,而且和服务器响应时使用的协议无关,也许它应该被叫做REQUEST_PROTOCOL。不过为了保持和 CGI 的兼容性,我们还是使用这个名字。)  

HTTP_ 变量

对应客户端提供的 HTTP 请求包头(即名字以「HTTP_」开头的各种变量)。这些变量的存在与否应该与请求中对应的 HTTP 包头是否存在相一致。

服务端应该尽可能提供所有可用的 CGI 变量。另外,如果使用了 SSL,服务端也应该尽可能提供所有可用的 Apache SSL 环境变量[5],比如 HTTPS=onSSL_PROTOCOL。不过要注意,使用了任何上面没有列出的 CGI 变量的应用程序对不支持相关扩展的服务器来说就不具有可移植性了。(比如 ,不发布文件的 Web 服务器就不能提供有意义的 DOCUMENT_ROOT 变量或 PATH_TRANSLATED 变量。)

支持 WSGI 的服务端应该在自述文档中说明它们可以提供些什么变量;应用端则应该简称所有需要的变量的存在性,并且在某变量不存在的时候有备用方案。

注意: 不存在的变量(比如在不需要验证的情况下的 REMOTE_USER 变量)应该被移出 environ 字典。另外要注意 CGI 定义的变量如果存在的话必须是原生字符串。任何 str 类型以外的 CGI 变量都是不符合本规范。

除了 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 编码而不报错,并且允许自行替代在该平台编码中无法渲染的字符。)
很多 Web 服务器中 wsgi.errors 是主要的错误日志,也有一些使用 sys.stderr 或其他形式的文件来记录。Web 服务器的自述文档中应该包含如何配置错误日志以及如何找到记录的位置。服务端可以在被要求的情况下,向不同的应用提供不同的错误日志。

wsgi.multithread

如果应用对象可能会被同一进程的另一个线程同步调用,此变量值为真,否则为假。

wsgi.multiprocess

如果同一个应用对象可能会被另一个进程同步调用,此变量值为真,否则为假。
wsgi.run_once 如果服务端期望(但是不保证能得到满足)应用对象在生命周期中之辈调用一次,此变量值为真,否则为假。一般只有在基于类似 CGI 的网关服务器中此变量才会为真。

最后,environ 字典也可以包含服务端定义的变量。这些变量的名称应当由小写字母、数字、点和下划线组成,并且应当带有一个所在服务端独有的前缀。比如 mod_python 可以定义 mod_python.some_variable 这样的变量。

输入流和错误流

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

方 法

Notes(?)

read(size)

输入流

1

readline()

输入流

1, 2

readlines(hint)

输入流

1, 3

__iter__()

输入流

 

flush()

错误流

4

write(str)

错误流

 

writelines(seq)

错误流

 

每个方法的语义如果上面没有特别指出均和 Python 标准库介绍中记载的一样:

  1. 不要求 Web 服务器读取超过客户端指定的 Content-Length 的内容,并且应该在应用尝试读取越界内容时虚拟出一个文件结束符。应用不应该尝试读取超过 Content-Length 指定长度的内容。
    Web 服务器应该允许不使用参数调用 read(),并返回客户端输入流剩余的部分;同时服务器应该对任何尝试读取空的或到文件尾的输入流的行为返回空字符串。
  2. Web 服务器应该支持 readline() 函数的可选「size」参数,但是在 WSGI 1.0 版本中可以忽略这一点。
    (在 WSGI 1.0 中,「size」参数并不要求提供,因为可能很难实现也不常用。但是由于 CGI 模块开始支持它了,所以生产环境中的 Web 服务器还是得实现「size」参数。)
  3. readlines() 函数的「hint」参数对于调用者和实现者来说都是可选的。应用端完全可以忽略它,服务端亦然。
  4. 由于错误流不能重设读写位置,服务端可以使用无缓冲模式来进行写操作。在这种情况下,flush() 函数不做任何操作。但是具有良好可移植性的程序不能假设输出流是无缓冲或 flush() 函数是误操作的,而应当在需要输出真的被写到存储设备中的时候调用 flush() 函数。(比如防止多进程写数据造成的混乱这种情况。)

上表中列出的方法必须被所有遵守本规范的服务端支持。遵守本规范的应用端则不应该调用任何其他的输入流和输出流相关的方法和属性。尤其要注意的是,应用端即使在这些流提供 close() 方法的情况下,也不应该尝试关闭它们。

start_response() 可调用者

传给应用程序对象的第二个参数是一个形为 start_response(status, response_headers, exc_info=None) 的可调用者。(像 WSGI 的所有其他可调用者一样,这个参数必须使用形式参数提供,而不能以关键字参数提供。)start_response 可调用者用于开始 HTTP 响应,它必须返回一个 write(body_data) 可调用者(参见 缓冲区和流处理)。

status 参数是一个形如「200 OK」或「404 Not Found」这样的 HTTP 状态字符串。换言之是一个由状态编号和具体信息组成的字符串,按顺序并用空格隔开,两头没有其他空格和其他字符。(更多信息参见 RFC 2616,6.1.1 小节。)该字符串禁止包含控制字符,也不允许以回车、换行或二者的组合形式结尾。

response_headers 参数是一个(header_name, header_value)元组的列表。它必须是一个 Python 中的列表,即 type(response_headers) 的值为 ListType,并且 Web 服务器可以以任何方式改变其内容。每一个 header_name 必须是一个不含冒号或其他标点符号的合法的 HTTP header 字段名(RFC 2616 4.2 小节中有详细定义)。

每一个 header_value 禁止包含任何控制字符(包括回车或换行)。(这些要求是为了将那些必须检查或修改响应头的服务端和中间件所必须执行的解析工作的复杂性降到最低。)

一般来说,服务端要保证发送到客户端的包头的正确性:如果应用忽略了 HTTP 规定的包头(或其他类似的内容)服务端必须自己加上。比如 HTTP 的「Date:」和「Server:」包头一般都是由服务端提供的。

(服务端开发者小贴士:HTTP 包头名称是大小写敏感的,所以请确保你们检查应用提供的包头时考虑了这一点!)

应用和中间件禁止使用 HTTP/1.1 中的「逐跳」机制和包头,HTTP/1.0 中类似的机制也禁用,任何应用客户端到 Web 服务器的连接持久性的包头也都不允许使用。使用这些特性是服务端的特权,服务端发现客户端违反此规定时应视为致命错误,需在请求提交到 start_response() 时报错。(关于「逐跳」特性和包头,请参见「其他 HTTP 特性」小节。)

在 start_response 被调用时 Web 服务器需要检查是否有错误,所以可以在应用正在运行的时候报错。

但是,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 包头在其时已有输出,start_response 必须报错,且应当使用 exc_info 元组再报一次:

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

以上代码会把应用捕获的异常再抛出一次,原则上会终止应用。(当 HTTP 包头已经被送出后应用尝试将错误信息输出至浏览器的行为是不安全的。)如果应用使用 exc_info 参数调用 start_response,则禁止捕获任何由 start_response 抛出的异常,而应该让该异常被返回到服务端。详见「错误处理」小节。

当且仅当提供 exc_info 参数时,应用可能会调用 start_response 多次。说得更精确一点,如果 start_response 应该在当前的应用调用中被调用过了,再次调用时如果不提供 exc_info 参数就会引发一个致命错误。第一次调用 start_response 出错也包括在这种情况中。(参见上文的 CGI 网关示例以领会正确的逻辑流程。)

注意,集成了 start_response 的 Web 服务器、网关服务器和中间件必须保证在 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 包头,Web 服务器不应该传送大于该包头指定长度的数据给客户端,而应该在发送了足量数据之后停止对改请求进行迭代,或者在应用尝试在此后调用 write() 时报错。(当然,如果应用提供的数据量不够 Content-Length 指定的大小,Web 服务器应当关闭此连接或直接报错。)

如果应用没有提供 Content-Length 包头,服务端有好几种方法可以处理。最简单的一种是当响应结束后关闭客户端连接。

但是在某些情况下,服务端要么必须自己生成一个 Content-Length 包头,或者至少避免关闭客户端连接。如果应用没有调用 write() 函数,且返回一个长度为 1 的迭代器,服务端应该根据迭代器生成的第一个字节流字符串自动确定 Content-Length 包头的值。

如果服务端和客户端都支持 HTTP/1.1 标准中的「整块编码」[6],则服务端可以使用「整块编码」在每次调用 write() 或迭代器每生成一个字节流字符串就发送一个数据块,同时为每一个数据块生成一个 Content-Length 包头。这种方法使服务端能够在需要的情况下保证客户端的连接不断。在采用这种方法时服务端必须遵守 RFC 2616,否则就只能使用其他方法来处理 Content-Length 包头缺失的情况。

(注意:应用和中间件禁止在各自的输出中使用任何数据编码手段,比如分块或压缩;在进行「逐跳」操作时,这些编码方式是服务端的特权。参见「其他 HTTP 特性」小节以获取更多细节。)

缓冲区和流处理

一般来说,应用通过缓存适量的数据最后一次输出来达到最佳的吞吐量。这是现有框架比如 Zope 中常用的技术:输出被缓存在一个 StringIO 或类似对象中,最后与响应包头一起一次输出。

WSGI 中类似的处理是让应用简单地返回一个包含字节流字符串形式的响应包体的迭代器(比如一个列表)。对于绝大多数渲染数据量很小(足以放在内存中)的 HTML 页面的应用函数都建议采用这种方法。

但是对于大文件,或对于 HTTP 流的特殊用途(比如多部件「服务器推送」),应用可以根据需要将输出以小块形式提供(比如为了避免将一个大文件全部加载到内存中)。有时候响应的部分数据生成很耗时,也可以将已经生成的输出提前发送过去。

在这些情况下,应用经常会返回一个生成迭代器来生成一块一块的输出。这些数据块可以被打散以适应多部件边界(比如「服务器推送」),或者只是在耗时很长的任务之前进行(比如读取另一个在磁盘上的文件数据块)。

遵循 WSGI 的 Web 服务器,网关服务器和中间件不能延迟传送任何数据块。它们必须要么将整个数据块传送给客户端,要么保证即使应用在生成下一个数据块的时候它们仍然会继续传输,可以通过以下三种方法提供保证:

  1. 将这个数据块转交给操作系统并请求刷新所有系统缓存。
  2. 使用另一个单独的线程保证数据块在应用生成下一个数据块的时候继续传送。
  3. 中间件还可以将整个数据块传送给其上层的网关服务器或 Web 服务器。

通过提供这个保证,WSGI 保证数据传送不会再应用输出数据的某个时刻被打断。这对于诸如多部件「服务器推送」输出流之类技术有重要作用,在这些技术中多部件边间之间的数据要求必须完整地传送到客户端。

Block Boundaries 的中间件处理

为了更好地支持异步的应用和服务器,中间件不能在等待应用迭代器生成多个值时阻塞迭代。如果中间件需要在应用尚未有输出的时候收集更多数据,则它必须生成一个空的字节流字符串。

换言之,每次下层应用产生一个值时中间件都必须相应生成至少一个值。如果中间件不能生成任何有意义的值,则生成一个空的字节流字符串。

这个规定保证了异步应用和服务器能够合作,以减少同时支持固定数量应用实例所需的线程数量。

值得注意的是,这要求中间件必须在下层应用返回一个迭代器时也立即向上返回一个迭代器。中间件禁止使用 write() 可调用者来传送下层应用生成的数据。中间件只能调用上层服务器的 write() 可调用者来传送下层应用调用中间件自己的 write() 可调用者生成的数据。(译者注:即下层只能调用上层的 write() 可调用者。)

write() 函数

某些现有框架的 API 支持无缓冲输出的方法与 WSGI 不同。尤其是他们提供了某种形式的 write 函数以无缓冲地写入一个数据块,或者提供了一个有缓冲的 write 函数和一个 flush 机制来刷新缓存。

不幸的是这些 API 不能使用 WSGI 应用的迭代器返回值来实现,除非使用多线程或类似的特殊技术。

因此为了让这些框架继续使用必要的 API,WSGI 包含了一个特别的 write() 可调用者,由 start_response 可调用者返回。

新的 WSGI 应用和框架在不必要的时候不应该使用 write() 可调用者。write() 可调用者是为了支持必要的流式 API 的一种 hack 手段。一般而言,应用应该使用迭代器返回输出,这样 Web 服务器可以在同一个 Python 线程中交替完成不同的任务,从而潜在地提高服务器的吞吐量。

write() 可调用者由 start_response() 可调用者返回,只接受一个参数:作为 HTTP 包体一部分的一个字节流字符串,并将此字符串当作由输出迭代器生成的。换言之,在 write() 返回前,必须保证传入的字节流字符串要么被完整地传送到了客户端,或者在应用继续运行的时候已被缓存起来等待传送。

应用必须返回一个迭代器对象,即使它使用了 write() 来生成全部或者部分的响应包体。返回的迭代器必须为空(即不生成任何非空字节流字符串),但是如果它确实生成了非空字节流字符串,则该输出必须被 Web 服务器或网关服务器当作一般输出处理(即必须被立即传送或缓存)。应用禁止在其返回的迭代器内部调用 write(),因此任何迭代器生成的字节流字符串必须在所有传递给 write() 的字符串被发送给客户端之后才能进行传送。

Unicode 编码

HTTP 并不直接支持 Unicode,WSGI 亦然。所有的编码和解码工作由应用自己完成,所有发送给服务器或从服务器接收的字符串必须是 str 类型或 bytes 类型的,而不能是 unicode 类型。在要求使用字符串类型的地方使用 unicode 类型的结果是不可知的。

作为状态或响应包头传递给 start_response() 的字符串必须遵守 RFC 2616 中关于编码的规定,即要么是 ISO-8859-1 字符,要么使用 RFC 2047 MIME 编码。

在 str 或 StringType 类型实际上是 Unicode 编码的 Python 平台(如 Jython,IronPython,Python 3 等)上,所有对应于本规范的「字符串」只能包含对应于那些 ISO-8859-1 可表示编码点的 Unicode 编码(即 \u0000\u00FF)。应用使用的字符串中包含有其他字符或编码点会引发致命错误。Web 服务器和网关服务器也禁止提供包含其他 Unicode 字符的字符串。

再强调一遍,所有对应到本规范的「字符串」必须是 str 类型或者 StringType 类型,而不能是 unicode 类型或者 UnicodeType 类型。即使已有的平台允许在 str 或 StringType 类型中使用每个字符多于 8 比特的编码,也只有低 8 位允许使用。

本规范中所谓的「字节流字符串」(即从 wsgi.input 中读入的值,最后会传递给 write() 或由应用生成),其值必须是 Python 3 下的 bytes 类型,或者更低版本 Python 中的 str 类型。

错误处理

一般而言,应用应该自己捕获内部错误,并在浏览器中显示有帮助的错误信息。(由应用自己决定什么叫「有帮助」。)

但是要显示这条信息,应用在之前必须没有发送任何数据到浏览器,或者可以冒险中断响应。WSGI 提供了一个机制以使应用要么能够传送错误信息,要么会被自动终止:通过 start_response 的 exc_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 会重新抛出异常。这个异常不能被应用捕获,所以应用不会终止。Web 服务器或网关服务器能够捕获这个异常并终止响应。

服务器应该捕获并记录所有终止了应用或其返回值迭代过程的异常。如果错误发生时部分响应信息已经被传递给浏览器,Web 服务器或网关服务器可以尝试在输出中添加一条错误信息,只要已发送的包头包含服务器可以显式修改的 text/* 类型内容。

某些中间件可能希望能够提供其他的错误处理机制,或拦截和替换应用错误信息。在这种情况下,中间件可以选择不重新将 exc_info 抛出给 start_response,但是必须相应地抛出一个中间件特定的异常,或者缓存下提供的参数后简单地正常返回。这迫使应用返回其错误信息迭代器(或调用 write()),从而使中间件能够捕获和修改错误信息。这些技术要求开发者遵循如下规范:

  1. 开始错误响应时总是提供 exc_info 参数。
  2. 提供了 exc_info 参数的情况下不要捕获任何由 start_response 抛出的异常。

HTTP 1.1 相关

实现了 HTTP 1.1 标准的 Web 服务器和网关服务器必须提供 HTTP 1.1 标准中「expect/continue」机制的透明支持。这可以通过以下方法做到:

  1. 对于任何「Expect: 100-continue」的请求返回一个即时的「100 Continue」响应,然后正常继续运行。
  2. 继续正常运行,但是提供给应用一个 wsgi.input 流,这个流会在应用第一次尝试读取输入流的时候发送「100 Continue」响应。读请求之后必须阻塞,直到客户端响应为止。
  3. 阻塞请求直到客户端意识到服务器不支持 expect/continue 机制,然后自己发送请求包体。(这种方法不是最优的,不推荐使用。)

这些限制并不针对 HTTP 1.0 的请求,也不适用于不传递给应用对象的请求。参见 RFC 2616 8.2.3 小节和 10.1.1 小节以获取更多关于 HTTP 1.1 Expect/Continue 请求的信息。

其他 HTTP 特性

一般来说,服务端应该让应用全权负责控制自己的输出。服务端只能进行不影响应用响应语义的改动。应用开发者总是可以通过添加中间件来提供附件特性,因此服务端开发者在实现过程中必须尽可能保守。从某种意义上来说,Web 服务器应当视自己为 HTTP 「网关服务器」,而将应用看成一个 HTTP 的「源服务器」。(参见 RFC 2616 1.3 小节获取更多信息。)

但是因为 WSGI 服务端和应用端不通过 HTTP 交互,所以 RFC 2616 称之为「逐跳」包头的特性不适用与 WSGI 的内部通信。WSGI 应用不能生成任何「逐跳」包头[7],不能使用任何需要生成该包头的 HTTP 特性,也不能依赖于 environ 字典中的任何传入的「逐跳」包头。WSGI 服务端必须自己处理任何传入的能够支持的「逐跳」包头,比如对传入的 Transfer-Encoding 进行解码,如果可能的话其中也包括整块编码。

以上的原则适用于很多 HTTP 特性,服务端可以通过 If-None-Match 和 If-Modified-Since 请求包头以及 Last-ModifiedETag 响应包头来处理缓存生效的问题。但是这并不是必要的,应用如果想支持该特性应该自己处理自己的缓存生效问题,因为服务端不一定会处理。 

类似情况比如服务端可以对应用的响应进行重新编码或传输编码,但是应用应该自己选择一个合适的内容编码方式,并且禁止使用传输编码。服务端可以在客户端要求的时候传输应用响应的字节范围(?),而应用并不原生支持字节范围,但是同样的,应用应该在有需求时自己干这个。

请注意,这些限制条件并不是要求应用把每一个 HTTP 特性都自己重新实现一遍。很多特性可以部分或全部被中间件实现,从而避免服务端和应用端的开发者一次又一次地重复实现同样的特性。

线程支持

线程机制的支持与否取决与各 Web 服务器自身。可以并行处理多个请求的服务器,必须提供单线程运行应用的选项,以使非线程安全的应用或框架仍然能够在其上运行。

应用实现指南

服务器扩展 API

一些服务端开发者希望暴露更多的高级 API,以使应用端开发者用来实现特殊的需求。比如说一个基于 mod_python 的网关服务器会希望以 WSGI 扩展的方式暴露部分 Apache 的 API。

在最简单的情况下,这只要求定义一个 environ 变量,比如 mod_python.some_api。但是很多时候可能存在的中间件会使情况变得复杂起来。比如一个 environ 变量中提供访问某个 HTTP 包头功能的 API,可能在 environ 被中间件修改之后返回不同的值。

一般而言,任何复制、补足或绕过了部分 WSGI 功能的 API 都有与中间件不兼容的危险。服务端开发者不应该假设没有使用中间件,因为某些框架开发者尤其希望将他们的框架设计或重构成类似中间件的样子。

所以为了提供最大程度的兼容性,提供扩展 API 以取代某些 WSGI 功能的服务端应该精心设计,以使它们在被调用时使用了这些扩展 API。举例来说,一个访问 HTTP 请求包头的 API 必须要求应用传递其当前的 environ,以使服务端能确定通过该 API 能访问到的 HTTP 包头没有被中间件修改。如果扩展 API 不能保证其对 HTTP 包头的要求与 environ 一致,那么它必须通过报错、返回 None 而不是包头集合或任何其他合适的方式拒绝为应用服务。

类似的例子还有如果扩展 API 提供写入响应数据或包头的功能,它必须要求应用在使用扩展功能之前传入 start_response 可调用者。如果该可调用者与服务端最早从应用那儿收到的不一致,那么该 API 便不能保证正确的响应,只能拒绝为应用提供改扩展服务。

这些指导原则也适用于在 environ 中额外添加了类似解析过的 cookie、构造变量、会话等内容的中间件。尤其是那些将这些功能以作用于 environ 的函数形式提供的中间件,相比简单将数据插入 environ 中的中间件更要注意。这保证了在每次中间件对 environ 进行了 URL 重写或其他修改之后 environ 中的信息都会被检查一遍。

这些「安全扩展」的原则非常重要,服务端和应用端开发者都应该遵守,以避免未来的某个时候中间件开发者不得不删除某些或全部涉及 environ 的扩展 API,以免中间件的功能因为应用调用了扩展 API 而失效。

应用配置

本规范并没有定义服务端如何选择或获取一个应用来调用。这些以及其他的配置选项是由服务端根据自己的特定情况决定的。服务端开发者应该在自述文档中记述如何配置才能以特定的选项(如线程选项)执行一个特定的应用程序对象。

另一方面,框架开发者应该在自述文档中记载如何创建一个包含框架功能的应用对象。在服务端和应用端都使用了框架的用户必须将二者结合起来考虑。但是由于双方现在都有通用接口了,这只是个体力活儿,而不是一个重要的工程难题。

最后,有些应用、框架和中间件希望使用 environ 字典来收取简单的配置选项字符串。Web 服务器和网关服务器应该通过允许应用开发者在 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 reconstruction

如果应用希望重建一个请求的完整 URL,可以通过下面由 Ian Bicking 提供的算法来实现:

from urllib 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

某些 Web 服务器、网关服务器或者应用可能会需要支持低于 2.2 版本的 Python。在使用 Jython 作为平台的时候这一点尤其重要,因为高于 2.2 版本的 Jython 还不能在生产环境中应用。

对于 Web 服务器和网关服务器,这种支持相对直接:目标平台是低于 2.2 版本的 Python 的服务器和网关只能使用一个标准的 for 循环来迭代任何应用返回的迭代器。这是唯一保证各版本间的迭代器协议在源码级兼容的方法,后面我们会详细讨论。(最新的迭代器协议见 PEP 234。)

(注意这个技术只适用于 Python 下的 Web 服务器、网关服务器和中间件。其他语言中的迭代器协议如何正确使用超出了本规范的讨论范围。)

对于应用程序,支持低于 2.2 版本的 Python 有一点麻烦:

  • 你不能返回一个文件对象并期望它像一个迭代器一样工作,因为从 Python 2.2 开始文件就不是迭代器了。(一般而言你也不应该使用这种方法,因为绝大多数情况下这是一种丑陋的实现!)应该使用 wsgi.file_wrapper 或者应用指定的文件包装器。(参见 (可选)特定平台上的文件处理小节以获取更多文件包装器的信息,以及一个可以用来将文件包装为迭代器的样例类。)
  • 如果你返回一个经过定制的迭代器,它必须实现 2.2 版本之前的迭代器协议。亦即提供一个 __getitem__ 方法,这个方法接受一个整数键值,当该值耗尽时就会抛出 IndexError 异常。(内建的序列类型也是可接受的,因为它们已经集成了相关协议。)

最后,希望支持低于 2.2 版本的 Python 且迭代应用返回值或本身返回一个迭代器的中间件必须遵守以上提到的相应的推荐方法。

(注意:Web 服务器、网关服务器、应用或者中间件在支持低于 2.2 版本的 Python 都必须只使用该版本支持的特性,比如使用 1 和 0 来代替 True 和 False 等。)

(可选)特定平台上的文件处理

某些操作系统提供特殊的高性能文件传输功能,比如 Unix 的 sendfile() 系统调用。Web 服务器和网关服务器可以通过 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), '')

如果 Web 服务器或网关服务器支持 wsgi.file_wrapper,则它必须是一个可调用者,接收一个必须的形式参数和一个可选的形式参数。第一个形式参数是一个待发送的类文件对象,第二个可选的则是一个建议的块大小(服务端不一定要采纳)。这个可调用者必须返回一个迭代对象,并且禁止在服务端实际接收到该迭代器返回值之前传送任何数据。(不这样做的话会妨碍中间件对响应数据进行译码或修改。)

为了被看做一个文件,应用提供的对象必须有一个能接受一个可选文件大小参数的 read() 方法。该对象也可以有一个 close() 方法,如果提供了这个方法,wsgi.file_wrapper 返回的迭代器就必须提供一个 close() 方法,这个方法最终调用了对象提供的 close() 方法。如果该对象提供了任何与 Python 内建文件对象名字一样的方法或属性(比如 fileno()),wsgi.file_wrapper 可以假设这些方法和属性与它们作为内建的方法和属性时语义相同。

任何平台相关的文件处理必须是现在应用返回之后,并且由 Web 服务器和网关服务器来检查包装器对象是否返回了。(再强调一遍,由于中间件、错误处理程序之类的存在,并不保证包装器被创建了就一定会被使用。)

除了对于 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() 的迭代器,以使使用文件包装器的应用能够跨平台移植。下面有一个简单的不依赖特定平台的文件包装器类,适用于所有版本的 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()