对于Web系统来说,客户端一般就是浏览器,客户端与服务器之间使用HTTP协议通讯。在Python的Web开发中,服务器与Python Web应用之间交互的协议就是WSGI。它由PEP 333提出,并在PEP 3333中做了补充。如果你也想阅读WSGI相关的PEP规范,可以直接阅读PEP 3333,因为PEP 3333对PEP 333是向下兼容的。这里先翻译了PEP 333,后面还会翻译PEP 3333。
前言 Preface
注意:有关支持Python 3.x的此规范的更新版本,包含社区勘误,补充和说明,请参阅PEP 3333。
简介 Abstract
本文档描述一份在Web服务器与Web应用/Web框架之间的标准接口,此接口的目的是使得web应用在不同web服务器之间具有可移植性。
基本原理与目标 Rationale and Goals
Python目前拥有大量的Web框架,比如Zope, Quixote, Webware, SkunkWeb, PSO,和Twisted Web等。大量的选择使得新手无所适从,因为总得来说,框架的选择都会限制Web服务器的选择,反之亦然。
相比之下,虽然JAVA也拥有许多Web框架,但是JAVA的”servlet” API使得使用任何Java Web应用程序框架编写出来的应用程序可以在任何支持”servlet” API的Web服务器上运行。服务器中这种针对Python的API的使用和普及,无论这些服务器是用Python编写的(例如Medusa)、嵌入Python(例如mod_python)还是通过gateway协议(例如CGI,FastCGI等等)调用Python,将分离人们对Web框架和对Web服务器的选择,用户可以自由选择适合他们的组合,而Web服务器和Web框架的开发者也能够把精力集中到各自偏好的专业领域。
因此,这份PEP建议在Web服务器和Web应用/Web框架之间建立一种简单的通用的接口规范,Python Web Server Gateway Interface (WSGI)。但是,仅仅存在这么一份规范并不能改变Web服务器和Web应用/框架的现状。只有Web服务器和Web框架的作者们真正实现WSGI才能产生效果。然而,既然还没有任何框架或服务器实现了WSGI,对实现WSGI也没有什么直接的奖励,那么WSGI必须容易实现,这样才能降低作者的初始投资。因此,服务器和框架两边接口的实现的简单性,对于WSGI的作用来说,绝对是非常重要的。所以这一点是任何设计决策的首要依据。
对于框架作者来说,实现的简单和使用的方便是不一样的。WSGI为框架作者提供一套绝对没有”frills”的接口,因为像response对象和对cookie的处理这些问题和框架现有的对这些问题的处理是矛盾的。再次重申,WSGI的目的是使得Web框架和Web服务器之间轻松互连,而不是创建一套新的Web框架。
还要注意这个目标使得WSGI不能依赖任何在当前已部署版本的Python没有提供的任何功能,因此,也不会依赖于任何新的标准模块,并且WSGI也不要求Python版本大于2.2.2。(当然,将来的Python版本会在标准库提供的Web服务器中包含对此接口的支持也是个不错的主意。)
除了要让现有的或将要出现的框架和服务器容易实现,也应该容易创建请求预处理器、响应处理程序和其他基于WSGI的中间件组件,对于服务器来说他们是应用程序,而对于他们包含的应用程序来说他们是服务器。
如果中间件既简单又健壮,而且在服务器和框架中WSGI被广泛实现,那么就有可能出现全新的Python Web框架:整个框架都是由几个松散耦合的WSGI中间件组件组成。甚至现有框架的作者都会选择将已实现的服务以这种方式重构,变得更像一些和WSGI配合使用的库而不是一个独立的框架。这样,应用程序开发人员就可以选择具有特定功能的“最佳组件”,而不必承担单一框架的所有优点和缺点。
当然,在写这篇文章的时候,这一天无疑是相当遥远的。与此同时,WSGI使任何服务器都可以使用任何框架是一个足够的短期目标。
最后,需要指出的是当前版本的WSGI并没有规定一个应用具体以何种方式部署在Web服务器或gateway上。目前,这个需要由服务器或gateway的具体实现来定义。如果足够多实现了WSGI的服务器或gateway通过领域实践产生了这个需求,也许可以产生另一份PEP来描述WSGI服务器和应用框架的部署标准。
概述 Specification Overview
WSGI接口有两种形式:一个是针对服务器或gateway的,另一个针对应用程序或框架。服务器接口请求一个由应用接口提供的可调用的对象,至于该对象是如何被请求的取决与服务器或gateway。我们假定一些服务器或gateway会需要应用程序的部署人员编写一个简短的脚本来启动一个服务器或gateway的实例,并把应用程序对象提供的服务器,而其他的服务器或gateway需要配置文件或其他机制来指定从哪里导入或者或得应用程序对象。
除了纯粹的服务器/gateway和应用程序/框架,还可以创建实现了这份规格说明书的中间件组件,对于包含他们的服务器他们是应用程序,而对于他们包含的应用程序来说他们是服务器,他们可以用来提供可扩展的API,内容转换,导航和其他有用的功能。
在整个说明书中,我们将使用术语“可调用”来表示“函数,方法,类或带有__call__方法的实例”。实现可调用的服务器,gateway或应用程序根据需要选择合适的实现技术。相反,调用可调用对象的服务器,gateway或应用程序必须不依赖于提供给它的可调用对象类型。
应用程序/框架端 The Application/Framework Side
一个应用程序对象是一个简单的接受两个参数的可调用对象,这里的对象并不是真的需要一个对象实例,一个函数、方法、类、或者带有__call__方法的对象实例都可以用来做应用程序对象。应用程序对象必须可以多次被请求,实际上服务器/gateway(而非CGI)确实会产生这样的重复请求。
注意:虽然我们把他叫做”应用程序”对象,但并不是说程序员要把WSGI当作API来调用,我们假定应用程序开发者仍然使用更高层面上的框架服务来开发应用程序,WSGI是提供给框架和服务器开发者使用的工具,并不打算直接对应用程序开发者提供支持。
这里有两个应用程序对象的示例,一个是函数,另一个是类:
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"
服务器/gateway接口 The Server/Gateway Side
服务器或gateway调用可调用的应用程序,对于从HTTP客户端接收的每个请求,该应用程序都是针对应用程序的。为了说明,这里是一个简单的CGI gateway,作为一个应用程序对象的函数实现。请注意,这个简单的例子有限的错误处理,因为默认情况下,未捕获的异常将被转储到sys.stderr并由Web服务器记录。
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()
中间件:同时扮演两种角色的组件 Middleware: Components that Play Both Sides
注意到单个对象可以作为请求应用程序的服务器存在,也可以作为被服务器调用的应用程序存在。这样的中间件可以执行这样一些功能:
- 重写前面提到的environ之后,可以根据目标URL将请求传递到不同的应用程序对象
- 允许多个应用程序和框架在同一个进程中运行
- 通过在网络传递请求和响应,实现负载均衡和远程处理
- 对内容进行后加工,比如附加xsl样式表
一般来说,中间件的存在对于服务器接口和应用接口来说都应该是透明的,并且不需要特别的支持。希望在应用程序中加入中间件的用户只需简单得把中间件当作应用提供给服务器,并配置中间件足见以服务器的身份来请求应用程序。当然,中间件组件包裹的可能是包裹应用程序的另一个中间件组件,这样循环下去就构成了我们称为”中间件堆栈”的东西了。
中间件大部分要符合应用接口和服务器接口提出的一些限制和要求,有些时候这样的限制甚至比纯粹的服务器或应用程序还要严格,这些地方我们会特别指出。
这里有一个中间件组件的例子,他用Joe Strout的piglatin.py将text/plain的响应转换成pig latin(注意:真正的中间件应该使用更加健壮的方式——应该检查内容的类型和内容的编码,同样这个简单的例子还忽略了一个单词可能被分割到一个块边界的可能性)。
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))
规格的详细说明Specification Details
应用程序对象必须接受两个参数,为了方便说明我们不妨分别命名为environ和start_response,但并非必须取这个名字。服务器或gateway必须用这两个参数请求应用程序对象(比如像上面展示的,这样调用result=application(environ,start_response))
参数environ是个字典对象,包含CGI风格的环境变量。这个对象必须是一个Python内建的字典对象(不能是子类、UserDict或其他对字典对象的模仿),应用程序可以以任何他愿意的方式修改这个字典,environ还应该包含一些特定的WSGI需要的变量(在后面的节里会描述),还可以包含一些服务器特定的扩展变量,按照下面将要描述的约定进行命名。
start_response参数是一个接受两个必须参数和一个可选参数的可调用。方便说明,我们分别把他们命名为status,response_headers,和exc_info。应用程序必须用这些参数来请求可调用start_response(比如象这样start_response(status,response_headers))
参数status是一个形式像”999 Message here”的状态字符串。而response_headers是描述HTTP响应头的(header_name,header_value)元组列表。可选的exc_info参数会在下面的start_response()可调用和ErrorHandling两节中描述,他只有在应用程序产生了错误并希望在浏览器上显示错误的时候才有用。
start_response可调用必须返回一个write(body_data)可调用,它接受一个可选参数:一个将要被作为HTTP响应体的一部分输出的字符串(注意:提供可调用write()只是为了支持现有框架的必要的输出API,新的应用程序或框架尽量避免使用,详细情况请看Buffering and Streaming一节。)
当被服务器请求的时候,应用程序对象必须返回一个0或多个可迭代的字符串,这可以通过多种方法完成,比如返回一个字符串的列表,或者应用程序本身是一个生产字符串的函数,或者应用程序是一个类而他的实例是可迭代的,不管怎么完成,应用程序对象必须总是返回0或多个可迭代的字符串。
服务器必须将产生的字符串以一种无缓冲的方式传送到客户端,每次传完一个字符串再去获取下一个。(换句话说,应用程序应该实现自己的缓冲,更多关于应用程序输出必须如何处理的细节请阅读下面的Buffering and Streaming一节。)
服务器或gateway应该把产生的字符串当字节流对待:特别是应该确保行结束不被改变。应用程序负责确保字符串是以与客户端匹配的编码输出(服务器/gateway可能会附加HTTP传送编码,或者为了实现一些HTTP的特性而进行一些转换比如byte-range transmission,更多细节请看下面的Other HTTP Features)
如果调len(iterable)成功,服务器将认为结果是正确的。也就是说,应用程序返回的可迭代的字符串提供了一个有用的__len__()方法,那么肯定返回了正确的结果(关于这个方法正常情况下如何被使用的请阅读Handling the Content-Length Header)
如果应用程序返回的可迭代者有close()方法,则不管该请求是正常结束还是由于错误而终止,服务器/gateway都必须在结束该请求之前调用这个方法,(这是用来支持应用程序对资源的释放,该协议旨在补充PEP 325的生成器支持,以及其他常用的close()方法。
(注意:应用程序必须在可迭代者产生第一个字符串之间请求start_response()可调用,这样服务器才能在发送任何主体内容之前发送响应头,然而这一步也可以在可迭代者第一次迭代的时候执行,所以服务器不能假定开始迭代之前start_response()已经被调用过了)
最后,服务器或gateway不能应用程序返回的可迭代者的任何其他属性,除非是针对服务器或gateway特定类型的实例,比如wsgi.file_wrapper返回的“filewrapper”(阅读Optional Platform-Specific File Handling)。通常情况下,只有在这里指定的属性,或者通过PEP 234迭代API访问的属性才是可以接受的。
environ变量environ Variables
environ字典被用来包含这些在Common Gateway Interface specification中定义了的CGI环境变量。下面这些变量必须存在,除非其值是空字符串,这种情况下如果下面没有特别指出的话他们可能会被忽略。
- REQUEST_METHOD:HTTP请求的方式,比如”GET”或者”POST”。这个不可能是空字符串并且也是必须给出的。
- SCRIPT_NAME:请求URL中路径的开始部分,对应应用程序对象,这样应用程序就知道它的虚拟位置。如果该应用程序对应服务器的“根”的话,它可能是为空字符串。
- PATH_INFO“`html
请求URL中路径的剩余部分,指定请求的目标在应用程序内部的虚拟位置。如果请求的目标是应用程序跟并且没有 trailing slash 的话,可能为空字符串。 - QUERY_STRING:请求URL中跟在”?”后面的那部分, 可能为空或不存在。
- CONTENT_TYPE:HTTP请求中任何 Content-Type 域的内容。
- CONTENT_LENGTH:HTTP请求中任何 Content-Length 域的内容。可能为空或不存在。
- SERVER_NAME, SERVER_PORT:这些变量可以和 SCRIPT_NAME、PATH_INFO 一起组成完整的URL。然而要注意的是,重建请求URL的时候应该优先使用 HTTP_HOST 而非 SERVER_NAME。详细内容请阅读下面的 URL Reconstruction。SERVER_NAME 和 SERVER_PORT 永远是空字符串,也总是必须存在的。
- SERVER_PROTOCOL:客户端发送请求所使用协议的版本。通常是类似”HTTP/1.0″或”HTTP/1.1″的东西可以被用来判断如何处理请求 headers。(既然这个变量表示的是请求中使用的协议,而且和服务器响应时使用的协议无关,也许它应该被叫做 REQUEST_PROTOCOL。然后,为了保持和 CGI 的兼容性,我们还是使用已有的名字。)
- HTTP_Variables:对应客户端提供的 HTTP 请求 headers(也就是说名字以”HTTP_”开头的变量)。这些变量的存在与否应该和请求中的合适的 HTTP header 一致。
服务器或 gateway 应该尽可能提供其他可用的 CGI 变量。另外,如果用了 SSL,服务器或 gateway 也应该尽可能提供可用的 Apache SSL 环境变量,比如 HTTPS=on 和 SSL_PROTOCOL。不过要注意,使用了任何上面没有列出的变量的应用程序对不支持相关扩展的服务器来说就有点 necessarily non-portable。(比如,不发布文件的 web 服务器就不能提供一个有意义的 DOCUMENT_ROOT 或 PATH_TRANSLATED。)
一个符合 WSGI 的服务器或 gateway 应该在描述它们自己的同时说明它们可以提供些什么变量应用程序应该对所有他们需要的变量的存在性进行检查,并且在某变量不存在的时候有备用的措施。
注意:不需要的变量(比如在不需要验证的情况下的 REMOTE_USER)应该被移出 environ 字典。同样注意 CGI 定义的变量如果存在的话必须是字符串。任何 str 类型以外的 CGI 变量的存在都是对本规范的违反。
除了 CGI 定义的变量,environ 字典也可以包含任意操作系统的环境变量,并且必须包含下面这些 WSGI 定义的变量:
变量 | 值 |
wsgi.version | 元组(1, 0)表示 WSGI 版本 1.0 |
wsgi.url_scheme | 表示正在调用应用程序的 URL 的“方案”部分的字符串。通常情况下,这将有适当的值“http”或“https”。 |
wsgi.input | 一个输入流(文件类对象),HTTP 请求体可以从中读取。(服务器或 gateway 可以根据应用程序的请求执行按需读取,或者可以预先读取客户端的请求主体并将其缓存在内存中或磁盘上,或者使用任何其他技术来提供这样的输入流根据自己的喜好)。 |
wsgi.errors | 一个输出流(类文件对象),为了在一个标准化的,可能集中的位置记录程序或其他错误,可以写入错误输出。这应该是一个“文本模式”流,即应用程序应该使用“n”作为行尾,并假定它将被转换为由服务器/gateway 结束的正确行。
对于许多服务器,wsgi.errors 将是服务器的主要错误日志。或者,这可能是 sys.stderr,或某种类型的日志文件。服务器的文档应该包括如何配置这个或在哪里找到记录输出的解释。如果需要,服务器或 gateway 可以向不同的应用程序提供不同的错误流。 |
wsgi.multithread | 如果应用程序对象可能被同一进程中的另一个线程同时调用,则此值应为真,否则应为 false。 |
wsgi.multiprocess | 如果一个等价的应用程序对象可能被另一个进程同时调用,则此值应为真,否则应为 false。 |
wsgi.run_once | 如果服务器或 gateway 希望(但不能保证!)该应用程序在其包含的进程的生命周期中只被调用一次,则该值应该为真。通常情况下,对于基于 CGI(或类似的)的 gateway 来说,这只会是真的。 |
最后 environ 字典也可以包含服务器定义的变量。这些变量的名字必须是小写字母、数字、点和下划线,并且应该带一个能唯一代表服务器或 gateway 的前缀。例如,mod_python 可能会定义名称为 mod_python.some_variable 的变量。
输入和错误流 input and Error Streams
服务器提供的输入输出流必须提供以下的方法:
Method | Stream | Notes |
read(size) | input | 1 |
readline() | input | 1, 2 |
readlines(hint) | input | 1, 3 |
__iter__() | input | |
flush() | errors | 4 |
write(str) | errors | |
writelines(seq) | errors |
以上方法语义同 Python Library Reference 相同,处理下面列表指出的注意点之外。
- 服务器不一定需要读取客户端指定的 Content-length, 它可以模拟一个流结束(end-of-file)的条件,如果应用程序试图去读取它的话。另外应用程序不应该去尝试读取比指定 Content-length 长度更多的数据。
- 可选参数 size 是不支持用于 readline() 方法中的。因为它有可能是服务器程序作者复杂的实现,而且它不怎么用的上。
- 注意这个用户 readlines(hint) 中的参数 hint,对于调用方和实现方都是可选的。应用程序可以自由选择是否支持,服务器也可以自由选择是否忽略它。
- 由于错误流可能不能回转(rewound),服务器和 gateway 可以立即自由的不需要缓存地向前写(forward write)。在这种情况下 flush() 方法可能是个空操作(no-op)。然后可移植的程序不能假定这个 output 是无缓冲的或者 flush 是空操作。在需要确保数据流被真正写入的情况下,他们大多都会调用 flush()。(例如:从多个进程中写入同一个日志文件的时候,可以做到最小化的数据交织)
上表列出的方法,每个服务器都必须符合此规范。应用程序方也需符合此规范,不要用其他的其他不同方法或属性的 input 和 errors 对象。尤其,应用程序端不要试图去关闭这些流,即使他们有 close() 方法。
start_response()可调用 The start_response() Callable
“`传递给应用程序的第二个参数是一个可调用的形式,start_response(status, response_headers, exc_info=None)。(同所有的WSGI调用,参数位置必须是通过位置对应,而不是用 key 来对应)。
start_response 调用被用来开始 HTTP 响应,并且必须返回一个 write(body_data) callable (参考下面的 buffering and streaming 节)
status 参数是 HTTP 的 ‘status’ 字符,如 “200 OK”, “404 Not Found”。也就是说,它是一个由状态码和理由词组组成的字符串,由一个空格分开,没有周围的空格或其他字符。(有关更多信息,请参见 RFC2616 第 6.1.1 节。)字符串不能包含控制字符,也不能以回车,换行符等其他组合结束的符号。
response_headers 参数是(header_name,header_value)元组的列表。它必须是一个 Python 列表,即类型(response_headers)是 ListType,并且服务器可以以其期望的任何方式改变其内容。每个 header_name 必须是一个有效的 HTTP 标题字段名称(由 RFC2616,第 4.2 节定义),没有尾部冒号或其他标点符号。
所有的 header_value 不能包含任何控制字符,包括回车和换行。(这样的要求是为了方便那些必须检查相应头的服务器,gateway,中间件,使他们所必须的解析工作复杂度降到最低)
一般来说,服务器或 gateway 负责确保正确的头信息发送到客户端,如果应用程序(application)遗漏了必要的头信息(或其他相关的规范信息),服务器或 gateway 须补上。比如:HTTP date: 和 Server: 头信息通常是由服务器或 gateway 提供。
(一个要提醒给服务器/gateway 作者的事情: HTTP 头名称是区分大小写的,所以在检查 application 提供的头信息时一定要考虑大小写的问题)
应用程序和中间件禁止使用 HTTP/1.1 的 ‘hop-by-hop’ 特性或头信息,任何在 HTTP/1.0 中等价或类似的特性或头信息,都会影响到客户端和服务器的持久的连接。这些功能是实际 web 服务器的独占区域,服务器或 gateway 应将应用程序尝试发送这些功能视为致命错误,并在将它们提供给 start_response() 时引发错误。(为了解更多 ‘hop-by-hop’ 的细节和特性,请参阅下面的 Other HTTP Features 段)。
该 start_response 调用不能实际传输响应头。相反,它必须将它们存储在服务器或 gateway,只有在产生一个非空字符串的应用程序返回值的第一个循环之后发送,或者在应用程序的第一个的调用 write() 方法调用的时候。换句话说,response 头不能被发送在没有实际的 body 数据前,或者是应用程序返回的迭代器终止前。(唯一的例外就是 response 头信息里明确包含了 Content-Length 为 0。)
响应头传输的延迟这是为了确保有缓存或异步的应用程序能用出错信息替换掉原来可能要发送的数据,直到最后一刻。例如,应用程序可能需要从“200 OK”响应状态改变为“500 内部错误”,如果当正在应用缓冲器内产生的 body 发生错误。
exc_info 参数是可选的,如果提供的话,必须是一个 Python sys.exc_info() 元组。此参数只有在 start_response 请求错误 handler 才要求被提供。start_response 应与新提供的那些替换当前存储的 HTTP 响应头,从而使得当发生错误有关输出应用程序“改变主意”。
然而,如果 exc_info 被提供,并且 HTTP 头已经被发送,start_response 必须抛出错误,并且抛出 exc_info tuple,也就是下面的
raise exc_info[0], exc_info[1], exc_info[2]
抛出的异常会被应用程序重新捕获到,原则上应到终止应用程序。(应用程序会尝试发送 error output 到浏览器,一旦 HTTP headers 被发送,这样是不安全的)应用程序必须不捕获任何来自于 start_response 调用的异常,如果调用 start_response 用来 exc_info 参数,替代的做法应该允许这样的异常传回给服务器/gateway。更多信息见下面的 Error Handing。
应用程序可能多次调用 start_response 当且仅当 exc_info 参数提供的时候。更确切的说,如果 start_response 已经被当且应用程序调用过后,再次调用没有 exc_info 参数的 start_response 是个很致命的错误。(参考上面 CGI gateway 示例,其中说明了正确的逻辑)
注意:服务器/gateway/中间件实现 start_response 应当确保 exc_info 没有持续指向任何引用,当 start_response 方法调用完成之后。为了避免通过 traceback 和 frames involved 创建了回环的引用,最简单的例子如下:
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 gateway 例子提供了这种技术的另一个说明。
处理 Content-Length 头信息 Handling the Content-Length Header
如果应用程序没有提供 Content-Length 头,服务器/gateway 可以选择几种方式之一来处理它,最简单的方式便是当 response 完成的时候关闭客户端连接。
然而在某些情况下,服务器/gateway 之一可能会生成 Content-Length 头,或者至少避免了要关闭客户端连接,如果应用程序没有调用 write(),并返回一个 len() 是 1 的 iterable,那么服务器通过第一个 iterable 出来的字符串。便可以自动的确定 Content-Length 长度。
如果服务器和客户段都支持 HTTP/1.1 “chunked encoding”, 那么服务器可能在调用 write() 或迭代字符串的时候使用 chunked encoding 来发送块数据,因此会为每个 chunk 块数据生成 Content-Length。这允许服务器保持客户端长连接如果需要的话,注意如果真要这么做的话,服务器必须完全符合 RFC2616 规范,否则便回到了另外的策略上来处理 Content-Length 的缺失。
(注意:应用程序必须不能提供适合任何类型的 Transfer-Encoding 输出,像 chunking or gzipping, as “hop-by-hop” 操作,这些编码都是实际的服务器/gateway 的职权。详细信息参见下面的 Other HTTP Features 一节)
缓冲和流 Buffering and Streaming
一般而言,应用程序都会通过缓存 output 并且一次性发送来提高吞吐量。Zope 框架就有按此流程进行处理的:它通过缓存 output 到 StringIO 或类似的对象里面,并且顺着 response 响应头一次性发送。
在 WSGI 中相应的处理方法是应用程序简单地返回一个单元素迭代(single-element iterable)如列表 list,其中包含单一的 response body 字符串。这是对于绝大多数应用程序都推荐的方案,渲染 HTML 页面的文本很容易存放在内存中。
然后对于大文件或专门用途的 HTTP 流媒体(如“服务器推送”),应用程序或许需要提供小块状的输出(比如为了避免加载一个大的文件到内存中),有些时候部分的 response 可能需要花费更多的时间来生成,但是在它之前发送一部分的数据还是很有用的。
这种情况下,应用程序通常会返回一个可迭代的(通常是迭代生成器),使生成的输出是一块一块的。这些块大小可能会超过最大的边界(“服务器推”),或者在某个耗时的任务之前(比如在磁盘中读取其他块)。
WSGI 服务器/gateway 和中间件不能延迟传输任何块。他们要么完全将所有的块都传输给客户端,或者保证他们会继续传输即使应用程序正在生成下一个块。服务器/gateway 或中间件可能会提供以下三种方案中的一种:
- 发送整个块到操作系统(要求任何的 O/S 缓存被刷新)在返回控制之前给应用程序
- 使用不同的线程确保块被继续传输,而应用程序生成下一个块。
- (仅中间件)发送整个块到它的父服务器/gateway
通过提供这样的保证措施,WSGI 就可以允许应用程序确保在他们输出数据的任意点上不会变得停滞。这对正常运行是至关重要的,例如多媒体服务器推送流,多个边界间的数据应该被完整的传输到客户端。
中间件处理块边界 Middleware Handling of Block Boundaries
为了更好地支持异步应用程序和服务器,中间件组件不能阻止迭代从一个应用程序可迭代等待多个值。如果中间件需要在自己能生成输出流之前积攒从应用程序中的值,那么它必须 yield 一个空字符串。或者达到这种需求的其他方式,一个中间件必须每次在它底层应用程序生成一个值的时候至少生成一个值,如果中间件不能产生其他值那么它必须生成一个空字符串。
这样的要求确保了异步的服务器和应用程序能共同协作,在需要同时提供多个应用程序实例的时候减少线程的数量。
注意,这样的要求意味着中间件必须返回一个 iterable 一旦其底层应用程序返回一个 iterable。它也不允许中间件调用 write() 方法来发送数据。中间件仅可能用它父服务器的 write() 方法来传输数据,它底层的应用程序利用中间件提供的 write() 发送数据。
The write() Callable
一些现有框架 API 与 WSGI 的一个不同处理方式是他们支持无缓存的输出,特别指出的是,他们提供一个 write 函数或方法来写一个无缓冲的块或数据,或者他们提供一个缓冲的 write 函数和一个 flush 机制来 flush 缓冲。
不幸的是,这样的 API 无法实现像 WSGI 这样应用程序可迭代返回值,除非使用多线程或其他的机制。
因此为了允许这些框架继续使用这些必要的 API,WSGI 包含一个特殊的 write() 调用,由 start_response 调用返回。
新的 WSGI 应用程序和框架如果可以避免不应该使用 write() 调用。这个 write() 调用是完全 hack 用来支持必要的流能力的 API。一般来说,应用程序应该通过返回的 iterable 来生成输出流,因为这样可以让 web 服务器交错在同一个 Python 的其他线程上,总的来说可以为服务器提供更大的吞吐量。
这个 write() 调用是由 start_response 调用返回的,并且它接受一个单一的参数:一个将用来写入到 HTTP 一部分 response 体中的字符串,它被看作是已经被迭代生成后的结果。换句话说,在 writer() 返回前,它必须保证传入的字符串要么是完全发送给客户机,或者已经缓冲了应用程序要向前的传输。
一个应用程序必须返回一个可迭代的对象,即使它使用 write() 来生成全部或部分 response body。返回的可迭代可以是空的(例如一个空字符串),但是如果它不生成空字符串,那么 output 必须被处理,通常是由服务器/gateway 来做(例如它必须立即发送或加入队列中)。应用程序必须不在他们返回的 iterable 内调用 write()。因此所有由 iterable 生成的字符串会在传递给 write() 之后发送到客户端。
Unicode 问题 Unicode Issues
HTTP 不直接支持 Unicode,并且也没有这样的接口。所有的编码解码必须由应用程序来处理,所有传递给服务器的都必须是 Python 标准的字节字符串而不是 Unicode 对象。使用其中在需要字符串对象一个 Unicode 对象的结果,是不确定的。
注意所有的作为 response 头的字符传递到 start_response 必须遵循 RFC2616 的编码,也就是它们必须是 ISO-8859-1 字符,或者使用 RFC2047 MIME 编码。
Python 平台的 str 或 StringType 实际上是基于 Unicode (如 Jython, ironPython, python3000 等),所有的字符串都只包含 ISO-8859-1 编码规范中可表示的代码点(从 \u0000-\u00FF)。如果应用程序提供的字符串包含任何 Unicode 字符或编码点,这将是个致命的错误。同样地,服务器/gateway 也必须不提供任何 Unicode 字符到应用程序。
再次声明,本规定所有的字符串都必须是 str 或 StringType,并且不能是 unicode 或 UnicodeType。尽管一个开发的平台允许超过 8 位字节的字符对象,对于本规范所指定的字符串来说,也仅仅是低 8 位的字节被用到。
错误处理 Error Handling
一般来说,应用程序应到捕获自己的内部错误,并显示有用的信息到浏览器上。(由浏览器来决定哪些信息是有用的)
然后要显示这些信息,应用程序不能实际地发送任何数据到浏览器上,否则会破坏 response。因此 WSGI 提供了一种机制,要么允许应用程序发送它自己的错误信息,要不自动终止:这个 exc_info 参数 start_response。这里有个如何使用的例子。
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 将正常返回,并且应用程序返回的包含错误信息的 body 将被发送到浏览器。然而如果有任何的 output 被发送到浏览器,start_response 会重新抛出提供的异常。此异常不应该被应用程序捕获,并且应用程序会终止。服务器/gateway 会捕获异常这个致命的异常并终止响应。
服务器应该捕获任何异常并记录日志当终止应用程序或反复迭代他们的返回值。如果当应用程序出错的时候已经有一部分 response 被发送到浏览器了,服务器或 gateway 可以试图添加一个错误消息到 output,如果已经发送了指明 text/* 的头信息,那么服务器就知道如何修改清理。
一些中间件或许希望提供额外的异常处理服务,或拦截并替换应用程序的错误信息。在此情况下,中间件可能选择不再重新抛出提供的 exc_info 到 start_response,但换作抛出中间件特制的错误,或者在存贮了提供的参数后简单地返回且不包含错误。这将导致应用程序返回错误的 body iterable(或调用 write()。允许中间件来拦截并修改错误流,这些技术只会工作只要应用程序作者做到:
- 总是提供 exc_info 当开始一个错误的响应的时候。
- 当 exc_info 已经提供的情况下不要捕获来自 start_response 的异常。
HTTP 1.1 的长连接 HTTP 1.1 Expect/Continue
实现了 HTTP 1.1 的服务器/gateway 必须提供对 HTTP 1.1 中 “expect/continue”(长连接)的透明支持。可以用下面的几种方式完成。
- 用一个立即执行的 “100 Continue” response 响应一个包含 Expect: 100-continue 的请求,并且正常处理
- 正常处理请求,但提供给应用程序一个 input 流,当应用程序首先尝试从输入流中读取的时候发送一个 “100 Continue” 响应。这个读取必须保持阻塞至到客户端的响应。
- 等待客户端判断服务器不支持 expect/continue 以及客户端自身发来的 request body。(不好的方式,不推荐)
注意这些行为的限制不适用于 HTTP 1.0,或请求不是针对特定的应用程序对象。更多 HTTP 1.1 Except/Continue 的信息,请参阅 RFC2616 的 8.2.3 段和 10.1.1 段。
其他的 HTTP 特性 Other HTTP Features
通常来说,应用程序对其的 output 有完全的控制权,服务器/gateway 对此因不理不问,他们只做一些不影响应用程序响应语义的修改。往往很可能的是为应用程序开发者提供中间件来支持一些特性,所以服务器/gateway 开发者在他们实现的时候应当偏保守。在某种意义上,一个服务器应当使自己更像是一个 HTTP gateway 服务器,配合一个应用程序来作为一个 HTTP “origin server” (这些定义参加 RFC 2616 1.3 段)
然后由于 WSGI 服务器和应用程序不通过 HTTP 通信,RFC 2616 提到的 “hop-by-hop” 在 WSGI 内部通信中不适用。WSGI 应用程序必须不生成任何 “hop-by-hop” 头信息,试图使用 HTTP 的特性需要他们生成这样的头,或者依赖任何传入的 “hop-by-hop” 头内容在 environ 字典中。WSGI 服务器必须自己处理掉任何被支持入站的 “hop-by-hop” 头信息,像传输编码,包括适用的分块编码。
将这些原则应用到各种各样的 HTTP 特性中去,应当清楚一个服务器可能需要处理缓存验证、If-None-Match 和 If-Modified-Sine 的请求头,和一些 Last-Modified 和 ETag 响应头。然而它并不是必须要这样做,并且一个应用程序应到处理自己的缓存验证等,如果它自身相提供这样的特性,然后服务器/gateway 就不需要做这样的验证。
同样地,服务器可能会重新编码或传输编码一个应用程序的响应,但是应用程序应当对自己发送的内容做适当的编码,并且不提供传输编码。如果客户端需要服务器可能以字节码的方式传输应用程序的响应,并且应用程序本身是不支持字节码方式。再次申明,如果需要,应用程序应当自己执行相应的方法。
注意,这些在应用程序上的限制不是要求应用程序为每个 HTTP 特性重新实现一次,许多 HTTP 特性可以完全或部分地由中间件来实现,这样便可以让服务器和应用程序作者在一遍又一遍的实现这些特性中解放出来。
线程支持 Thread Support
线程的支持或缺乏,也是服务器依赖。服务器可以同时运行多个请求,也应当提供让应用程序运行在单一线程上的选项,因此让一些不是线程安全的应用程序或框架仍旧可以在这些服务器上使用。
实现/应用事项 Implementation/Application Notes
服务扩展 API Server Extension APIs
有些服务器作者可能希望提供更多的高级 API,应用程序和框架作者便可以专门的使用。例如一个以 mod_python 为基础的 gateway 可能就希望暴露一些 Apache 的 API 作为 WSGI 的扩展。
在最简单的情况下,它只需要定义一个 environ 变量,类似 mod_python.some_api。但是,更多情况下,可能存在的中间件会使此变得更困难。例如,一个 API 提供的在 environ 变量中访问的 HTTP 头,可能会被中间件修改掉而返回不一样的值。
通常,任何扩展的 API,复制、取代或者绕过部分 WSGI 的功能与中间件会有不兼容的风险。服务器/gateway 开发者不应当假设没人会使用中间件,因为一些框架作者专门的打算重新改写或组织他们的框架代码,使之几乎就像是各种各样的中间件。
所以,为了提供最大的兼容性,服务器/gateway 提供一些扩展的 API,取代一些 WSGI 的功能,必须设计这些 API 以便他们使用部分替换过的 API 调用。例如:一个允许访问 HTTP 请求头的扩展 API 需要求应用程序通过当前的 environ,所以服务器/gateway 可能会验证那些可被访问的 HTTP 头,保证没有被中间件修改过。如果这扩展 API 不能保证,它总是 evniron 和 HTTP 头信息里面的内容,它必须拒绝向应用程序提供服务。例如,通过抛出一个错误,返回 None 代替头信息的收集等等是适合 API 的。
同样地,如果扩展 API 提供交替的方法来写 response 数据或是头信息,它应当要求 start_response 调用在应用程序能获得的扩展的服务之前已被传入。如果传入的对象和最开始服务器/gateway 提供给应用程序的不一样,它不能保证正确的操作并且必须拒绝给应用程序提供扩展的服务。
这些指导方针同样适用于中间件,添加类似解析过的 cookies 信息,表单变量,sessions,或者像 evniron。特别地,这样的中间件应当使提供这些的特性类似一个在 environ 里操作的函数,而不是简单的想 evniron 里面添加些东西。这些有助于保证来自 evniron 里面的信息计算是在任何中间件对 URL 重写过后或者是其他 evniron 变量的修改之后。
或许以后一些中间件作者为了保证应用程序在使用他们的中间件过程中不被跳过,他们不得不从 environ 中删除全部或部分的扩展 API。为了避免这些,这些安全的规则对所有的服务器/gateway 和中间件作者来说都是非常重要的。
应用程序结构配置 Application Configuration
这些规格说明没有定义服务器如何选择如何得到一个应用程序来调用。这些和其他一些配置选项都是高度地服务于服务器的。这些期望服务器/gateway 作者能讲如何配置选项(如线程的选项),如何配置执行一个特定的应用程序对象等文档化。
另一方面,框架作者应当将如何创建一个被框架功能包装过的应用程序对象文档化。用户可以选择服务器和应用程序框架,但必须将两者连接在一起。然而,因为现在有了两者共同的接口,这应该只是一个机械式的问题,而不是为了将新的应用程序或服务器配对组合的重大工程了。
最后,一些应用程序,框架,中间件可能希望使用 evniron 字典来接受一些简单的字符串配置选项。服务器/gateway 应当通过允许应用程序部署者向 evniron 里面指定特殊的名值对来支持这些。在最简单的情况下,这些支持可以仅仅由拷贝操作系统提供的环境变量 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)
但是,大多存在的应用程序和框架可能只需要从 environ 里面一个单一的配置值,用来指示它们的应用程序或框架专门的配置文件位置。(当然,应用程序应当缓存这些配置,以避免每次调用都重复读取)
URL 的构建 URL Reconstruction
如果应用程序希望改造请求的完整 URL,可以使用如下的算法,由 lan Bicking 提供:
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 可能不完全相同 URI 由客户端的请求。服务器重写规则,例如,可能已经修改了客户的最初请求的 URL,以将其放置在一个规范的形式。
支撑的 Python 旧版(<2.2)版本 Supporting Older (<2.2) Versions of Python
有些服务器,gateway 或应用程序可能希望支持 Python 中的较旧的(<2.2)版本。这一点尤其重要,如果 Jython 是一个目标平台,因为在写本书的 Jython 2.2 的生产就绪版本尚未公布。
对于服务器和 gateway,这是相对简单的:面向 Python 2.2 之前版本的服务器和 gateway 必须限制自己只使用标准的“for”循环来迭代应用程序返回的任何 iterable。这是确保与 2.2 版之前的迭代器协议(下面将进一步讨论)和“今天的”迭代器协议(参见 PEP 234)的源代码级兼容性的唯一方法。
(请注意,此技术只适用于用 Python 编写的服务器、gateway 或中间件。关于如何从其他语言正确使用迭代器协议的讨论不在本 PEP 的范围之内。)
对于应用程序,支持 Python 2.2 之前的版本稍微复杂一些:
- 你可能不会返回一个文件对象,并期望它作为一个迭代的工作,因为 Python 2.2 之前,文件没有迭代。(一般来说,您不应该这样做,因为它在大多数情况下性能都很差!)使用 file_包装或应用程序特定的文件包装类。(有关详细信息,请参阅可选的特定于平台的文件处理 wsgi.file_包装,以及可用于将文件包装为 iterable 的示例类。)
- 如果返回自定义 iterable,则它必须实现 2 之前的迭代器协议。也就是说,提供一个接受整数键并在耗尽时引发 IndexError 的方法。(请注意,内置序列类型也是可接受的,因为它们也实现了此协议。)
最后,希望支持 Python 2.2 之前版本、迭代应用程序返回值或本身返回 iterable(或两者都返回)的中间件必须遵循上述适当的建议。
(注意:为了支持 Python 2.2 之前的版本,任何服务器、gateway、应用程序或中间件也必须只使用目标版本中可用的语言功能,使用 1 和 0 而不是 True 和 False 等。)
可选的特别的平台文件处理 Optional Platform-Specific File Handling
一些操作环境提供高性能的文件传输设施,像 Unix 的 sendfile() 方法。服务器和 gateway 可能会通过 environ 中的 wsgi.file_wrapper 选项来暴露这功能。应用程序可以使用此“文件包装器”将文件或类似文件的对象转换为 iterable,然后返回,例如:
if 'wsgi.file_wrapper' in environ: return environ['wsgi.file_wrapper'](filelike, block_size) else: return iter(lambda: filelike.read(block_size), '')
如果服务器或 gateway 提供 wsgi.file_wrapper,它必须是个可调用的,并且接受一个必要的位置参数,和一个可选的位置参数。第一个参数是将被发送的类文件对象,第二个参数是可选的,标示分快大小的建议(这个服务器/gateway 不需要)。这个调用必须返回一个可迭代的对象,并且不能执行任何的数据传输直到服务器/gateway 真正接受到了作为应用程序返回值的迭代对象(否则会阻止中间件解析或覆盖响应体)。
由应用程序提供的被认为是类文件的对象必须有可选大小参数的 read() 方法。它或许有 close() 方法,如果有,那么 wsgi.file_wrapper 必须有一个会调用类文件对象原始 close() 方法的 close() 方法。如果类文件对象有任何的方法或属性与 Python 内置的文件对象的属性或方法名相同(例如 fileno()),那么 wsgi.file_warpper 可能会假定这些方法和属性与 Python 内置的语义是相同的。
实际在任何特定平台上实现的文件处理必须发生在应用程序返回之后,并且服务器/gateway 检查是否是一个包装对象被返回。(再次声明,因为存在的中间件,错误处理等等类似的东西,它不保证任何生成的包装会被实际使用)
除了处理 close(), 从语义上讲,应用程序返回一个包装的文件应该和返回 iter(filelike.read, ”) 一样。换句话说,当传输开始的时候,应当从文件的当前位置开始传输,并且继续直到最后完成。
当然,特定平台的文件传输 API 通常不接受随意的类文件对象,所以,一个 wsgi.file_wrapper 为了判断类文件对象是否适应于他们特定的平台,不得不对提供的对象做一些像 fileno() (Unix-like OSes) 或者是 java.nio.FileChannel (在 Jython 下) 的自省检查。
注意:即使对象不适用与特定的平台 API,wsgi.file_wrapper 必须仍旧返回一个包装了 read() 和 close() 的迭代,因此应用程序使用这文件包装器便可以再不同平台间移植。这里有个简单的平台无关的文件包装类,适应于老(2.2 之前)的和新的 Python,如下:
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 的服务器/gateway 代码片段。
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()
QA 问答 Questions and Answers
为什么 evniron 必须是字典?用它的子类会有什么问题呢?
用字典的原理是为了最大化满足在服务器间的移植性。另一种选择就是定义一些字典的子集,用字典的方法作为标准的便捷的接口。然而事实上,大多服务器可能只需要找到一个字典就足够了,并且框架的作者会预计完整的字典特性可用,因为多半情况是这样的。但是如果一些服务器选择不使用字典,那么将可能会有互用性的问题出现尽管服务器给出了一致性的规范。因此使用强制的字典简化了规范和互相的验证确保。注意这些不妨碍服务器或框架开发者向 evnrion 字典里加入自定义的变量来提供特别的服务。这是提供任何 value-added 服务推荐的方式。
为什么你能执行 write() 并且 yield 字符串/返回一个迭代器?我们不应该选择一种做法吗?
如果我们仅仅使用迭代的做法,那么现存的框架面临可用的”push”被恶化。如果我们只支持通过 write() 推送,那么服务器在传输大文件的时候性能将恶化(如果一个工作线程没有将所有的 output 都发送完成,那么将无法进行下一个新的 request 请求)。因此,这种平衡允许应用程序支持两个方法,视情况而定,比起需要 push-only 的方式来说,只给服务器实现者一点负担。
close() 是什么?
当 writes 在应用程序执行期间完成,应用程序可以通过 try/finally 块来保证资源被释放。但是如果应用程序返回一个迭代,直到迭代器被垃圾收集器收集前任何资源都不会被释放。约定俗成的 close() 允许应用程序在结束一个 request 请求时释放临界资源,并且它向前兼容于在 PEP 325 中被提议的在迭代器中的 try/finally.
为什么这个接口如此低级?我如果想加入一些特性呢(例如 cookies, sessions, persistence,…)这并不是另一个 python 的 web 框架,这仅仅是一个框架和 web 服务器之间通信的方法,反之亦然。如果你想使用这些特性,你需要选择一个提供这些你想要的特性的框架。并且如果这些框架让你创建一个 WSGI 应用程序,你应当能让它在大部份支持 WSGI 的服务器上运行。同样,一些 WSGI 服务器可能通过在他们提供的 environ 字典里的对象来提供一些额外的服务,可以参阅这些服务器适当的文档了解详情。(当然,使用这样扩展的应用程序将无法移植到其他的 WSGI-based 服务器上)
为什么使用 CGI 的变量代替好的老 HTTP 头,并且为什么让他们和 WSGI 定义的变量混合在一起呢?
许多现存的框架很大程序上是建立在 CGI 规范的基础上的,并且现存的 web 服务器知道如何生成 CGI 变量。相比之下,一些可选的方式来呈现 HTTP 信息会是分散破碎的并且缺乏市场。因此使用”CGI 标准”看起来是个好的办法来利用现有的实现。对于将他们同 WSGI 变量混合在一起,分离他们的话会导致需要两个字典参数需要传入,这样做没什么好处。
关于状态字符串,我们可不可以仅仅使用数字来代替,像用 200 代替”200 OK”?
这样做会使服务器或 gateway 被复杂化,因为他们需要一个字符串与数值的映射表。相比之下,应用程序或框架作者在他们输入一些额外的信息到他们处理细节的响应代码中要简单,并且现存的框架时常有一个表包含这些必要的信息。所以,总体来说这个让应用程序/框架来负责要比服务器或 gateway 来负责要好。
为什么 wsgi.run_once 不能保证 app 只运行一次?
因为它只不过是建议应用程序应当”装配稀少地运行”。这是用于应用程序框架用不同的模式来操作缓存,sessions 等待。在”multiple run”模式下,这样的框架可能会预先装载缓存,并且在每个 request 请求之后不会写操作,如写入 logs 或 session 数据到硬盘上。在”single run”模式下,这样的框架避免预加载,避免每个 request 请求之后 flush 所有必要的写操作。然而,为了验证在后者的模式下应用程序或框架的正确动作,可能会必要地(或是权宜之计)调用它不止一次。因此,一个应用程序应当不能仅仅因为设置了 wsgi.run_once 为 True 就假定它肯定不会再运行,
Feature x (dictionaries, callables, etc.) 对于应用程序代码来说是很丑陋的,我们可以使用对象来代替吗?
WSGI 所有的这些选择实现都是为了从另一个特性中解耦合;重新组合这些特性到一个封装的对象中会使更难写服务器/gateway,并且会使书写中间件来代替或修改一部分整体的功能的难度上一个数量级。本质上,中间件希望有个”责任链”模式,凭这个对于一些功能来说它可以看作是一个”handler”,而让其他东西保持不变。这用普通的 python 对象是非常难的,如果这接口是保持可扩展性的。例如,一个必须使用 __getattr__ 或者 __getattribute__ 的覆盖,来确保这些扩展(比如一个 WSGI 未来的版本定义的一个变量)是被通过的。这种类型的代码是出了名的难以保证 100% 正确,有些人可能会自己重写。他们会因此复制其他人的实现,但是从其他已经正确的案例中复制过来的时候却未能成功更新。进一步讲,这样必要的样本将纯碎 excise, 一个开发者为了能让应用程序框架得到更漂亮的 API 而向中间件开发者支付税务。但是应用程序框架开发者会通常只升级一个框架用来支持 WSGI,并且对于框架整体来说只是非常小的一部分。这可能会成为他们第一次(甚至是最后一次)的 WSGI 实现,并且因此他们可能实现这些规范起来伸手可及。因此,努力使使用对象属性创造出来的 API 漂亮这类的做法,可能会遗失掉这些支持者。我们鼓励那些希望有漂亮的(或者是改进的)WSGI 接口的人,对直接开发 web 应用程序项目(与 web 框架开发)的用户开发为方便应用程序开发者而包装过 WSGI 的 API 或框架,通过这种方式,WSGI 可以保持对服务器或中间件的便利性,而不是对应用程序作者”丑陋”。
建议/讨论 Proposed/Under Discussion
这些项目目前正在在 Web-SIG 讨论和其他地方,或者是 PEP 作者的“待办事项”列表:
- 如果 input 是一个迭代器,而不是一个文件?这将有助于为异步应用和编码分块输入流。
- 可选的扩展正在用于暂停的应用程序的输出的迭代直到输入可用,或者直到一个回调发生讨论。
- 添加在这些领域进行同步和异步应用程序和服务器,相关的线程模型和问题/设计目标部分。
致谢 Acknowledgements
感谢 WebSIG 邮件列表上的许多人,他们周到的反馈使修订草案成为可能。尤其是:
- Gregory “Grisha” Trubetskoy, mod_python 的作者,他抨击初稿没有提供比“普通的 CGI”任何优势,因此鼓励我寻找更好的方法。
- Ian Bicking,他帮助我正确地指定多线程和多进程选项,并要求我提供一种机制,让服务器向应用程序提供自定义扩展数据。
- Tony Lownds 提出了一个 start_response 函数的概念,该函数接收状态和头,并返回一个 write 函数。他的输入还指导了异常处理设施的设计,特别是在允许覆盖应用程序错误消息的中间件方面。
- alankennedy 勇敢地尝试在 Jython 上实现 WSGI(早在规范最终确定之前)帮助形成了“支持 Python 的旧版本”部分以及可选的 file_包装设施。
- marknottingham,他对规范进行了广泛的审查,发现了 http rfc 遵从性方面的问题,特别是关于 HTTP/1.1 特性的问题,直到他指出这些问题时,我才知道这些特性的存在。
参考 References
- Python 的维基“Web 编程”主题
- 通用 gateway 接口规范,版本 1.1,第 3 草案
- “分块传输编码”-HTTP/1.1,3.6.1 节
- “端到端和逐跳包头”-HTTP/1.1,第 13.5.1
- mod_ssl 的参考,“环境变量”
参考链接: