起步
惭愧啊,惭愧啊,距离上一篇这个系列的文章已经是半年前的了,随着 Django2.0 的发布,感觉之前分析的 1.10.5
版本似乎有点老了,我看了一下,好在和我前面文章分析的内容差异不大,基本上也是可以就着前面的分析内容来品尝最新的 django 代码。
那我接下来阅读的版本就从当前能获取的 2.0.6
来分析了。不过呢,本章要将的内容,可能和 django 代码本身没太多关系。本章来理解一下 WSGI 协议,django 就是遵守这个协议的web开发框架,本章重点是协议方面的说明,顶多会讲讲django里相应的 wsgi 的代码,而不对 django 代码做分析。
什么是 WSGI
WSGI (Web Server Gateway Interface)是用来指定 Web 服务器与 Python Web 应用程序或框架之间标准接口,以促进跨各种Web服务器的Web应用程序可移植性。
在这个规范出来之前,Python 拥有各种各样的 Web 应用程序框架,这也就产生了一个问题,开发者选择Web 框架会限制他们选择web 服务器,反之亦然。
因此,python就提出了一个简单而通用的 Web 服务器与 Web 应用程序之间的接口:Python Web服务器网关接口(WSGI)。
WSGI 的目标是促进现有服务器和应用程序的轻松互联,而不是创建新的Web框架。
调用方式
WSGI 协议要面对两个端:一个是服务器或者说是网关端,另一个是应用程序或者说框架端。就需要处理一个问题,是谁调用了另一方。
在协议中规定了调用方式:服务器端调用应用程序端提供的 可调用
对象。
也就是说,web 应用程序需要提供一个可调用对象给web服务器调用,这个可调用的对象可以是 函数,方法,类或者带有 __call__
方法的实例。
可调用对象的构成
这个可调用对象的构成也很简单,它接收 两个参数,该对象必须允许能够调用多次,如下面的示例:
def simple_app(environ, start_response):
"""最简单的应用程序对象"""
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return ['Hello world!\n']
这样就是一个满足 WSGI 协议的web程序应用了,是不是很简单。对应的django里,可以从 wsgi.py
中看到 application = get_wsgi_application()
这个函数展开基本和我们实例的最简单的应用程序对象结构一样了:
class WSGIHandler(base.BaseHandler):
request_class = WSGIRequest
def __call__(self, environ, start_response):
request = self.request_class(environ)
response = self.get_response(request)
status = '%d %s' % (response.status_code, response.reason_phrase)
response_headers = list(response.items())
start_response(status, response_headers)
return response
服务器端
服务器的作用是接收每一个 HTTP 请求,应用程序对象调用时需要传入 environ
和 start_response
,因此这两个参数需要由服务器端来整理并提供给应用程序使用。
environ
是一个字典,以一个简单的 CGI 网关为例,它的值可以这么设置:
import os
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'
start_response
则是一个函数,原型是 start_response(status, response_headers, exc_info=None)
并且这个函数要返回一个可调用的 write(body_data)
对象。例如:
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')
headers_set = [] # 待发送的响应的header信息
headers_sent = [] # 已发送的响应的header信息
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]
return write
这样其实一个满足 WSGI 协议的 web服务器端 就基本完成了,现在需要整合一下,由于需要涉及到请求包的分析过程,我们就直接用标准库 wsgiref.simple_server
中的 WSGIServer
作为web服务器。
整合一下:
import sys
import os
from wsgiref.simple_server import WSGIServer, WSGIRequestHandler
def demo_app(environ,start_response):
"""
示例的 app
"""
stdout = "Hello world!"
h = sorted(environ.items())
for k,v in h:
stdout += k + '=' + repr(v) + "\r\n"
print(start_response)
start_response("200 OK", [('Content-Type','text/plain; charset=utf-8')])
return [stdout.encode("utf-8")]
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(request, client_address, server):
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]
return write
application = server.get_app()
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()
server = WSGIServer(('', 8000), run_with_cgi)
server.set_app(demo_app)
server.serve_forever()
运行这个程序,然后用浏览器访问本地 8000 端口,就能看到终端输出了 environ
。
中间件
一个中间件扮演了与某些 application
相关的角色,同时,中间件也可以是某些服务器的应用程序。
中间件拥有如下功能:
- 适当修改
environ
后,根据目标 URL 将请求分配到不同的应用程序对象; - 允许多个
application
在同一个进程中并行; - 通过网络转发请求和响应来负载平衡和远程处理;
- 执行内容后处理,例如应用XSL样式表。
一般来说,中间件对于 "server/gateway" 和 "application/framework" 都是透明的,并且不需要特别的支持。如果用户将中间件集成到 application
中,那中间件提供给服务器调用,此时中间件就像 application
一样了;反过来,如果配置的中间件是调用 application
的调用方,那它就像服务器一样了。
因此,中间件包装的 "应用程序" 实际上也可能是另一个包装着应用程序的中间件。
大多数情况下,中间件必须符合 WSGI
服务器和应用程序端的限制和要求,django 中的中间件都是符合这些要求的。