WSGI web服务网关接口(3)

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

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

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

缓冲与流

一般来说,应用程序会通过缓冲其输出(适度的大小)并一次性全部输出来实现最大的吞吐量。这是现有框架很常见的一种方法,比如在Zope中,输出被缓存在一个StringIO的对象或类似的对象中,并随着响应头一起传输给客户端。

在WSGI中相应的方法是将包含响应主体的单元素可迭代对象(比如列表)作为一个字符串返回,对于绝大多数的应用程序来说,这是推荐的方法,它可以很容易的实现HTML文本在内存中的渲染。

对于大文件,或者是特别指明需要利用HTTP流的(比如服务端推送),框架端可能需要更小块的输出(避免一次加载大文件进入内存)。有时候也会出现一部分响应很耗时的情况,此时,我们可以选择提前将已经可以响应的部分先发出去。

在这些情况下,应用程序通常会返回一个迭代器(通常是一个生成器迭代器),以逐块方式生成输出。这些块在某些情况下也可能会被分割开来用多块进行传输(服务端推送)或先于耗时任务前发送(比如读取硬盘上的某一块的文件)。

服从WSGI规范的服务器、网关和中间件不能延误任何块的传输;它们必须保证完整的把整个块传送给客户端,或者在保证框架端产生下个块时,持续的进行传输。服务端和中间件可以通过以下三个方法之一来提供保证:

  1. 在把控制权交给框架前,把整个块发送给操作系统(并请求清空所有的O/S缓冲)。
  2. 用另外一个线程来保证应用程序在生成下个块时,持续的进行传输。
  3. 发送整个块给服务端(仅适用于中间件)。

通过这些保证,使得应用程序的输出不会在任意一个时刻停滞。这对于多路服务推送功能的实现是至关重要的。多个块边界之间的数据应该完整的传送给客户端。

块边界的中间件处理

为了更好的支持异步框架和服务端,中间件不能阻止块的迭代去等待框架端的多个返回值。如果中间件需要在产生输出之前累积来自应用程序的数据,那么必须产生一个空字符串。

换个方式来理解的话,应用程序产生一个值,中间件也必须产生到一个值。如果中间件不能产生任何值的话,就产生一个空的字符串。

这个需求是为了确保异步应用程序和服务端可以在一起确定,是否需要减少运行给定数量应用程序实例的线程个数。

还要注意,这个需求意味着中间件必须在应用程序返回一个可迭代时立即返回一个迭代对象。它也禁止中间件直接用write()函数来传输底层应用程序返回的数据。中间件仅能使用它父服务端提供的write方法,而应用程序则使用由中间件提供的write方法。

可调用的write()函数

一些现有的应用程序框架API以一种不同的方式提供了无缓冲数据的输出。比如,它们提供了write方法和函数来支持无缓冲块数据的输出,或者提供了write方法写入数据到缓冲区,以及flush机制来清空缓冲区。

不幸的是,这些API并没有实现应用程序端返回可迭代对象的功能,除非使用了线程或者其他特殊的机制。

因此,为了使得现有的框架继续使用现有的API,WSGI规范必须包含特殊的write方法,并由start_response方法返回。

如果可以避免的话,新的应用程序和框架不应该再继续使用write方法。write方法仅仅是提供了一种比较hack方式给那些现在迫切需要使用的API,一般来说,应用程序的输出应该是一个可迭代的对象,这也使得服务端可以在同一个线程中交错的执行其他的任务,潜在的为服务端提供了更好的吞吐量。

write方法由start_response方法返回,它只接收一个参数:作为响应主体的一个字符串,我们可以把它看作是已经迭代输出以后的结果。换句话说,在write方法返回前,它必须保证其是需要传给用户的完整数据,或者在应用程序继续进行时被缓冲并用于传输。

一个应用程序必须返回可迭代对象,即使使用write方法来产生所有或者部分的响应主体。返回的可迭代对象可能为空,但是一旦产生非空字符串,输出必须可以被服务端正常处理(例如:它必须被立即发送或者放入队列),应用程序不能在它们返回的迭代器中调用write方法。因此,在所有字符串都被传给write方法并发送给客户端后,由迭代对象产生的任何字符串也被传输。

统一编码问题

HTTP并不直接支持统一的编码,这个规范也同样不支持。所有的编/解码问题必须由应用程序处理,而所有由服务端传入的字符串都必须是标准的Python字节流,不是Unicode对象。在一个需要字符串的地方传入一个Unicode对象,会导致未定义的错误。

注意,无论是作为状态传给start_response函数的字符串还是响应头都必须遵守RFC 2616的编码规范。也就是说,它必须是ISO-8859-1字符,或者使用RFC 2047 MIME编码方式。

在一些str或StringType基于Unicode编码的python平台上(比如,Jython,IronPython, Python3000等),这篇规范中提到的所有字符串,指仅包含可用ISO-8859-1编码表示的码点(从\ u0000到\ u00FF,其中包括\u00FF)。提供任何包其他Unicode字符或码点的应用程序都是错误的。类似的,服务端不能提供包含其他Unicode字符的字符串给应用程序。

需要再次强调的是,所有这里提到的字符串,都是指str或StringType类型的,而不是unicode或UnicodeType。即使给定的平台允许在一个str或StringType对象中一个字符超过8个bit位,在本规范中提及到的字符串,也仅仅只使用其低8位。

错误处理

一般来说,应用程序只能捕捉它们自己的或是内部的一些错误,并且在浏览中给出一些有用的提示信息(这里的提示信息取决于应用程序觉得在特定的上下文环境中哪些是有用的)。

不过,如果要显示这些错误信息,不能直接把这些信息发送客户端的浏览器,这有可能会扰乱响应。因此WSGI规范提供了一种机制,允许应用程序发送错误消息或自动丢弃:start_response的exc_info参数。这里是使用它的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
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会正常返回,由应用程序返回的错误会被发送给浏览器。不过,如果在异常发生时,已经有输出发送给了客户端,start_response应该抛出异常。这个异常不应该由应用程序捕捉,而应该中止。服务端则可以捕捉这个异常并停止响应。

服务端应该捕捉和记录那些导致应用程序以及返回值迭代中止的异常。如果应用程序发生错误时,部分响应已经发送给客户端,并且服务器知道如何修改Content-Type为Text/*类型的输出,那么服务器可能会尝试向输出添加错误消息。

一些中间件可能希望提供额外的异常处理、以及应用程序错误消息的拦截和替换。这种情况下,中间件可能会选择不抛出提供给start_response的exc_info错误,而是用一个特定的中间件错误来代替或是在记录异常之后正常返回而不做任何的处理。这也会导致应用程序返回其可迭代的错误主体,并允许其被中间件捕捉和修改。此外,中间件也必须遵循以下与应用程序作者一样的规范:

  1. 在准备错误响应时,提供exc_info参数。
  2. 当提供exc_info参数时,不要捕捉由start_response抛出的错误。

HTTP 1.1中的Expect/Continue

实现对HTTP1.1支持的服务端必须对HTTP1.1的Expect/Continue进行支持。这可以通过以下几种方式完成:

  1. 对于包含Expect: 100-continue请求头的请求,即时的给出“100-continue”响应,并继续处理。
  2. 正常处理请求,在应用程序端尝试从输入流中读取时,提供wsgi.input给应用程序并发送“100-continue”响应。读取请求必须保持阻塞,直到客户端响应。
  3. 等到客户端意识到服务端不支持expect/continue,并自行发送请求主体。(次优方案,不推荐)

注意这些行为限制不适用于HTTP 1.0请求,也不适用于不针对应用程序的请求。如果想要了解更多关于expect/continue的信息,请参阅RFC 2616的8.2.3及10.1.1两节。

其他的HTTP特性

一般来说,服务端不应该干涉应用程序对输出的控制。它们只能做出不会对应用程序响应语义做出改变的修改。应用程序的开发人员总是可以通过添加中间件组件来提供额外的功能。所以服务端开发人员应该相对保守一些,从某种意义上来说,一个服务器应该就像是一个真正的“网关服务器”,而应用程序才是真正的“源服务器”。参阅RFC 2616的1.3节获取这些术语的定义。

不过,因为服务器和应用程序之间并不通过HTTP协议进行通讯,因此RFC 2616中提到“hop-by-hop”头部并不适用于WSGI的内部通信。服从WSGI规范的应用程序不能产生“hop-by-hop”头部,如果想要使用该特性,必须依赖由eviron传入的“hop-by-hop”中的内容。WSGI服务端必须独立实现对“hop-by-hop”的支持,例如通过解码任何传输编码,包括分块编码(如果适用的话)。

这些原则也同样适用于HTTP的其他特性,如服务端可能处理包含 If-None-Match 和 If-Modified-Since 请求头以及Last-Modified 和 ETag 的缓存验证。不过,服务端并不是一定要支持这个功能,但如果要支持这个功能,应用程序就必须有它自己的缓存验证,因为服务端并没有强制要求去做这个事情。

类似地,服务器可以对应用程序的响应进行重新编码或传输编码,但是应用程序应该自己使用合适的内容编码,但不能应用传输编码。服务端可能会根据客户端的要求按字节范围进行传输,应用程序本身并不支持字节的范围传输,因此如果想实现该功能,应用程序必须单独实现。

注意这些在应用程序上的限制,并不是意味着每个应用程序都必须实现HTTP协议中的所有特性,许多的HTTP特性可以部分实现或者交由中间件来实现,从而使得服务器和应用程序开发者不需要一次又一次的实现相同的功能。

线程支持

是否支持线程,也同样是与服务器相关的,服务端可以以并行的方式运行多个请求同时也提供一个单线程运行的选项,因此那些不是线程安全的应用程序仍然可以跑在服从WSGI规范的服务器上。

Leo wechat
欢迎订阅公众号,建设中!