[译]pep 3333 -- Python Web 服务器网关接口 v1.0.1

文章目录
PEP: 3333
Title: Python Web Server Gateway Interface v1.0.1
Author: P.J. Eby
Discussions-To: Python Web-SIG <web-sig at python.org>
Status: Final
Type: Informational
Created: 26-Sep-2010
Post-History: 26-Sep-2010, 04-Oct-2010
Replaces: 333
Source: PEP 3333 – Python Web Server Gateway Interface v1.0.1
译文: https://zhuanlan.zhihu.com/p/27600327

概述

WSGI 的全称是:Web Server Gateway Interface,介于 web server 和应用 server 之间的接口规范。当使用 Python 进行 web 开发时,要深刻理解 Django、Flask、Tornado 等 web 框架,WSGI 是你绕不过去的槛儿。为了方便中文世界的同行,我粗略地将规范进行翻译。不周之处,请联系我,定当不遗余力地予以修正。

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

PEP 3333 是 PEP 333 的升级版本,进行了略微修改以提高在 Python 3 下的可用性。同时,合并了几点长期存在的、实际的修改。(代码示例也已经兼容 Python 3)

尽管出于程序原因[6],这必然是一个独特的 PEP,但没有进行任何更改以使以前符合 Python 2.x 的服务器或应用程序失效。 如果您的 2.x 应用程序或服务器符合 PEP 333,则它也符合此 PEP。

当然,在使用 Python 3 时,你的 App 或者 server 必须遵守以下规则:有关字符串类型的说明Unicode 。下文有这两点的详细描述。

摘要

这篇文章提出了一个介于 web 服务器和 Python web 应用或者框架之间的、建议的标准接口,以推进 python web 应用和各种 web 服务器之间的良好兼容。

基本原理和目标(PEP 333)

在 Python 中出现了各种 Web 应用框架,如 Zope、Quixote、Webware、SkunkWeb、PSO、和 Twisted Web 等。对 Python 新手来说,众多的框架是一个问题。通常来说,他们的选择会限制他们对 Web 服务器等的选择。

相比之下,尽管 Java 也存在众多 web 开发框架,servlet API 出现之后,使得用任何 Java web 框架编写的应用运行在任何支持 servlet API 的 web 服务器上成为可能。

服务器中这种针对 Python 的 API 的使用和普及——不管服务器是用 python 写的(如: Medusa),还是内嵌 python(如: mod_python),亦或是通过一种网关协议来调用 Python(如:CGI, FastCGI 等),把人们从 web 框架的选择和 web 服务器的选择中剥离开来,使他们能够任意选择适合自己的组合,而 web 服务器和 web 框架的开发者们也能够把精力集中到各自的领域。

因此,这个 PEP 提出了一套简单而通用的、介于 web 服务器和 web 应用或框架之间的接口:Python WSGI。

但是,仅仅存在一个 WSGI 规范起不到什么作用,只有 web 服务器和框架的作者们和维护者们实际去实现 WSGI 规范才行。

由于现在没有支持 WSGI 的 web 服务器和框架,对实现 WSGI 支持的作者来说,几乎没有快速的回报。因而,WSGI 必须易于实现,实现此接口的作者的早期投入才会比较低。

因此,此接口在 web 服务器和框架中实现的简单性就极为重要了,也是做任何设计决策的重要原则。

需要指出,对框架作者的实现简单性和 web 应用的开发者的实现简单性不是同一件事。对框架作者来说,WSGI 是一个完全没有余饰的接口(不多也不少),因为像 Response 对象和 cookie 处理等用框架已经存在的方式处理即可。再次说明,WSGI 的目标是辅助已经存在的 web 服务器和 web 应用或框架交互,而不是发明一个新的 web 框架。

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

除了使已经存在和以后的框架和服务器易于实现此规范,WSGI 接口对于创建请求预处理器(request preprocessors),响应后处理器(response postprocessors)以及其他基于 WSGI 的中间件组件(middleware)也是容易实现的。这些中间件组件对包含它们的服务器来说就像应用,对它们包含的应用来说就像服务器。

如果中间件可以做到既简单又稳定,WSGI 也广泛地被服务器和框架实现,那么完全崭新的 python web 框架的出现便成为可能,这种框架由松耦合的 WSGI 中间件组成。确实,已存在框架的作者甚至可能用这种新的方式重构框架,使其变成一个更像使用 WSGI 的库,而不是一个庞大的框架。这种变化使应用开发者为特定的功能选择最佳方案,而不需要提交一个简单框架的所有优势和劣势。

当然,到写这篇文稿的时间,那一天无疑还是遥远的。WSGI 的最短期目标就是:可以在任何 web 服务器上使用任何框架。

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

规范概述

WSGI 接口有服务端和应用端两部分:服务端也可以叫网关端,应用端也叫框架端。服务端调用一个由应用端提供的可调用对象。如何提供这个对象,由服务端决定。例如某些服务器或者网关需要应用的部署者写一段脚本,以创建服务器或者网关的实例,并且为这个实例提供一个应用实例。另一些服务器或者网关则可能使用配置文件或其他方法以指定应用实例应该从哪里导入或获取。

除了这些比较纯的服务器/网关 和应用/框架,我们还可以创建实现了此规范两端的中间件组件。这个中间件对服务器来说像是应用,对它所包含的应用来说像是服务器。中间件可以用来提供扩展 API、内容转换、导航等其他有用的功能。

纵观整个规范,术语『可调用对象』(a callable)可能代表函数、方法、类或者实现了__call__方法的实例。这取决于服务器、网关或者应用选择哪种实现技术。相反,正在调用可调用对象的服务器,网关或应用程序必须不依赖于提供给它的可调用类型。可调用对象仅仅是被调用,不会内省自己。

字符串类型

一般来说,HTTP 处理的是字节,这意味着 WSGI 规范也要处理字节。然而,这些字节内容往往有某种文本解释。在 Python 中,字符串是处理文本最方便的方式。

但是在很多 Python 版本和实现中,字符串是 Unicode,不是字节(bytes)。这需要在可用的 API 和 HTTP 上下文中的字节和文本之间正确转换之间谨慎地保持平衡……尤其是要支持在具有不同str类型的 Python 实现之间移植代码。

因此 WSGI 定义了两种“字符串”(string):

  • 原生字符串(总是使用 str 来实现)用于请求/响应 的头部(headers)和元数据(metadata)
  • 字节字符串(在 Python3 中用 bytes 实现,其他版本中用 str 实现)用于请求/响应的数据部分(如 POST/PUT 的输入数据,HTML 的输出内容等)。

不要弄混了:即使 Python 的 str 实际上是 unicode 字符,原生字符串内容也必须支持通过 Latin-1 转换成字节码。

简而言之:当你在本文档中看到 string 时,它代表『原生』字符串,例如一个 str 类型的对象,不管它内部实现上用的是字节还是 unicode。当你看到 bytestring,应该视作一个在 python3 中的 bytes 对象, 在 python2 中的 str 对象。

所以,尽管在某种意义上,HTTP 就是很像字节,有多个方便的 API 可以将其转换成 Python 的默认 str 类型。

应用/框架端

应用对象(application object)就是一个简单的接受两个参数的可调用对象。不要混淆术语”object”就真的是一个对象实例。python 中的函数、方法、类、实现了__call__的实例都是可以接受的。应用对象必须可以被多次调用,因为实际上所有服务器/网关(除了 CGI 网关)都会重复地调用它。

注意:我们总是讲“应用”对象,不要误解为应用开发者需要使用 WSGI 作为 web 编程 API!应用开发者可以继续使用已经存在的、高级框架服务去开发他们的应用。WSGI 是一个为框架开发者和服务器开发者准备的工具,应用开发者不需要直接使用 WSGI。

下面是两个应用对象(application object)的示例。一个是函数(function),一个是类(class):


HELLO_WORLD = b"Hello world!\n"

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

class AppClass:
"""产生相同的输出,但是用类实现。

(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 消息文本』的状态字符串。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变量

environ 字典必须包含 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。 Note, however, that an application that uses any CGI variables other than the ones listed above are necessarily non-portable to web servers that do not support the relevant extensions. (For example, web servers that do not publish files will not be able to provide a meaningful DOCUMENT_ROOT or PATH_TRANSLATED .)

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

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

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

wsgi.version 使用 tuple (1, 0) , 表示 WSGI 的版本是 1.0
wsgi.url_scheme 表示 URL 的模式部分,应用对象将以这个模式被调用。一般情况下,值为”http”或者”https”。

wsgi.input 一个输入流(类文件对象),从这个对象读取 HTTP 请求体。(当被应用对象请求时,服务器/网关执行 read ,或者预读取请求体,缓存它到内存或者磁盘中,或者用输入流提供的其他技术)
wsgi.errors 错误输出流(类文件对象), 日志记录程序或其他标准的/中心位置的目的。它应该是一个文本模式的流。应用对象使用”\n”作为行结束符,而服务器/网关会将它转换成相应的输出值。

(在一些平台上,str 是 unicode 字符。错误输出流应该能接受并记录普通 unicode 而不抛出异常。然后允许用流的编码转换不能显示的 unicode 字符。)

对许多服务器来说,wsgi.errors 应该是主错误日志。另一个选择是 sys.stderr,或者某个日志文件。服务器文档应该包含如何配置错误日志文件的说明。服务器/网关可能支持错误流到不同的日志处理应用上去,如果这是希望的。
wsgi.multithread 如果应用对象能够同时被同一个进程中的另一个线程使用(支持多线程),这个值应该是 true,否则为 false。
wsgi.multiprocess 如果等价的应用对象支持被另一个进程同时使用(支持多进程),这个值应该是 true,否则是 false
wsgi.run_once 如果服务器/网关希望应用对象在包含它的进程中仅执行一次这个请求,它的值为 true。正常情况下,只有是基于 CGI 的网关时,这个值才是 true。

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

输入流和错误流

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

方法 流 说明

read(size) input 1

readline() input 1,2

readlines(hint) input 1,3

__iter__() input

flush() errors 4

write(str) errors

writelines(seq) errore

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

服务器不需要读取超过客户端指定的 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)(所有 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 头名称(在 RFC 2616, 4.2 节定义),结尾没有冒号或者其他符号。

每一个 header_value 必须不能使用控制字符,包括回车或者换行,不管是在中间还是结尾。(这个要求是为了最小化服务器/网关和响应处理器(需要检查或修改响应头)解析的复杂度)

通常来说,服务器/网关负责确认发送给客户端的头的正确性:如果应用对象忽略了 HTTP 需要的头,服务器/网关必须添加上。例如:『Date:』和『Server:』通常由服务器/网关提供。

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

禁止应用对象和中间件使用 HTTP 1.1 的 hop-by-hop 特性或者头,任何在 HTTP/1.0 中等价的特性,或任何影响客户端到 web 服务器端连接的持久化的头。这些特性对某些服务器是专用的,排他的。如果应该对象尝试发送这样的头,服务器/网关应该视作一个致命错误,如果 start_response()提供了这样的头,服务器/网关应该抛出错误。(更多有关 hop-by-hop 特性的介绍,请查看 其它 HTTP 特性 一节)

调用 start_response() 的时候,服务器应该检查响应头中的错误,这样可以在应用执行时将错误抛出。

However, the start_response callable must not actually transmit the response headers. Instead, it must store them for the server or gateway to transmit only after the first iteration of the application return value that yields a non-empty byte string, or upon the application’s first invocation of the write() callable. In other words, response headers must not be sent until there is actual body data available, or until the application’s returned iterable is exhausted. (The only possible exception to this rule is if the response headers explicitly include a Content-Length of zero.)

This delaying of response header transmission is to ensure that buffered and asynchronous applications can replace their originally intended output with error output, up until the last possible moment. For example, the application may need to change the response status from “200 OK” to “500 Internal Error”, if an error occurs while the body is being generated within an application buffer.

如果提供了 exc_info 参数,其值必须是 Python 的 sys.exc_info()返回的 tuple。只有 start_response()被错误处理程序调用时,这个参数才会提供,并且是有应用对象提供。如果 提供了 exc_info,并且还没有 HTTP 头输出,start_response 应该用新的 HTTP 头替换当前缓存的 HTTP 头。因此,当错误发生时允许应用对象改变输出内容。

然而,如果提供了 exc_info,而已经有 HTTP 头发送,start_response()必须抛出异常,并且应该使用 exc_info tuple 重新抛出。用代码表示:

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

这会重新抛出由应用对象产生的异常,原则上应该终止应用的执行。(对应用对象来说,一旦 HTTP 头已经发送出去,再尝试向浏览器发送错误输出是不安全的。)如果应用对象调用 start_response()时传入了 exc_info,应用对象不应该再抛出任何异常。相反,它应该允许这样的异常扩散给服务器/网关。查看下面的 错误处理 一节有更详细的信息。

应用对象可能调用 start_response()不止一次,当且仅当提供了 exc_info 的时候。更精确地讲,当 start_response()已经被应用对象调用时,再次调用 start_response()而没有传入 exc_info 参数,这是一个致命错误。这包括第一次调用 start_response()时抛出了错误。(查看上一篇文章中有关 CGI 的代码示例中相关逻辑)

注意:为了避免对 trackback 和 frame 发生循环引用,服务器、网关、实现了 start_response 的中间件应该确定在 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 缺失的策略之一。

(应用和中间件必须不能提供任何 Transfer-Encoding 到输出,例如 chunk 或者 gzip;和 hop-by-hop 操作类似,这些编码是实际 web 服务器/网关的特别处理,查看 其他 HTTP 特性 获取详细信息)

缓冲和流

通常来说,应用会通过缓冲(合适的大小)输出和一次发送来实现最佳生产能力。这在已经存在的框架(如 Zope)中是常见方法:用 StringIO 或类似对象缓冲输出,然后和响应头一起,一次发送。

在 WSGI 中,对应用对象来说,相应的方法是:简单地返回单个可迭代对象,其包含了整个响应体(单个字节字符串)。对各种呈现 HTML 页面(页面文本很容易存放在内存里)的应用功能来说,这是推荐的方法。

然而,对大文件,或者 HTTP 流的特殊用法(如多部分服务器推送)来说,应用可能需要提供小块输出(避免将大文件加载到内存中)的功能。有时也会遇到响应的部分内容比较耗时,但是如果先发送已经准备好的部分内容将是有用的。

上面这些情况,应用对象通常返回一个迭代器(经常是一个生成器迭代器),产生一块一块的输出。这些块可能因为多部分边界(如服务器推送)或者耗时任务结束(从磁盘文件读取一个块)之前的原因被分开。

WSGI 服务器、网关和中间件不能延迟任何块的发送;他们必须要么完整地传输块到客户端,要么保证在应用对象生产下一个块的同时继续发送数据。服务区/网关或者中间件必须用下面三种方法之一提供这种保证:

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

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

处理块边界的中间件

为了更好地支持异步应用和异步服务器,中间件组件不能为了等待从可迭代应用对象获得更多数据而阻塞块的迭代。在中间件组件可以产生任何输出之前,如果中间件组件需要积累更多的数据,它必须先 yield 一个空字符串。

用另一种形式表达的话,在每次应用对象 yield 一个值的时候,中间件组件必须 yield 一个值。如果中间件组件不能 yield 其他值,那就 yield 一个空字符串。

这个需求确保异步应用和服务器一起,可以减少同时运行给定数目应用对象实例的线程数

这个需求也意味着在应用对象返回可迭代对象时,中间件组件也必须返回可迭代对象。对中间件组件来说,使用 write()发送来自下层应用 yield 的数据是?禁止的。中间件可能仅仅能够使用它们父一级的服务器的 write()来发送下一级应用对象使用中间件提供的 write()方法 yield 的数据。(译者:因为多了中间件,情况变得略微复杂,文字描述台抽象,把图画出来就额容易理解了。)

write() 可调用对象

一些已经存在的应用框架 API 支持非缓冲输出,和 WSGI 的行为不太一样。尤其是,他们提供了 某种 write 函数或方法来写非缓冲块数据,或者提供一个缓冲的 write 函数和一种 flush 机制来刷新缓冲。

不幸的是,这些 API 不能实现 WSGI 定义的可迭代应用,除非使用线程或其他特殊的机制才行。

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

(译者:以上说明了 WSGI 为什么要提供 write())

新的 WSGI 应用和框架不应该再使用 write(),如果他们能够避免使用 write()。实际上,write()是一种 hack 技术,为了支持哪些已经存在的必要的流式 API。通常来说,应用通过迭代它的返回值就可以产生输出,对 web 服务器来说,这使得在同一个 Python 线程中插入其他任务成了可能,潜在地,从整体来说,也提供了更好的生产能力。

write()由 start_response()返回,它接受单个参数:作为 HTTP 响应体写入的字节字符串,被视作它好想从输出的可迭代对象 yield 出来的。换句话说,在 write()返回之前,它必须确认传递进来的字节字符串已经发送到了客户端,或者被缓冲起来等待应用对象进一步处理时发送出去。

应用对象必须返回可迭代对象,即使它用 write()产生所有或部分响应体数据。返回的可迭代对象可能是空(yield 空内容),但是如果它 yield 非空字节字符串,通常被服务器或者网关视为输出数据(数据必须立刻发送出去或进入队列)。应用不能在它返回的可迭代对象内部调用 write(),因此全部传递给 write()的字节字符串被发送给客户端之后,任何由可迭代对象 yield 的字节字符串才被发送出去。

Unicode 问题

HTTP 不直接支持 Unicode。本规范也不支持。所有的编码/解码必须有应用处理;所有传递给服务器的字符串和从服务器返回的字符串都必须是 python2 的 str 或者 python3 的 bytes,不是 Unicode。在需要字符串的地方使用 unicode 的结果为定义。

也请记着,床底给 start_response()的字符串,不管是用于 status 还是响应头,都必须符合 RFC 2616 指定的编码。也就是说,它们必须是 ISO-8859-1 字符,或者使用 RFC 2047 MIME 编码。

一些 Python 平台,str 或 StringType 实际上是基于 Unicode 的,比如 Jython、IronPython、Python 3 等。这个规范提到的字符串都必须是 ISO-8859-1 编码的(从\u0000 到\u00FF)。对应用来说,提供包含 Unicode 字符或者不属于前面区间的代码点,将是致命错误。类似地,服务器和网关不能提供包含任何 Unicode 字符的字符串给应用对象。

再一次,本规范提供的字符串对象是 str 或者 StringType,而不是 unicode 或者 UnicodeType。并且,即使某个平台允许多于 8 比特的 str/StringType 对象,只有低 8 位会被使用。

本规范引用的字节字符串(如从 wsgi.input 读取的,传递给 write()的,或者由应用对象 yield 的),其值必须在 Python3 中必须是 bytes,在 Python2 中是 str。

错误处理

通常来说,应用应该尝试 trap 它们自己,或者内部错误等,显示一个有用的帮助消息给浏览器。(决定有用的消息是什么的是应用对象)

然而,要显示这样的消息,应用对象不能实际发送任何数据给浏览器,紧急中断这个响应。因此,WSGI 提供了一种机制:要么允许应用发送错误消息,要么自动中断:将 exc_info 传给 start_reponse。下面是个例子:

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()会重新抛出这个异常。这个异常不应该被应用对象 trap,所以应用对象将异常中断(abort)。服务器/网关则 trap 这个异常,终止响应。

服务器应该 trap 并记录任何中断应用对象或者应用对象的返回值的迭代异常。如果应用对象错误发生时响应的部分数据已经发送给浏览器,服务器/网关会尝试添加错误消息到输出中,如果已经发送的头包含了一个 text/*的内容类型,服务器清晰地知道怎么做出修改。

某些中间件可能希望提供额外的异常处理服务,或者拦截并替换应用错误消息。这些情况下,中间件会选择不抛出由 start_response()提供的 exc_info 信息,但是替换地抛出一个中间件特定的异常,或存储相关参数后不带异常地返回。然后,这是的应用对象返回它的错误体,允许中间件捕获并修改错误输出。这些技术一直有效,就像应用的作者们:

  1. 总是提供 exc_info 当开始错误响应时。
  2. 当提供了 exc_info 时永不 trap start_response()产生的错误。

HTTP 1.1 Expect/Continue

实现 HTTTP 1.1 的服务器/网关必须提供 HTTP1.1 的 expect/continue 机制的透明支持。这有以下几种方法可以实现:

  1. Respond to requests containing an Expect: 100-continue request with an immediate “100 Continue” response, and proceed normally.
  2. Proceed with the request normally, but provide the application with a wsgi.input stream that will send the “100 Continue” response if/when the application first attempts to read from the input stream. The read request must then remain blocked until the client responds.
  3. Wait until the client decides that the server does not support expect/continue, and sends the request body on its own. (This is suboptimal, and is not recommended.)

对 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 通信,RFC2616 所说的 hop-by-hop 头不提供 WSGI 内部通信。 WSGI 应用也不能产生任何 hop-by-hop 头。仅视同需要他们的 HTTP 特性的头,或依赖任何进来的 hop-by-hop 头。WSGI 服务器必须自己处理任何支持的内部的 hop-by-hop 头,如决定内部的 Transfer-Encoding,如果可用,包含 chunked 编码等。

应用这些原则在大量 HTTP 特性上,服务器就可以通过 If-None-Match 和 If-Modified-Since 请求头、Last-Modified 和 Etag 响应头 处理缓存验证。然而,不需要这样做。如果应用想支持这个特性,应该执行自己的缓存验证,因为服务器/网关不需要做这样的验证。

类似地,服务器可能重新编码或者转换编码一个应用的响应,但应用对象则应该有自己合适的编码,并且不能提供传输编码。服务器可能按应用的响应的顺序发送数据,而应用不需要原生地支持字节排序。再一次,如果希望有这个功能,应用应该自己实现自己的逻辑。

这些有关应用的限制不是必要的,这意味着每个应用必须重复实现每一个 HTTP 特性;许多 HTTP 特性部分地、全部地由中间件组件实现,因而对服务器和应用作者们来说,不必一遍一遍地实现相同的特性了。

线程支持

支持或者不支持线程,服务器独立的。可以并行运行多个请求的服务器,应该提供在单个线程中运行应用的选项,所以不支持线程安全的应用或者框架仍旧可以和这个服务器配合使用。

实现/应用

服务器扩展 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 有更深层的理解!

原规范还有《建议、讨论中》、《知识》、《参考文献》。由于和规范不太相关,就不作翻译了。

问题和回答

  1. 为什么 environ 必须用字典表示?用字典子类表示会出现什么问题?

使用字典的理由是,可以最大化服务器之间的可移植性。替换方案是定义一个字典的子类作为标准的、可移植的接口,其所有方法是 dict 类的方法的子集。然而,实际上,大部分服务器会找到一个满足需要的字典即可,因此框架作者们渐渐发现它们希望找到一个可用完整字典特性的字典实现,因为它们通常会需要。但如果某个服务器选择不适用字典表示 environ,就会出现互操作性问题,尽管服务器会尽量和规范保持一致。因此。为了简化规范和保持一致性,字典就变得非常必要了。

这并不表示服务器或者框架的开发者们不能增加其他内容(如在 environ 里自定义一些变量)。这恰恰是规范推荐的方法。

  1. 为什么你可以调用 write(),也可以使用 yield 字符串/return 迭代对象?使用其中之一不就够了?

如果只支持迭代方法,当前的一些使用 push 方法的框架就无法兼容;但是如果我们只通过 write() 支持 push 方法,服务器性能则因为传输大文件而下降(工作线程直到当前输出完成之后,才能开始处理新请求)。因而,我们做出一点妥协,应用框架就可以支持者两种方法;相应地,服务器开发者则要比只支持 push 方法多做一点工作。

  1. close()有什么用?

当应用对象完成写操作后,通过 try/finally 块,应用对象可以确保相关资源得到释放。但是,如果应用返回的是迭代对象,相关资源要等迭代对象被 gc 回收之后才会释放。而 close()则使应用对象在请求处理完成之后,释放相关资源(调用 close()释放资源)。这也是向前兼容的,支持生成器内部的 try/finally 块。

  1. 为什么 WSGI 这么低级(译者注:CGI 变量相对于应用中的对象是低级的)?我想用高级特性,如 cookies、sessions、持久化……

WSGI 不是另一个 Python web 框架。它仅仅是一种让框架和 web 服务器通信的方式,反过来也成立。你想要的这些特性应该由框架提供。并且,如果让你自己创建 WSGI 应用对象,你应该能够在多数支持 WSGI 的服务器上运行它。有些服务器会在 environ 字典中存放额外的信息;去看看服务器的文档吧。当然,使用了这些扩展信息的应用很可能不兼容其他支持 WSGI 的服务器。

  1. 为什么使用 CGI 变量而不是 HTTP 头?为什么将他们和 WSGI 定义的变量混用?

相对 CGI 规范,许多现存的 web 框架是沉重的;而现存的 web 服务器则擅长生成 CGI 变量。Many existing web frameworks are built heavily upon the CGI spec, and existing web servers know how to generate CGI variables. 相反,另一种方法,

In contrast, alternative ways of representing inbound HTTP information are fragmented and lack market share. Thus, using the CGI “standard” seems like a good way to leverage existing implementations. As for mixing them with WSGI variables, separating them would just require two dictionary arguments to be passed around, while providing no real benefits.

  1. status 是什么?能不能用数字 200 代替 “200 OK”呢?

用 200 代替”200 OK”会使得服务器/网关复杂化,因为这会要求它们维护一张数字和消息映射表。然而,对应用或者框架作者来说,输入消息文本和相应的状态码是容易的。现存的框架一般已经有这张表,所以,让应用/框架来负责这件事,要比服务器/网关要好。

  1. 为什么 _wsgi.run_once_ 不能保证只运行应用一次?

Because it’s merely a suggestion to the application that it should “rig for infrequent running”. This is intended for application frameworks that have multiple modes of operation for caching, sessions, and so forth. In a “multiple run” mode, such frameworks may preload caches, and may not write e.g. logs or session data to disk after each request. In “single run” mode, such frameworks avoid preloading and flush all necessary writes after each request.

However, in order to test an application or framework to verify correct operation in the latter mode, it may be necessary (or at least expedient) to invoke it more than once. Therefore, an application should not assume that it will definitely not be run again, just because it is called with wsgi.run_once set to True .

  1. Feature X (dictionaries, callables, etc.) are ugly for use in application code; why don’t we use objects instead?

All of these implementation choices of WSGI are specifically intended to decouple features from one another; recombining these features into encapsulated objects makes it somewhat harder to write servers or gateways, and an order of magnitude harder to write middleware that replaces or modifies only small portions of the overall functionality.

In essence, middleware wants to have a “Chain of Responsibility” pattern, whereby it can act as a “handler” for some functions, while allowing others to remain unchanged. This is difficult to do with ordinary Python objects, if the interface is to remain extensible. For example, one must use __getattr__ or __getattribute__ overrides, to ensure that extensions (such as attributes defined by future WSGI versions) are passed through.

This type of code is notoriously difficult to get 100% correct, and few people will want to write it themselves. They will therefore copy other people’s implementations, but fail to update them when the person they copied from corrects yet another corner case.

Further, this necessary boilerplate would be pure excise, a developer tax paid by middleware developers to support a slightly prettier API for application framework developers. But, application framework developers will typically only be updating one framework to support WSGI, and in a very limited part of their framework as a whole. It will likely be their first (and maybe their only) WSGI implementation, and thus they will likely implement with this specification ready to hand. Thus, the effort of making the API “prettier” with object attributes and suchlike would likely be wasted for this audience.

We encourage those who want a prettier (or otherwise improved) WSGI interface for use in direct web application programming (as opposed to web framework development) to develop APIs or frameworks that wrap WSGI for convenient use by application developers. In this way, WSGI can remain conveniently low-level for server and middleware authors, while not being “ugly” for application developers.

参考链接

  1. The Python Wiki “Web Programming” topic
  2. The Common Gateway Interface Specification, v 1.1, 3rd Draft
  3. “Chunked Transfer Coding” – HTTP/1.1, section 3.6.1
  4. “End-to-end and Hop-by-hop Headers” – HTTP/1.1, Section 13.5.1
  5. mod_ssl Reference, “Environment Variables”
  6. Procedural issues regarding modifications to PEP 333
  7. SVN revision history for PEP 3333, showing differences from PEP 333