WSGI web服务网关接口(4)

本文译自:https://www.python.org/dev/peps/pep-0333

前一部分内容请查看:http://10111000.com/2017/12/22/PEP333_3/

tips: 以下, 服务端代指服务端和网关, 框架端代指应用端和框架端

实施与应用笔记

服务端扩展API

服务端的开发者可能想要提供一些更加高级的API,让框架或应用程序作者用于一些特殊的目的。比如,基于mode_python的网关可能想暴露部分Apache的API作为WSGI的扩展。

在这种简单的情况下,这个需求无非是定义一个environ变量,比如mod_python.some_api。但是,在大多数情况下,现存的一些中间件可能会使得的情况变得更为复杂。比如,一个API可以提供获取environ变量中相同HTTP首部的内容,如果environ被中间件修改,就可能返回不同的内容。

一般情况下,任何复制、取代或是绕过WSGI部分功能的API都有与中间件不兼容的风险。服务端开发者不应该假定没有任何人使用中间件,因为一些框架开发者在试图组织或重构他们的框架时,会完全从中间件的角度出发来提供功能。

因此,为了最大限度的提高兼容性,服务端提供的一些代替WSGI功能的扩展API,必须由它们想要替代的那个API来调用。例如,访问HTTP首部的扩展API必须要求应用程序传入当前的环境,然后由服务端通过API去验证首部是否被中间件修改。如果扩展API不能保证它始终与传入环境变量中的HTTP首部保持一致,那它必须拒绝框架端的服务请求,并抛出异常,返回None来代替首部集合,或任何适用于API的内容。

类似的,如果一个扩展API提供写入响应数据或头部的另外一种方法,那么在应用程序在获得扩展服务之前,它应该要求框架端传入start_response函数。如果传入不是服务端提供的原始对象,那么它就不能保证会执行正确的操作,所以它应该拒绝提供扩展服务给框架端。

这些准则也适用于中间件,它们可能会添加诸如cookies解析,表单变量,会话等类似信息到environ变量。具体的来说,这样的中间件应该把对envrion的操作作为一个函数提供,而不是简单的把这些东西添加到environ变量中,这可以帮助确保在任何中间件在对任何URL进行重写或其他envrion修改后再调用该方法进行修改。

这些安全扩展的规则很重要,服务端和中间件开发者必须遵守,也为了避免将来中间件开发人员被迫从环境中删除任何和所有的扩展API,来确保他们不被使用这些扩展的应用程序绕过!

应用程序配置

这篇文档并没有定义服务端如何选择或者获取要调用的应用程序。这些或其他的配置项与服务端密切相关。服务端开发者应该给出如何配置调用应用程序及有什么配置选项的详细文档(比如线程选项)。

从另一方面来说,框架作者应该给出如何创建一个应用程序对象及打包框架的说明文档。选择了服务端和应用程序框架端的用户,应该把这两者连起来。不过,既然框架和服务端有一个共同的接口,这个仅仅是一个手工操作的问题,而不是对每一个新的服务端和框架的组合都要进行一系列的工程操作。

最后,一些应用,框架和中间件可能希望用envrion字典去接收一些简单的字符串作为配置信息。服务端应该支持应用部署人员把具体的键值对放入envrion变量中。最简单的情况,这个支持可以通过复制由os.environ提供的所有环境变量到envrion中,因为部署人员原则上可以把这些外部配置传递给服务器或像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)

但是,大多数现在的应用程序和框架可能仅仅需要从环境变量获取一个配置信息,为了指明它们的应用程序和具体框架的配置文件的位置。(当然,应用程序应该缓存这些配置,避免再次调用的时候重新读取。)

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 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并不完全一致。比如,服务端的重写规则,可能会修改客户端的原始请求并改为更为规范的形式。

支持低于2.2版本的Python

一些服务端,网关或应用程序可能希望支持低(<2.2)版本的Python。如果Jython是一个目标平台,这一点尤为重要,因为在撰写本文时,Jython 2.2的生产版本尚不可用。

对于服务端,这个相对来说会比较简单:对于低于2.2版本的Python,简单的限制自己仅使用标准的“for”循环语句去迭代任何由应用程序返回的可迭代对象。 这也是唯一确保从源码层保持低版本迭代协议和“今天的”迭代协议兼容的方法。(参阅PEP 234)。

(注:这个方法仅仅适用于用Python写的服务端、网关或中间件,如何从其他语言中使用迭代协议不在这篇文档的讨论范围。)

在应用程序中,如何支持低版本的Python会稍微复杂一些:

  • 不能返回一个文件对象并期待它可以像个可迭代对象一样工作。因为在2.2版本之前,文件不可迭代。(一般来说,你不应该这么做,因为在大多数情况下,它的性能会非常差!)使用wsgi.file_wrapper 或类应用程序定义的file wrapper对象。(请参阅可选特定平台的文件处理一节,获取关于wsgi.file_wrapper 的更多细节,并且你可以用提供的例子类使得一个文件可以迭代。)
  • 如果你返回了一个自定义的可迭代对象,它必须实现了2.2版本之前的迭代协议。也就是说,提供一个接收整型键的__getitem__方法,并在耗尽时引发IndexError错误。(注:内置的序列类型也是可以接受的,因为它们实现了这个协议。)

最后,如果中间件也希望支持低于2.2版本的Python,并迭代应用程序返回值或本身返回一个迭代(或两者),必须遵循上面的建议。

(注:不言而喻,要支持低于2.2版本的Python,任何服务器,网关,应用程序或中间件必须使用目标版本中的语言特性,比如用1 和 0 代替TrueFalse等。)

可选特定平台的文件处理

一些操作环境提供了特殊的高性能文件传输,比如调用Unix中的sendfile()。服务端可能会通过environwsgi.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,它必须可被调用,并接受一个位置参数和一个可选的位置参数。第一个参数是一个将要发送的类文件对象,第二个参数是一个可选的块大小“建议”(服务端并不需要)。可调用对象必须返回一个可迭代对象,并且除非服务器/网关实际接收到来自应用程序的可返回值作为返回值,否则不得执行任何数据传输。(否则会阻止中间件解释及覆盖响应数据。)

类文件是指,由应用程序提供的对象必须有一个read() 方法,接收一个可选的大小参数。它可能有一个close方法,如果这样的话,由wsgi.file_wrapper返回的可迭代对象也必须拥有一个close方法,并调用类文件对象的原始close方法。如果类文件对象有其他的与Python内置文件对象中相同的方法和属性名(比如fileno()),wsgi.file_wrapper可能会假设这些方法或属性与系统内置的文件对象有相同的语义。

任何特定平台的文件处理的实现必须在应用程序返回后进行,服务器或网关将检查是否返回了包装对象。(需要强调的是,由于中间件,错误处理等的存在,不能保证所创建的任何包装将被实际使用。)

除了处理close()方法,从应用程序返回的文件包装器的语义应该与应用程序返回的iter(filelike.read, '')一样。换句话说,传输应该从文件当时开始传输时的位置开始,并继续传输直到结束。

当然,特定平台的文件传输API通常不能接收任意的类文件对象,因此,为了确定类文件对象是否适用于特定平台的API,wsgi.file_wrapper必须提供如fileno()(类Unix操作系统)或java.nio.FileChannel(Jython平台)方法。

注意,即使对象不适合平台的API,wsgi.file_wrapper仍然要返回一个拥有readclose方法的可迭代对象,使得应用程序可以在各个平台之间进行移植。这是一个简单的与平台无关的文件包装器类,适用于旧版本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
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()
Leo wechat
欢迎订阅公众号,建设中!