0%

PEP3333 (Part 4) -- WSGI 的实现说明(完.)

最后这部分,是关于 WSGI 一些在实现上的说明,用于给那些要支持 WSGI 标准的服务器程序或框架的实现提供指导意见,包括 服务器端扩展 API应用程序端的配置URL重写 以及 文件处理。这部分的翻译省略了 Supporting Older (<2.2) Versions of Python 一节,毕竟目前对于 Python 2.2 之前版本的使用可以说是九牛一毛了(而且 WSGI 标准本身就要求 Python 2.2 版本以上),有兴趣或者需要的读者可以自行参阅。对于 PEP3333 规范的解读告一段落,不过 WSGI 的解读还有下文,到时候可能会在 http.server 中穿插探讨。

关于实现与应用的说明

服务端扩展 APIs

一些服务器作者可能希望公开更多高级的 API,那样的话应用程序或框架的作者可以使用它们实现制定的用途。例如,一个基于 mod_python 的网关可能希望将部分 Apache API 作为 WSGI 扩展暴露储区。

在最简单的情况下,这只需要定义一个环境变量(environ variable),例如 mod_python.some_api。但是,大多数情况下,可能存在的中间件会使得这个办法难以实现。例如,API 通过环境变量提供了一个访问方法来获得和 API 同名的 HTTP 报头,如果环境变量被中间件修改了,那么就可能返回不同的数据。

一般来说,任何用于重复、替换或者绕过某些 WSGI 功能的扩展 API 都有可能与中间件组件不兼容。服务器端(网关侧)开发者不应该假定没有人会使用中间件,因为一些框架作者朱门打算组织或重新组织它们的框架,使其几乎完全作为各种中间件来运行。

因此,为了最广泛的兼容,那些提供扩展 API 来替代某些 WSGI 功能点的服务器端和网关侧,必须谨慎设计这些 AIP ,以便使用这些 API 足以替代它们想取代的 WSGI 的功能。例如,一个访问 HTTP 请求头的扩展 API 必须要求应用程序传入其当前环境,以便服务器端(网关侧)可以验证,通过这个 API 访问的 HTTP 头并没有被中间件改动过。如果扩展 API 在 HTTP 头内容上不能保证和传入的环境保持一致的话,则这个 API 必须拒绝为应用程序提供服务。例如,通过抛出一个错误,返回 None 而不是报头集合抑或者任何 API 需要的对象来拒绝服务。

同样,如果一个扩展 API 提供了另一种方法去写入响应数据或者响应头,他应该在应用程序可以使用这个扩展服务之前,要求(应用程序)传入 start_response 可调用对象给他。如果这个对象和服务器端(网关侧)原本提供给应用程序的不一样,那么它(API)就不能保证正确的执行,并且必须拒绝为应用程序提供这个扩展服务器。

这些规则同样适用于那些想通过解析诸如 cookies、表单变量(form variables
)、会话信息(session)等来为 environ 字典附加信息的中间件。具体来说,那些中间件应该以操作 environ 字典的函数的形式提供这些功能,而不是简单地将值填入 environ 字典中就完了。这有助于确保当有任何中间件执行了任何 URL 重写或者改动 environ 字典操作之后,可以从 environ 字典中重新计算得到信息。

对于服务器端(网关侧)开发者以及中间件开发者来说,遵守这种“安全扩展”规则是非常重要的,这是为了确保避免以后出现这样一种双败的局面:中间件开发者不得不从 environ 字典中删除任何或者全部的扩展 API 来保证它们中间件的干预不会因为应用程序使用了这些扩展功能而被绕过,从而无法起到作用。

应用程序的配置

这个规范没有定义服务器要如何选择或获取它要调用的应用程序。这些和其他配置选项一样是一些和服务器程序紧密相关的问题。我们希望是服务器端(网关侧)的作者可以以文档的形式记录,如何配置服务器来执行特定的应用程序对象(application object),以及要使用哪些选项(如多线程选项)。

另一方面,框架作者应该要已文档的形式记录下如何创建一个包装了(warp)他们框架功能的应用程序对象(application object)。那些已经选择了服务器程序以及应用程序框架的使用者们,必须知道如何将这两者结合起来。但是,由于现在每个框架和服务器程序都有了一个通用的接口(WSGI),那么这项工作就变成了一个单纯的定式工作项。对于任何的服务端程序/应用程序框架组合来说都不再是一个影响重大的工程了

最后,一些应用程序、框架以及中间件可能想使用 environ 字典来获得简单的字符串配置选项。服务器端和网关侧应该要提供支持,即允许应用程序的部署者可以在 environ 字典中指定键值对。最简单的例子是,这个支持可以仅仅只到将操作系统提供的环境变量从 os.environ 复制到 environ 字典中的程度而已,因为部署者原则上可以从外部环境中配置到服务器上,或者再使用 CGI 的情况下他们可以通过服务器的配置文件进行设置。

应用程序应该尽可能将这些所需的变量数量保持在一个最少可接受范围内,因为不是所有的服务器都可以支持简单的配置工作。当然,即使在最坏的情况下,部署应用程序的人也可以写一个脚本来提供必要的配置值:

1
2
3
4
5
from the_app import application

def new_app(environ, start_response):
environ['the_app.configval1'] = 'something'
return application(environ, start_response)

但是,大多数现有的应用程序和框架可能只需要来自 environ 字典的单个配置值,以只是他们应用程序或者特定于框架的配置文件的位置(当然,应用程序应该缓存这样的配置,以避免每次调用的时候都需要重新去读取它)。

URL 重写

如果应用程序希望重新构建完整的请求 URL,那么它可以使用由 lan Bicking 提供的以下算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from urllib.parse import quote
url = environ['wsgi.url_scheme']+'://'

if environ.get('HTTP_HOST'):
url += environ['HTTP_HOST']
else:
url += environ['SERVER_NAME']

if environ['wsgi.url_scheme'] == 'https':
if environ['SERVER_PORT'] != '443':
url += ':' + environ['SERVER_PORT']
else:
if environ['SERVER_PORT'] != '80':
url += ':' + environ['SERVER_PORT']

url += quote(environ.get('SCRIPT_NAME', ''))
url += quote(environ.get('PATH_INFO', ''))
if environ.get('QUERY_STRING'):
url += '?' + environ['QUERY_STRING']

注意,这样重建的 URL 可能会与客户端请求的 URI 不完全相同。例如,服务器重写规则可能会修改客户端原始的请求 URL 以将其变得符合范式。

可选的平台相关的文件处理

某些操作系统提供特殊的高性能文件传输工具,例如 Unix 系统的 sendfile() 调用。服务器端和网关侧可能会通过一个在 environ 字典中可选的 wsgi.file_wrapper 键来暴露这个功能出来。一个应用程序可能使用这个 “文件包装器”(file wrapper)来把一个文件或者类文件对象转变为应用程序返回的可迭代对象,例如:

1
2
3
4
if 'wsgi.file_wrapper' in environ:
return environ['wsgi.file_wrapper'](filelike, block_size)
else:
return iter(lambda: filelike.read(block_size), '')

如果服务器端(网关侧)提供了 wsgi.file_wrapper,则它必须是一个接收一个必须位置参数以及一个可选位置参数的可调用对象。第一个参数是要被发送的类文件对象,而第二个参数是一个可选的“建议”划分块大小(服务器端/网关侧可能用不上)。这个可调用对象必须返回一个可迭代对象,而且除非服务器端(网关侧)确实接收到这个作为应用程序返回值的可迭代对象,否则不能执行任何的数据传输(否则会 阻止/短路 中间件去解析或修改该响应数据)。

这个由应用程序提供的“类文件”对象,必须有一个带可选 size (大小)参数的 read() 方法。它可以提供 close() 方法,如果提供了,由 wsgi.file_wrapper 返回可迭代对象也必须要由 close() 方法来调用这个最初的类文件对象的 close() 方法。如果这个“类文件”对象有其他命名和 Python 内建的文件对象匹配的方法或属性(如 fileno()),那么 wsgi.file_wrapper 可以假定这些方法或属性都有和内建文件对象同样的语义。

任何平台相关的文件处理的实际实现必须在应用程序返回之后进行,而且服务器端(网关侧)将检查是否返回了一个包装对象。(相反地,因为中间件的存在,错误处理等程序不能保证可以使用任何创建出来的包装对象)

除了处理 close() 之外,如果应用程序返回 iter(filelike.read, '') 可迭代对象,那么它应该要和应用程序返回的文件包装对象的语义一样。换句话说,传输应该从传输开始时,“文件”内的当前位置开始,一直到文件的结尾,或者写入了 Content-Length (内容长度)指定长度的字节数位置。(如果应用程序不提供 Content-Length 内容长度,服务则可以从它基本的文件处理实现的知识中生成一个)

当然,平台相关的文件传输 API 通常不会随意接受任何(arbitrary )的“类文件”对象。因此,一个 wsgi.file_wrapper 必须对提供的对象进行内省,检查是否有 fileno() (Unix-like 系统下)或 java.nio.FIleChannel(Jython 环境下),以判断这个类文件对象是否适合使用它当前支持的平台相关的 API。

注意即使对象不适合当前平台的 API,这个 wsgi.file_wrapper 也必须返回一个包装了 read()close() 对象的可迭代对象,以便使用这个文件包装对象的应用程序可以跨平台移植。这里有一个简单的平台无关的文件包装类,适用于旧版本(2.2 之前版本)以及新版本的 Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 的访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.
# 检查 reslt.filelike 是否能被 w/平台相关的 API 所使用
# 如果可以,直接使用这个 API 去传输结果
# 如果不可以,往下传递给常规的可迭代对象处理循环中去
for data in result:
# etc.

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