器→工具, 编程语言

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

钱魏Way · · 537 次浏览

WSGI(Python Web Server Gateway Interface) 为 Web Server 和 Python Web Application 之间提供了标准的数据通道. 是 Python 界的 一个广泛的可用的 WEB API 规范, 使 web server 提供更加规范的 API, 给 web Application, 从而使 开发者更加专注于业务逻辑。WSGI一开始是在PEP-0333中定义的,最新版本是在Python的PEP-3333定义的。下文是整理的PEP-3333的中文翻译。

为了解 PEP 333的读者准备的前言

本文档是 PEP 333 的升级版,针对 Python 3 进行了可用性方面的细微修改,采纳了几个针对 WSGI 协议的存在已久的事实修订 (相关代码示例也已经移植到 Python 3)。 由于流程上的原因,本次修改是独立的,不会影响到 Python 2.x 下服务端或应用端的程序。如果你的程序遵守 PEP 333,则它必然符合此次升级后的要求。但是如果你的程序运行在 Python 3 环境下,则必须注意下文中「字符串类型」和「Unicode 编码」两节中的要求。文本上的具体差别可以查看 SVN 历史版本,版本号 84854 之前的是老版。

摘要

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

基本原理和目标(来自PEP 333)

Python 目前有很多 Web 框架,比如 Zope,Quixote,Webware,SkunkWeb,PSO 和 Twisted 等等。 新手面对如此多的选择十分纠结,因为一般来说,框架的选择会限制他们对 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 服务器,网关服务器或应用程序不可以依赖具体的实现方式。「可调用者」只能被调用,不能自省。

字符串类型

一般来说,HTTP 协议处理字节流,也就是说本文档主要面向字节流的处理。不过字节流经常是文本意义上可读的,而在 Python 中,字符串类型是处理文本的趁手工具。 但是在很多 Python 的版本和实现中,字符串是 Unicode 编码的,而不是字节流。 这要求我们在 HTTP 字节流与文本的相互转换和好用的 API 之间保持很好的平衡, 尤其要注意支持基于不同版本的 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 默认的字符串类型来解析会带来不少 API 使用上的好处。

应用/框架端

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

(注意:虽然我们称之为「应用」对象,不要误解为应用开发者需要使用 WSGI 作为 web 编程 API!!应用开发者可以继续使用已经存在的、高级框架服务去开发他们的应用。WSGI 是一个为框架开发者和服务器开发者准备的工具,应用开发者不需要直接使用 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 网关,用Python函数实现,接收应用对象。注意这个简单的示例在错误处理方面相当简单,因为默认情况下,未捕获的异常会被 dump 到 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,将请求对象路由给不同的应用对象。
  • 支持多个应用或者框架顺序地运行于同一个进程中。
  • 通过转发请求和响应,支持负载均衡和远程处理。
  • 支持对内容做后处理(postprocessing),比如处理一个 XSL 样式表文件。

中间件的灵魂是:对 WSGI 接口的服务器/网关端和 应用/框架端是透明的,不需要其他条件。希望将中间件合并进应用的用户,将这个中间件传递给服务器即可,就好像这个中间件是一个应用对象;或者让中间件去调用应用对象,好像这个中间件就是服务器。当然,被中间件包装(wrap)的应用对象,实际上可能是另一个包装了另一个应用的中间件,以此类推,就创建了一个中间件栈(middleware stack)。

最重要的,中间件必须同时满足服务端和应用端的限制和条件。然而,在有些情况下,中间件需要的条件比单纯的服务端或者应用端更严格,这些点会在下面予以说明。

以下是一个中间件示例。它转换 text/plain 响应为 pig Lain 响应,使用Joe Strout的脚本piglatin.py。(真正的中间件组件会用一种更完善的方法来检查内容类型和内容编码。这个例子也忽略了一个单词可能跨越块边界(block boundary)的情况的处理。)

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))

规范细节

应用对象接受两个位置参数。为了便于说明,我们分别称他们为 environ 和 start_response,但不一定非要叫这两个名字。服务器或者网关必须使用位置参数而不是关键字参数调用应用对象。例如:result = application(environ, start_response)

  • environ参数是一个字典对象,包含了 CGI 样式的环境变量,必须使用 Python 内置的字典对象(不是子类、UserDict 或者其他模拟字典的对象),应用对象可以任意修改这个 environ。 environ 必须包含一些 WSGI 需要的变量(在后面的小节里会详述),也可能包含服务器扩展的一些变量,下文会有描述。
  • start_response 参数是一个可调用对象,接受两个必须的位置参数和一个可选参数。为了便于说明,我们叫这三个参数 status、response_headers 和 exc_info,但它们也可以改为其他名字。应用对象必须用位置参数调用 start_response,例如:start_response(status, response_headers) 。

status 是一个格式为「999 Message here」的状态字符串。response_headers 是一个 (header_name, header_value)的元组列表,描述了 HTTP 的响应头信息。可选的exc_info参数会在 start_reponse() 和 错误处理介绍,仅当应用对象出现异常并尝试向浏览器显示一个错误消息时需要。

start_response则必须返回 write(body_data)的可调用对象。body_data是字节字符串,表示 HTTP 的响应体部分。请记住:之所以需要write(),是因为已经存在的有些框架中必须用到它。在新的应用或者框架中应该尽量避免使用 write()。查看 缓冲和流 这一节可以了解更多。

当服务器调用应用对象时,应用对象应该返回一个可迭代的(iterable) 对象,此对象生成0或者多字节的字节字符串。这可以有多种方法实现,比如返回一个字节字符串的 list、应用使用生成器函数实现,或者应用对象用类实现,而此类实现了迭代器协议。不管应用对象如何实现,它总是返回一个可迭代产生0或者多字节字符串的对象。

在处理下一个请求之前,服务器/网关则必须以非缓冲的方式传输生成的字节字符串给客户端。换句话说,缓冲应该由应用对象负责。查看 缓冲和流 这一节了解应用对象如何处理输出。

服务器/网关应该视这些被生成的字节字符串为二进制字节序列:尤其需要确认行结束符没有被修改。应用对象负责确保输出的字节字符串以客户端可以理解的格式输出。(服务器/网关应该设置 HTTP 传输编码,或者为了实现某个 HTTP 特性而转换传输编码。查看 HTTP 特性 可以了解更多)

如果调用 len(iterable)成功了,服务器使用这一准确结果。也就是说,应用对象返回的可迭代结果对象实现了__len__()方法,服务器必须返回这个结果。(查看 处理 Content-Length 头这一节了解如何处理这个头的)

如果应用对象返回的可迭代对象支持 close()方法,服务器/网关必须在结束当前请求处理之前调用这个方法,不管是正常完成,还是在迭代期间或者过早断开了和浏览器的连接而异常终止。(需要close()方法是因为应用对象需要释放一些资源。本协议希望支持 PEP 342的生成器和其他常见的带有 close()方法的可迭代对象)。

返回生成器或者其他自定义迭代器的应用对象不能假设返回对象总是会正常结束,因为有可能会被服务器提前关闭。

(请记住:应用对象必须在可迭代对象生成第一个字节之前调用 start_response(),因为服务器在发送任务 HTTP 响应体之前需要先发送响应头。然而,服务器/网关不能寄希望于可迭代对象的第一次迭代结果,也就是服务器开始对可迭代对象输出之前不能假设start_response()已经被调用过了,有可能没有调用)。

最后,服务器/网关绝对不能直接使用应用对象返回的这个可迭代对象的其他属性,除非它是一个对此服务器/网关 类型特定的对象,例如 wsgi.file_wrapper 的实例(查看 平台特定的文件处理)。一般情况下,只有规范指定的属性和 PEP 234 可迭代 API 指定的属性可以访问。

环境变量

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

  • REQUEST_METHOD HTTP 的请求方法,如「GET」、「POST」。REQUEST_METHOD 永远不可能是空字符串,所以总是出现。
  • SCRIPT_NAME 请求的 URL 的路径(path)的末尾部分,应用对象知道它的虚拟位置。如果应用对象对服务器的「root」进行相应,这个值可能是空字符串。
  • PATH_INFO 请求 URL 的剩余部分,指出请求的目标在应用对象上的具体位置(location)。这个变量的值也有可能是空字符串,如果请求 URL 的目标是应用对象的 root,并且没有以”/”结束。
  • QUERY_STRING 如果请求 URL 中有”?”,就是”?”之后的部分。所以也有可能是空字符串。
  • CONTENT_TYPE HTTP 请求中Content-Type的值,可能为空,或者不存在。
  • CONTENT_LENGTH HTTP 请求中 Content-Length 的值,可能为空,或者不存在
  • SERVER_NAME , SERVER_PORT 把SCRIPT_NAME 和 PATH_INFO 相加,就是完整 URL。然而,如果有HTTP_HOST ,HTTP_HOST应该使用SERVER_NAME一起构造请求 URL。查看URL 结构 这一节了解更多。 SERVER_NAME 和 SERVER_PORT 永远不是空字符串,所以总是需要的。
  • SERVER_PROTOCOL 发送请求的客户端使用的协议版本,常见的如「HTTP/1.0」 「HTTP/1.1」。应用对象用来处理相关请求头(这个变量可能叫做REQUEST_PROTOCOL更合适,因为它描述了请求的版本,在服务器的响应中是无关紧要的,然而,为了兼容 CGI,我们还是保持现在的名字)
  • 以 HTTP_开头的变量客户端给出的其他以 HTTP_ 开头的变量。这些变量应该根据请求头是否存在而出现在 environ 中。服务器/网关应该尝试提供这些 CGI 变量。另外,如果使用了 SSL,服务器/网关应该尽量提供一些 Apache 的 SSL 环境变量,如 HTTPS=on 和 SSL_PROTOCOL。 不过要注意,使用了任何上面没有列出的 CGI 变量的应用程序对不支持相关扩展的服务器来说就不具有可移植性了。 (比如 ,不发布文件的 Web 服务器就不能提供有意义的 DOCUMENT_ROOT 变量或 PATH_TRANSLATED 变量。)

 

WSGI 兼容的服务器/网关应该随着他们的定义文档,也用文档说明它提供的变量。应用对象应该检查他们需要的任何变量,当发现缺少某个变量时准备一个回调计划。

注意:丢失的变量(如当没有验证时没有REMOTE_USER)也不要出现在 environ 中。CGI 定义的变量必须是原生字符串(python2中 str,python3中 bytes)

除了 CGI 定义的变量,environ 字典可能包含额外的操作系统的「环境变量」。除了这些,还必须包含以下 WSGI 定义的变量:

  • version 使用 tuple (1, 0) , 表示 WSGI 的版本是 1.0
  • url_scheme 表示 URL 的模式部分,应用对象将以这个模式被调用。一般情况下,值为”http”或者”https”。
  • input 一个输入流(类文件对象),从这个对象读取 HTTP 请求体。(当被应用对象请求时,服务器/网关执行 read ,或者预读取请求体,缓存它到内存或者磁盘中,或者用输入流提供的其他技术)
  • errors 错误输出流(类文件对象), 日志记录程序或其他标准的/中心位置的目的。它应该是一个文本模式的流。应用对象使用”\n”作为行结束符,而服务器/网关会将它转换成相应的输出值。(在一些平台上,str 是 unicode 字符。错误输出流应该能接受并记录普通 unicode 而不抛出异常。然后允许用流的编码转换不能显示的 unicode 字符。)对许多服务器来说,wsgi.errors应该是主错误日志。另一个选择是 sys.stderr,或者某个日志文件。服务器文档应该包含如何配置错误日志文件的说明。服务器/网关可能支持错误流到不同的日志处理应用上去,如果这是希望的。
  • multithread 如果应用对象能够同时被同一个进程中的另一个线程使用(支持多线程),这个值应该是 true,否则为 false。
  • multiprocess 如果等价的应用对象支持被另一个进程同时使用(支持多进程),这个值应该是 true,否则是 false
  • run_once 如果服务器/网关希望应用对象在包含它的进程中仅执行一次这个请求,它的值为 true。正常情况下,只有是基于 CGI 的网关时,这个值才是 true。

最后,environ字典也可能包含一些服务器定义的变量。这些变量用小写字母、数字和 “.”、”_”命名。并且使用一个唯一的前缀。例如 mod_python 可能应以一个变量为 “mod_python.some_variable”。

输入流和错误流

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

Method Stream Notes
read(size) input 1
readline() input 1,2
readlines(hint) input 1,3
iter() input
flush() errors 4
write(str) errors
writelines(seq) errors

 

每个方法的含义都可以在标准库文档中查找。除了下表中列出的几点:

  • 服务器不需要读取超过客户端指定的 Content-Length的内容,而如果应用对象尝试读取超过 Content-Length长度的内容,服务器应该模拟已经读到文件结束。应用对象不应该尝试读取超过 CONTENT_LENGTH 变量指定长度的内容。(所以,两端读取都要克制,并且服务器要处理应用端读取过长的情况)。服务器应该支持不带参数地调用 read(),这时返回客户端输入流的全部剩余内容。当从空的输入流或者读完的输入流再次读时,服务器应该返回空字符串(当然是字节字符串了。)
  • 服务器应该让 readline() 支持可选参数 size。但是在 WSGI 1.0的时候,服务器是可以不支持的。(在 WSGI 1.0中,由于在实现上比较复杂,实际上也很少使用,size 参数不被支持。后来 cgi 模块开始使用 size 参数,所以实际的服务器不得不用任何办法去支持 size。)
  • readlines()的 hint 参数四可选的,不管是调用方还是实现方。应用对象可以自动决定是否实现它,服务器/网关也可自由地决定是否忽略它。
  • 由于错误流一般不能倒回(重读),服务器/网关自由地推进写操作,不带缓冲地。这种情况下,flush()方法相当于 空操作(noop)。然而,可移植的应用不能假设输出是无缓冲的或者 flush()是空操作。他们必须调用 flush(),如果他们需要确认输出已经被写入。(例如,最小化从多个进程写入相同错误日志的混合数据)

符合 WSGI 规范的服务器都必须实现上表中的方法。符合 WSGI 规范的应用不能使用上表所列以外的方法或者属性。特别地,应用对象不能尝试关闭输入流和错误流,即使应用对象可以调用 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」等。也就是说,它是一个由状态码和原因短句组成的字符串,状态码在前,原因短句在后,中间用一个空格分隔,没有其他空白字符或者其他内容。这个字符串绝对不能使用控制字符,不能用回车、换行或者他们的组合作为结尾。

response_headers参数是一个形如(header_name, header_value) tuple的列表。而且必须是 Python 的 list 实例,执行 type(response_headers)返回 ListType。服务器可以用任何需要的方法修改它。每一个 header_name 必须是一个不含冒号或其他标点符号的合法的 HTTP header 字段名。(在 RFC 2616, 4.2节定义)

每一个header_value必须不能使用控制字符,包括回车或者换行,不管是在中间还是结尾。(这些要求是为了将那些必须检查或修改响应头的服务端和中间件所必须执行的解析工作的复杂性降到最低。)

通常来说,服务器/网关负责确认发送给客户端的Header的正确性:如果应用对象忽略了 HTTP 需要的Header(或其他类似的内容),服务器/网关必须添加上。例如:「Date:」和「Server:」通常由服务器/网关提供。

(服务器/网关作者请注意:HTTP Header的名字是大小写敏感的,在检查应用对象提供的Header名称时请注意这一点。)

应用和中间件禁止使用 HTTP/1.1 中的「hop-by-hop」机制或者Header,HTTP/1.0 中类似的机制也禁用,任何应用客户端到 Web 服务器的连接持久性的Header也都不允许使用。 使用这些特性是服务端的特权,服务端发现客户端违反此规定时应视为致命错误,需在请求提交到 start_response() 时报错。(关于「hop-by-hop」特性和Header,请参见「其他 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 头,服务器不应该传输多于Content-Length指定长度的内容给客户端,而是当发送了足够的内容后,停止对 response 迭代;或者当应用对象尝试写超过长度的内容时抛出异常。(当然如果应用对象没有提供它用Content-Length声明的长度时,服务器应该关闭连接,将错误写入日志,或者报告此错误)

如果应用对象没有提供Content-Length头,服务器/网关一下方法之一来提供这个头。最简单的方法是当响应结束时关闭客户端连接。

然而,在某些情况下,服务器/网关可能能够生成Content-Length头,或者至少避免关闭客户端连接。如果应用对象没有调用 write(),并且返回长度为1的可迭代对象,那么服务器应该,通过这个可迭代对象生成的第一个字节字符串的长度,自动地检测/设置Content-Length。

如果服务器和客户端都支持 HTTP/1.1 的 chunked encoding,服务器必须使用 chunked encoding来发送每次 write()的一个 chunk,或者从可迭代对象 yield 的字节字符串,因此,服务器为每个 chunk生成一个 COntent-Length 头。这允许服务器保持客户端连接 alive,如果服务器希望这么做。这么做时服务器必须完全遵守 RFC 2616,或者返回到处理Content-Length缺失的策略之一。

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

缓冲和流

通常来说,应用会通过缓冲(合适的大小)输出和一次发送来实现最佳生产能力。这在已经存在的框架(如 Zope)中是常见方法:用 StringIO或类似对象缓冲输出,然后和响应头一起,一次发送。在 WSGI 中,对应用对象来说,相应的方法是:简单地返回单个可迭代对象,其包含了整个响应体(单个字节字符串)。对各种呈现 HTML 页面(页面文本很容易存放在内存里)的应用功能来说,这是推荐的方法。然而,对大文件,或者 HTTP 流的特殊用法(如多部分服务器推送)来说,应用可能需要提供小块输出(避免将大文件加载到内存中)的功能。有时也会遇到响应的部分内容比较耗时,但是如果先发送已经准备好的部分内容将是有用的。

上面这些情况,应用对象通常返回一个迭代器(经常是一个生成器迭代器),产生一块一块的输出。这些块可能因为多部分边界(如服务器推送)或者耗时任务结束(从磁盘文件读取一个块)之前的原因被分开。WSGI 服务器、网关和中间件不能延迟任何块的发送;他们必须要么完整地传输块到客户端,要么保证在应用对象生产下一个块的同时继续发送数据。服务区/网关或者中间件必须用下面三种方法之一提供这种保证:

  • 返回到应用对象之前发送整个块到操作系统。(刷新 OS 缓冲区)。
  • 应用对象生产下一个数据块的同时,用另一个线程继续发送数据。
  • (仅适用中间件)发送这个数据库到上一级服务器/网关。

通过这种保证,WSGI使得应用对象能够在输出数据的任何地方不停止数据的传输。这对一个功能是极为重要的,如服务器推送流,在multipart boundaries之间的数据应该完整发送到客户端。

处理块边界的中间件

为了更好地支持异步应用和异步服务器,中间件组件不能为了等待从可迭代应用对象获得更多数据而阻塞块的迭代。在中间件组件可以产生任何输出之前,如果中间件组件需要积累更多的数据,它必须先 yield 一个空字符串。换言之,每次下层应用产生一个值时中间件都必须相应yield至少一个值。 如果中间件不能yield任何有意义的值,则yield一个空的字节流字符串。这个规定保证了异步应用和服务器能够合作,以减少同时支持固定数量应用实例所需的线程数量。

值得注意的是,这要求中间件必须在下层应用返回一个迭代器时也立即向上返回一个迭代器。中间件禁止使用 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()),从而使中间件能够捕获和修改错误信息。这些技术要求开发者遵循如下规范:

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

HTTP 1.1 Expect/Continue

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

  • 对于任何「Expect: 100-continue」的请求返回一个即时的「100 Continue」响应,然后正常继续运行。
  • 继续正常运行,但是提供给应用一个input 流,这个流会在应用第一次尝试读取输入流的时候发送「100 Continue」响应。 读请求之后必须阻塞,直到客户端响应为止。
  • 阻塞请求直到客户端意识到服务器不支持 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 称之为「hop-by-hop」 Header的特性不适用与 WSGI 的内部通信。 WSGI 应用不能生成任何「hop-by-hop」Header,不能使用任何需要生成该Header的 HTTP 特性,也不能依赖于 environ 字典中的任何传入的「hop-by-hop」Header。 WSGI 服务端必须自己处理任何传入的能够支持的「hop-by-hop」Header,比如对传入的 Transfer-Encoding 进行解码,如果可用,包含 chunked 编码等。

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

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

请注意,这些限制条件并不是要求应用把每一个 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 而不是Header集合或任何其他合适的方式拒绝为应用服务。

类似的例子还有如果扩展 API 提供写入响应数据或Header的功能,它必须要求应用在使用扩展功能之前传入 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重组

如果应用希望重组一个请求的完整 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 可能不是客户端请求的那个 URL,比如服务器重写规则可能会修改客户端请求的原始 URL 以使其符合规范。

支持更早版本(<2.2)的Python

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

QA问答

1.为什么evniron必须是字典?用它的子类会有什么问题呢?

用字典的原理是为了最大化满足在服务器间的移植性。另一种选择就是定义一些字典的子集,用字典的方法作为标准的便捷的接口。然而事实上,大多服务器可能只需要找到一个字典就足够了,并且框架的作者会预计完整的字典特性可用,因为多半情况是这样的。但是如果一些服务器选择不使用字典,那么将可能会有互用性的问题出现尽管服务器给出了一致性的规范。因此使用强制的字典简化了规范和互相的验证确保。

注意这些不妨碍服务器或框架开发者向evnrion字典里加入自定义的变量来提供特别的服务。这是提供任何value-added服务推荐的方式。

2.为什么你能执行write()并且yield 字符串/返回一个迭代器?我们不应该选择一种做法吗?

如果我们仅仅使用迭代的做法,那么现存的框架面临可用的”push”被恶化。如果我们只支持通过write()推送,那么服务器在传输大文件的时候性能将恶化(如果一个工作线程没有将所有的output都发送完成,那么将无法进行下一个新的request请求)。因此,这种平衡允许应用程序支持两个方法,视情况而定,比起需要push-only的方式来说,只给服务器实现者一点负担。

3.close()是什么?

当writes在应用程序执行期间完成,应用程序可以通过try/finally块来保证资源被释放。但是如果应用程序返回一个迭代,直到迭代器被垃圾收集器收集前任何资源都不会被释放。约定俗成的close()允许应用程序在结束一个request请求时释放临界资源,并且它向前兼容于在PEP 325中被提议的在迭代器中的try/finally.

4.为什么这个接口如此低级?我如果想加入一些特性呢(例如cookies, sessions, persistence,…)

这并不是另一个python的web框架,这仅仅是一个框架和web服务器之间通信的方法,反之亦然。如果你想使用这些特性,你需要选择一个提供这些你想要的特性的框架。并且如果这些框架让你创建一个WSGI应用程序,你应当能让它在大部份支持WSGI的服务器上运行。同样,一些WSGI服务器可能通过在他们提供的evniron字典里的对象来提供一些额外的服务,可以参阅这些服务器适当的文档了解详情。(当然,使用这样扩展的应用程序将无法移植到其他的WSGI-based服务器上)

5.为什么使用CGI的变量代替好的老HTTP头,并且为什么让他们和WSGI定义的变量混合在一起呢?

许多现存的框架很大程序上是建立在CGI规范的基础上的,并且现存的web服务器知道如何生成CGI变量。相比之下,一些可选的方式来呈现HTTP信息会是分散破碎的并且缺乏市场。因此使用”CGI标准”看起来是个好的办法来利用现有的实现。对于将他们同WSGI变量混合在一起,分离他们的话会导致需要两个字典参数需要传入,这样做没什么好处。

6.关于状态字符串,我们可不可以仅仅使用数字来代替,像用200代替”200 OK”?

这样做会使服务器或网关被复杂化,因为他们需要一个字符串与数值的映射表。相比之下,应用程序或框架作者在他们输入一些额外的信息到他们处理细节的响应代码中要简单,并且现存的框架时常有一个表包含这些必要的信息。所以,总体来说这个让应用程序/框架来负责要比服务器或网关来负责要好。

7.为什么wsgi.run_once不能保证app只运行一次?

因为它只不过是建议应用程序应当”装配稀少地运行”。这是用于应用程序框架用不同的模式来操作缓存,sessions等待。在”multiple run”模式下,这样的框架可能会预先装载缓存,并且在每个request请求之后不会写操作,如写入logs或session数据到硬盘上。在”single run”模式下,这样的框架避免预加载,避免每个request请求之后flush所有必要的写操作。然而,为了验证在后者的模式下应用程序或框架的正确动作,可能会必要地(或是权宜之计)调用它不止一次。因此,一个应用程序应当不能仅仅因为设置了wsgi.run_once为True就假定它肯定不会再运行,

8.Feature x(dictionaries, callables, etc.)对于应用程序代码来说是很丑陋的,我们可以使用对象来代替吗?

WSGI所有的这些选择实现都是为了从另一个特性中解耦合;重新组合这些特性到一个封装的对象中会使更难写服务器/网关,并且会使书写中间件来代替或修改一部分整体的功能的难度上一个数量级。

本质上,中间件希望有个”责任链”模式,凭这个对于一些功能来说它可以看作是一个”handler”,而让其他东西保持不变。这用普通的python对象是非常难的,如果这接口是保持可扩展性的。例如,一个必须使用__getattr__或者__getattribut__的覆盖,来确保这些扩展(比如一个WSGI未来的版本定义的一个变量)是被通过的。这种类型的代码是出了名的难以保证100%正确,有些人可能会自己重写。他们会因此复制其他人的实现,但是从其他已经正确的案例中复制过来的时候却未能成功更新。

进一步讲,这样必要的样本将纯碎excise,一个开发者为了能让应用程序框架得到更漂亮的API而向中间件开发者支付税务。但是应用程序框架开发者会通常只升级一个框架用来支持WSGI,并且对于框架整体来说只是非常小的一部分。这可能会成为他们第一次(甚至是最后一次)的WSGI实现,并且因此他们可能实现这些规范起来伸手可及。因此,努力使使用对象属性创造出来的API漂亮这类的做法,可能会遗失掉这些支持者。我们鼓励那些希望有漂亮的(或者是改进的)WSGI接口的人,对直接开发web应用程序项目(与web框架开发)的用户开发为方便应用程序开发者而包装过WSGI的API或框架,通过这种方式,WSGI可以保持对服务器或中间件的便利性,而不是对应用程序作者”丑陋”。

参考链接:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注