WSGI web服务网关接口(1)

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

序言

注意:如果需要查看支持Python 3.x版本的更新(包括社区勘误,附录和说明),请移步查看PEP3333

摘要

为了提高网络应用在一系列web服务器之间的可移植性,这篇文档给出了web服务端和python web应用或框架的标准接口。

理由和目标

Python语言中目前涌现了一大批的网络应用框架, 比如Zope, Quixote, Webware, SkunkWeb, PSO 和 Twisted Web, 这里仅仅列出了其中的一部分Python Wiki。如此多的选择对于Python的初学者来说也许是个难题,因为一般来说,Web框架的选择一定程度上限制了Web服务器的选择, 反之亦然。

相比之下,尽管Java有如此多的Web应用框架可以使用,但是Servlet API的使用使得所有支持Servlet API的网络应用框架可以跑在任何同样支持Servlet的网络服务器上。

这样一个支持Python的API如果也能得到广泛使用的话(不管服务器是用Python-Medusa,还是嵌入式Python - mod_python, 亦或是通过网关协议来调用Python - CGI,FastCGI 等),那么就可以将框架与Web服务器的选择分开,使用户能够随意搭配他们想要的服务器或者web框架,同时也释放了框架和服务器开发人员,使其专注于他们偏好的专业领域。

这篇PEP文档,提出了一种用于服务器端和应用框架之间简单且通用的接口:Python Web 服务网关协议接口(WSGI)。

但是WSGI规范的存在并不能改变现有的服务器及应用框架的状态,服务器及框架作者和维护者必须切实的实现WSGI才可以使它生效。

不过,因为目前没有支持WSGI规范的服务器和框架,而对于准备实施及支持WSGI规范的作者来说也没有即时的报酬。因此,WSGI必须很容易实现,开发者在实施起来时的初始投资也能在一个很低的合理范围。

因此,服务端和框架端的实施的简单性相比于WSGI接口的实用性来说要显得更为重要,这也是在作任何设计决定时的首要原则。

但框架作者去实现一个框架的简单性并不意味应用程序作者的简单性,这是两回事。WSGI提供给框架作者的是不加任何修饰的接口,因为像响应对象和cookie处理这样花里胡哨的东西只会阻碍现有框架。需要强调的是,WSGI的目标是促进现有服务器和应用框架的互连,而不是创建一个新的Web框架。

另外,这个目标也避免了一些已经部署的Python版本中不可用的东西。因此,新的标准库模块不是这个规范里必须的,同时也不要求Python的版本大于2.2.2。(这将是一个好主意,未来Python版本中的标准库中Web server也会包含对这个接口的支持)。

除了让现有的和未来的框架及服务器容易实施以外, 它也应该很容易的创建请求预处理器,响应处理器以及其他基于WSGI的中间件组件,中间件组件对于包含这个中间件的服务器来说它是一个应用 ,对于中间件包含的应用来说的话,它是一个服务器。

如果中间件不仅简单而且足够健壮,且WSGI在服务端和框架中被广泛应用,那么会出现一种全新的Python web应用框架:一个由松散耦合的WSGI中间件组成的框架结构。显然, 现有的框架作者甚至可以选择重构他们现有的服务,变得更像一个使用WSGI的库,而不是一整个的整体框架。这也允许应用开发者选择最好的组件组合去提供一些特定的功能,而不是必须提交单一框架的的所有利弊。

当然,截至本文,距离那一个目标还有很远,目前的WSGI短期目标是使任何的框架可以跑在任何的服务器上。

最后要提到的一点是,目前版本的WSGI并没有规定用一个服务器或网关部署一个应用的规范,现在这些都必须在服务端定义并实现,在有足够多的服务器及框架实现了WSGI并在服务部署上积累了一定经验后,可以创建另一个PEP文档,描述WSGI服务器及应用框架的部署标准。

概述

WSGI接口包含两个方面 :服务或网关,应用或框架。服务端调用应用端提供的一个可以调用的对象,如何提供对象的细节取决于服务器或网关。某些服务器或网关可能需要应用程序的部署者编写一个简短的脚本来创建服务器或网关的一个实例并为其提供一个应用对象。也可能有其他的服务器需要通过配置文件或其他机制来定义从哪里引入应用对象或者获得。

除了单一化服务端和应用端的以外,还可以创建服从两端规范的中间件。这些组件可以充当包含它们的服务器的应用程序,也可以作为包含应用程序的服务器,另外也可以用来提供扩展API,内容转换,导航和其他有用的功能。

在整个这篇规范中,我们将使用术语“可调用”来表示“函数,方法,类或具有call方法的实例”。为了满足服务端、网关或应用程序的需求,它可以选择用适合的技术去实现。反过来也就是说,调用可调用对象的服务器,网关或应用程序对可调用的对象类型不能有任何依赖,可调用对象只能被调用,而不能内省。

应用端/框架端

应用对象是一个简单的可接收两个参数的可调用对象,对象这个词不应该误导你认为需要一个真正的对象实例:一个函数,方法,类或拥有call方法的实例都可以作为一个应用对象被接受。应用对象也必须可以被多次调用,因为几乎所有的服务器或者网关(CGI除外)都会做出重复的请求。

注意:尽管我们把它称作为“应用”对象, 这也并不意味着应用开发者应该直接使用WSGI作为Web开发编程的API,我们是假设应用开发人员是基于现有的,更高抽象层次的框架上去进行开发的。WSGI是一个服务器和框架开发者的工具,并不会直接支持应用开发人员。

这里有两个应用对象的示例,一个为函数,另外一个是一个可调用的类。

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
def simple_app(environ, start_response):
"""Simplest possible application object"""
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return ['Hello world!\n']


class AppClass:
"""Produce the same output, but using a class

(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!\n"
服务/网关端

针对应用程序的每个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
import os, sys

def run_with_cgi(application):

environ = dict(os.environ.items())
environ['wsgi.input'] = sys.stdin
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):
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
sys.stdout.write('Status: %s\r\n' % status)
for header in response_headers:
sys.stdout.write('%s: %s\r\n' % header)
sys.stdout.write('\r\n')

sys.stdout.write(data)
sys.stdout.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[0], exc_info[1], exc_info[2]
finally:
exc_info = None # avoid dangling circular ref
elif headers_set:
raise AssertionError("Headers already set!")

headers_set[:] = [status, response_headers]
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将请求路由到不同的应用程序对象。
  • 允许多个应用程序或框架在同一个进程中并行
  • 通过请求转发和响应,实现负载均衡和远程处理
  • 对内容进行后处理,比如应用XSL样式表

一般而言,中间件的存在对于接口的“服务器/网关”和“应用/框架”是透明的,不应该需要特别的支持。一个用户想整合中间件,只需要把其作为应用程序提供给服务器,并把中间件看作是服务器,利用它调用真正的应用程序。当然,“应用程序”也可能是另一个包装了应用程序的中间件,这里由多个中间件组成的棧就叫做中间件堆棧。

大部分情况下,中间件都必须同时遵守服务端和应用端两方面的限制及满足两方面的需求。不过在一些情况下,中间件的需求甚至比单纯的服务器或应用程序更严格,这些必须在中间件的文档中提出来。

这个(不认真的)中间件组件是把响应类型是text/plain的文本用Joe Strout的piglatin.py转化为pig latin文,(注意:一个“真正”的中间件可能会用更健壮的方法来检查内容类型,同时检查内容编码。另外,这个简单的例子也忽略一个单词在边界被分割开的情况)

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
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 string, 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())
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))
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)
Leo wechat
欢迎订阅公众号,建设中!