0%

PEP3333 (Part 2) -- WSGI 规范概述

这一部分主要是关于 WSGI 规范的概述部分。在这里我们会提到 WSGI 主要的三个组成部分:服务器端(网关侧)、应用程序端(框架侧)以及中间件,同时还有官网提供的这三个部分的示例实现。

规范概述

WSGI 接口包括两个方面:服务器端或网关侧,以及应用端或框架侧。服务器端调用由应用端提供的可调用对象,而这个对象能提供的细节内容取决于服务器端或网关侧。
它假设一些服务器端或网关侧可能会要求一个应用的部署器(deployer),这个部署器可以写入短脚本来创建服务器端或网关侧的实例(instance),然后提供给应用程序对象(application object)。

其它服务器端或网关侧可能会使用配置文件或者其他机制来指定应用程序对象(application object)应该要从哪里获得或者以其他方式获得。

除了纯粹的服务器(网关)/应用程序(框架)模式之外,WSGI 还可以创建同时实现这两端的规范的“中间件”组件。这些组件可以在其所在服务器中充当应用程序,又可以作为服务器来运行应用程序,同时还能提供扩展 API、内容转换(content transformation)、导航(navigation)以及其他有用的功能。

在本规范中,我们使用“可调用对象”(callable)一词,来形容像函数(function)、方法(method)、类(class)或者一个包含 __call__ 方法的对象实例。这取决于服务器端(网关侧)或者应用程序端根据自己的需要,选择以上的哪个最合适的途径来实现这个所谓的“可调用对象”。相反,调用可调用对象的服务器、网关或者应用程序不必依赖于这个可调用对象到底是以什么形式提供的。Callable (可调用对象)只需要被调用,而不是被内省。

关于字符串类型的注释

通常,HTTP 处理程序都是在处理字节的,这意味着这个规范主要还是关于如何处理字节的。

但是,这些字节的内容通常会有某种文本字面上的解释。在 Python 中,string(字符串)是处理文本的最好方式。

但是在许多 Python 版本以及实现中,字符串是 Unicode,而不是字节。这需要在提供可用的 API 和实现 HTTP 上下文中的字节和文本的正确翻译之间做好谨慎的权衡,特别是对于那些在处理 str 类型实现迥异的 Python 实现或版本(如 Python2 和 Python3, Python2 和 IronPython)之间提供可移植代码的时候。

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

Native” 字符串(指那些使用名为 str 的类型实现的字符串)来作为请求(request)/响应(response)的头以及元数据的载体。
Bytestrings” 字符串(指那些在 Python3 中使用 bytes 类型实现的,其他地方使用 str 实现的字符串)用于处理请求的请求体以及响应的响应体(例如 POST/PUT 的输入数据以及 HTML 页面的输出)。
但是请不要混淆:尽管 Python 的 str 类型实际上是 Unicode,native 字符串仍必须通过 Latin-1 编码为字节(详见本文档后面的 《Unicode 编码问题》 部分)!

简而言之,在这篇文档中,如果在哪里看到了 “string“(字符串),它就意味着 “Native” 字符串,例如,一个 str 的对象(无论实际上它内部是以 bytes 还是 unicode 实现的)。如果你看到了一个 “bytestring”(字节串),那它应该被理解为 “Python3” 下的 bytes 对象,或者 Python2 的 str 对象。

因此,尽管 HTTP 在某种层面上“仅仅只是字节(bytes)”而已,但是使用 Python 默认的 str 类型对很多 API 来说都很方便。

应用端/框架侧

应用程序对象(application object)仅仅是一个接收两个入参的可调用对象。“对象”一次在这里不应该被误解为需要一个实际的对象实例,一个函数、方法、类或者包含 __call__ 方法的实例都是可以的。应用程序对象必须能够被多次调用,因为实际上所有的服务器端/网关侧(除了 CGI)都会存在重复请求。

注意,虽然我们把它称为“应用程序”(application)对象,但它不应该被解读为这意味着应用开发者会直接使用 WSGI 作为编程所需的 API 接口。它假设应用开发者会继续使用现有的高层次的框架服务来开发他们的应用。WSGI 仅仅只是框架或者服务器端开发者的工具,不是直接面向应用开发者的。

这里有两个应用程序对象(application object)的例子:一个是函数,而另一个是类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# coding: utf-8
HELLO_WORLD = b"Hello world!\n"


def simple_app(environ, start_response):
"""尽可能小的应用程序对象 application object """
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [HELLO_WORLD]


class AppClass:
"""提供相同的输出,不过是使用类来实现

(注意,AppClass 在这里是应用 application,所以调用
这个类返回的AppClass 实例,对它调用 iter 方法得到的
可迭代对象就是规范中要求的 application object 应用程序对象

如果我们想使用 AppClass 的实例而不是它的可迭代对象,我们必须实现
__call__方法,这个方法会被调用并执行这个应用,然后我们需要由服
务器端或者网关侧来创建这个实例
"""

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 网关,实现为一个带应用程序对象为参数的函数。请注意,这个简单的示例里有有限的错误的处理,因为一个未被捕获的异常默认会被传递到 sys.stderr 并且由 Web 服务求记录下来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import os
import sys

enc, esc = sys.getfilesystemencoding(), 'surrogateescape'


def unicode_to_wsgi(u):
# 转换环境变量作为一个 WSGI 的 “bytes-as-unicode” 兼容的字符串
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:
# 在得出第一个 output 输出之前,发送存储的头信息
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:
# 如果头信息发送了,重新抛出原生异常
raise exc_info[1].with_traceback(exc_info[2])
finally:
exc_info = None # 避免循环引用
elif headers_set:
raise AssertionError("Headers already set!")

headers_set[:] = [status, response_headers]

# 注意:对于头信息的错误检查应该在这里来做
# 也就是在头信息被设置“之后”来做。这样的话,如果一个错误发生,
# start_response 只能带着 exc_info 集合被重复调用
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()

中间件:扮演双边角色的组件

注意,一个单一的对象可以在面向应用程序的时候扮演服务器的角色,同时也可以在面向应用的时候代表服务器的身份。像这样的“中间件”组件可以发挥以下功能:

  • 在重写(rewrite)了环境(environ)字典后,可以基于目标 URL 相对应的路由请求到不同的应用程序对象中去;
  • 允许多个应用程序或者框架在同一个进程中并行执行;
  • 通过网络来转发请求和响应来实现负载均衡和远程过程调用;
  • 执行内容后期处理,例如给返回值应用 XSL 样式表

一般来说,中间件的存在对于“服务器端/网关侧”以及“应用端/框架侧”两边的接口来说都是透明的,并不需要它们提供什么特殊的支持。希望把中间件集成到应用程序中去的用户,只需要将中间件组件当作一个应用程序一样交给服务端,然后配置中间件组件来调用应用程序,就好像中间件就是一个服务器一样。当然,这里中间件封装的“应用”实际上可能是被另一个应用程序所封装的中间件之类的,这种是我们称之为“中间件栈”(middleware stack)的东西。

大多数情况下,中间件必须同时遵守 WSGI 对于服务器端和应用端的限制和要求。然而,在某些情况下,中间件的这些要求会比一个单纯的“服务端”或者“应用端”来得更为严格。这些要点会在详述(specification)中予以关注。

下面是一个(不那么走心,tongue-in-cheek)的中间件的例子,它使用 Joe Strout 的 piglatin.py 来将 text/plain 的响应转换为 pig Latin

注意,一个“真正”的中间件会使用更加健壮的途径来检查内容类型(content type),而且应该要检查内容编码(content encoding)。同样,这个简单的例子忽略了一个单词可能在数据块的边缘被分割的情况(也就是说单词会被划分为两部分,把它当作两个单词)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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))