WSGI规范(PEP 3333) 第三部分(实现)

服务器扩展 API

有些服务器作者可能希望暴露更高级的 API,那样的话,应用或框架的作者可以有一些特殊的用途。例如:一个基于 mod_python 的网关可能希望暴露一部分 Apache 的 API 作为 WSGI 的扩展。

最简单的情况下,需要的不比定义一个environ 变量多,例如 mod_python.some_api。但是,在大多数情况下,中间件的存在使得这件事变得复杂。例如访问相同的、出现在环境变量里的 HTTP 头的某个 API,如果环境变量被中间件修改,就可能返回不同的数据。

通常来说,任何复制、替代或者绕过 WSGI 部分功能的扩展 API,可能有和中间件组件无法兼容的风险。服务器/网关的开发者们不应该假设没有人用中间件,因为一些框架开发者尤其要想使用各种各样的中间件来组织(重新组织)他们的框架。

有些服务器/网关提供了扩展 API 来替换某些 WSGI 功能。为了保持最大化的兼容性,这些服务器/网关必须好好设计这些 API。例如,某个访问 HTTP 请求头的扩展 API 必须要求应用对象将它的当前环境作为参数传入,这样,服务器/网关可以验证被这个扩展 API 访问过的 HTTP 头是否被中间件修改过。如果这个扩展 API不能保证总是和 HTTP 头的环境变量一致,它就必须拒绝为应用对象服务,可能抛出异常、返回 None ,却不会返回 HTTP 头或其他东西。

类似地,如果扩展 API 给出了另一个有关写入响应数据或响应头的含义,在应用对象使用这个扩展API之前,应该将 start_response()传入本扩展 API。如果传入的对象和服务器/网关原本传给应用对象的不是同一个,扩展 API 就不能保证正确的操作,必须拒绝向应用对象提供本扩展服务。

以上指导方针也适用于添加内容的中间件,如解析 cookie、表单变量、session 和与 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)

但是,已经存在的应用和框架可能仅仅需要一个简单的配置选项,就可指明应用或框架需要的配置文件的位置。(当然,应用应该缓存这份配置,以避免每次执行请求过程都要读取一次)

重组 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 修改成规范的形式。

对低于 Python版本低于2.2的支持情况

由于现实几乎不再使用低于2.2的 Python,本小节不做翻译。但留下原文,以备需要。

Some servers, gateways, or applications may wish to support older (<2.2) versions of Python. This is especially important if Jython is a target platform, since as of this writing a production-ready version of Jython 2.2 is not yet available.

For servers and gateways, this is relatively straightforward: servers and gateways targeting pre-2.2 versions of Python must simply restrict themselves to using only a standard "for" loop to iterate over any iterable returned by an application. This is the only way to ensure source-level compatibility with both the pre-2.2 iterator protocol (discussed further below) and "today's" iterator protocol (see PEP 234 ).

(Note that this technique necessarily applies only to servers, gateways, or middleware that are written in Python. Discussion of how to use iterator protocol(s) correctly from other languages is outside the scope of this PEP.)

For applications, supporting pre-2.2 versions of Python is slightly more complex:

  • You may not return a file object and expect it to work as an iterable, since before Python 2.2, files were not iterable. (In general, you shouldn't do this anyway, because it will perform quite poorly most of the time!) Use wsgi.file_wrapper or an application-specific file wrapper class. (See Optional Platform-Specific File Handling for more on wsgi.file_wrapper , and an example class you can use to wrap a file as an iterable.)
  • If you return a custom iterable, it must implement the pre-2.2 iterator protocol. That is, provide a __getitem__ method that accepts an integer key, and raises IndexError when exhausted. (Note that built-in sequence types are also acceptable, since they also implement this protocol.)

Finally, middleware that wishes to support pre-2.2 versions of Python, and iterates over application return values or itself returns an iterable (or both), must follow the appropriate recommendations above.

(Note: It should go without saying that to support pre-2.2 versions of Python, any server, gateway, application, or middleware must also use only language features available in the target version, use 1 and 0 instead of True and False , etc.)

平台特定的文件处理

一些平台提供高性能文件传输设施,如 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,wsgi.file_wrapper 必须是一个可调用对象,接受一个必须的位置参数和一个可选的位置参数,第一个参数是类文件对象,第二个参数是可选的块尺寸(服务器/网关不会使用它)。这个可调用对象必须返回可迭代对象,并且不能执行任何数据传输,直到,或者除非服务器/网关从应用对象接收到这个可迭代对象。(To do otherwise would prevent middleware from being able to interpret or override the response data.)


因为是一个类文件对象,这个由应用对象提供的对象必须实现 read()方法(接受一个可选的大小参数)。此对象可能提供close()方法,如果提供了,由 wsgi.file_wrapper 返回的可迭代对象必须也有 close()方法,可迭代对象的 close()方法内部调用 类文件的 close()方法。如果这个类文件对象拥有其他名字和 Python 内建文件对象一样的属性\方法,wsgi.file_wrapper 必须假定这些属性\方法和内建文件对象的语义相同。

特定平台的文件处理的实例化,必须发生在应用返回之后。服务器/网关会检查是否返回了一个包装后的对象。(再一次说明,由于中间件的出现,错误处理等文件类操作,不能保证创建的包装器对象会被使用。)(译者:有可能在中间件层被替换了。)


除了 close()的处理,从应用对象返回的类文件包装器的语义,应该和应用对象已经返回的 iter(flielike.read, '')一样。换句话说,发送数据应该从发送开始时类文件对象的当时位置开始。,直到到达文件末尾,或者直到发送了Content-Length指定的长度。(如果应用对象没有提供Content-Length,服务器可能会利用这个文件 生成一个Content-Length)
当然,平台特定的文件传输 API通常不接受随意的一个类文件对象,因此, wsgi.file_wrapper不得不内省这个对象,看看是否具有 fileno()(Unix 类平台)或java.nio.FileChannel (Jython),确保这个类文件对象适合这个平台特定的文件 API。

即使这个类文件对象不适合这个平台 API,wsgi.file_wrapper 必须仍旧返回支持 read()和close()的可迭代对象,这样一来,使用文件包装器的应用可以跨平台移植。以下是一个简单的平台未定的文件包装器类:

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


译者:花了相当大的精力,整个 PEP 3333的主体内容已经完成了。对 WSGI、框架、服务器和中间件的理解也加深了一层。这很好!但是规范本身是细致入微的。对我们这些使用者来说(就是规范里说的应用的作者,除非你也想写一个像 Gunicorn 的服务器,或者一个新的 WSGI 框架),这种细致入微就是没有太大必要了,一个阐释到一定层次的 WSGI 文章足够,如果将现实中某个服务器、中间件、框架的实现作为示例,不管是对 WSGI 本身,还是对那个用作示例的框架的理解,都是更好的。可惜,现在能够找到的资料都没有达到这个层次。所以我会再花些时间,解读一下服务器、框架有关 WSGI 部分的内容。相信到时候,会对 WSGI 有更深层的理解!


WSGI规范(PEP 3333) 第一部分(概述)

WSGI规范(PEP 3333) 第二部分(细节)

WSGI规范(PEP 3333) 第四部分(常见问题)

发布于 2017-07-29

文章被以下专栏收录