文章目录
1. 什么是PEP?
PEP是Python Enhancement Proposals的缩写。一个PEP是一份为Python社区提供各种增强功能的技术规格,也是提交新特性,以便让社区指出问题,精确化技术文档的提案。
2. PEP 3333
本文档指定了Web服务器与Python Web应用程序或框架之间的标准接口,以提高Web应用程序和Web服务器之间的可移植性。
Python当前拥有各种各样的Web应用程序框架,例如Zope,Quixote,Webware,SkunkWeb,PSO和Twisted Web。但是,由于不同的Web应用程序框架可能支持不同的Web服务器,所以对于Python新手而言,在选择Web应用程序框架时可能会遇到一些问题。
相比之下,Java也有许多中Web应用程序框架,但是Java的“Servlet" API 使得任何使用Java Web应用程序框架编写的应用程序都能够在支持Servlet API的Web服务器中运行。
Python也需要这样一个接口,这个PEP提出了Web服务器与Web应用程序/框架之间的简单且通用的接口:Python Web服务器网关接口(WSGI)。
仅仅只有一个接口规范是不行的,还需要Web服务器和Web应用程序框架实现WSGI接口才能真正起到作用。
WSGI接口要考虑两个部分:server/gateway端,application/framework端。server端需要调用application所提供的可调用对象。这个可调用对象的提供方式,根据server或gateway的不同而不同,例如:
- 通过application编写脚本来创建server或gatewya实例,并为其提供应用程序对象。
- 通过使用配置文件来指定应该从何处导入应用程序对象。
因此,WSGI定义了两种“字符串”:
- “Native string” 我认为就是原生字符串:用于请求/响应头和元数据(Python3中的str类型,Python2中的unicode类型)
“Bytestrings”(字节串,我认为是字节类型的字符串,字节的序列)(Python 3中的bytes类型,python2中的str类型),用于请求和响应的主体(例如POST / PUT输入数据和HTML页面输出)。
2.1 Application/Framework端
WSG接口规范中所提到的应用程序对象是一个接受2个参数的可调用对象,可以是函数、方法、类或实现了__call__()方法的实例。例如:
HELLO_WORLD = b"Hello world!\n"
# 这是一个应用程序对象
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]
# 这是另外一个应用程序对象
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
2.2 Server/Gateway端
server/gateway每次接收到从HTTP客户端发送的request时,都会调用一次这个应用程序对象。下面用一个例子来证明,
import os, sys
enc, esc = sys.getfilesystemencoding(), 'surrogateescape'
def unicode_to_wsgi(u):
# Convert an environment variable to a WSGI "bytes-as-unicode" string
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:
# Before the first output, send the stored headers
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:
# Re-raise original exception if headers sent
raise exc_info[1].with_traceback(exc_info[2])
finally:
exc_info = None # avoid dangling circular ref
elif headers_set:
raise AssertionError("Headers already set!")
headers_set[:] = [status, response_headers]
# Note: error checking on the headers should happen here,
# *after* the headers are set. That way, if an error
# occurs, start_response can only be re-called with
# exc_info set.
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()
2.3 Middleware:Components that Play Both Sides
这样一个中间件,既可以扮演server端,也可扮演application端。对于server端而言,中间件扮演application;对于application端而言,中间件扮演server。
- 在重写
environ
之后,根据目标UR路由request到不同的应用程序对象。 - 允许多个应用程序/框架同时运行。
- 通过网络转发request和response,实现负载均衡和远程处理。
- perform content postprocessing
一段中间件的示例代码如下:
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))