浅谈 WSGI
WSGI 是 Python Web 开发中经常提到的名词,在维基百科中,定义如下:
Web服务器网关接口(Python Web Server Gateway Interface,缩写为WSGI)是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口。自从WSGI被开发出来以后,许多其它语言中也出现了类似接口。
正如定义,WSGI 不是服务器,不是 API,不是 Python 模块,而是一种规定服务器和客户端交互的 接口规范。
WSGI 目标是在 Web 服务器和 Web 框架层之间提供一个通用的 API 标准,减少之间的互操作性并形成统一的调用方式。根据这个定义,满足 WSGI 的 Web 服务器会将两个固定参数传入 WSGI APP:环境变量字典和一个初始化 Response 的可调用对象。而 WSGI APP 会处理请求并返回一个可迭代对象。
WSGI APP
根据定义,我们可以实现一个非常简单的满足 WSGI 的 App:
代码语言:javascript复制def demo_wsgi_app(environ, start_response):
status = '200 OK'
headers = [('Content-type', 'text/plain')]
start_response(status, headers)
yield "Hello World!"
可以看到,该 App 通过 start_response
初始化请求,并通过 yield 将 body 返回。除了 yield,也可以直接返回一个可迭代对象。
在标准库 wsgiref 中已经包含了一个简单的 WSGI APP,可以在 wsgiref.simple_server 中找到,可以看到,这也是在做相同的事情:
代码语言:javascript复制def demo_app(environ,start_response):
from io import StringIO
stdout = StringIO()
print("Hello world!", file=stdout)
print(file=stdout)
h = sorted(environ.items())
for k,v in h:
print(k,'=',repr(v), file=stdout)
start_response("200 OK", [('Content-Type','text/plain; charset=utf-8')])
return [stdout.getvalue().encode("utf-8")]
将这个 App 运行起来如下:
在 Django 中,可以在默认 app 下的 wsgi.py 中找到 get_wsgi_application
,Django 通过这个方法创建并返回了一个 WSGIHandle
,其本质,依然是一个 WSGI APP,可以看其 __call__
方法:
class WSGIHandler(base.BaseHandler):
request_class = WSGIRequest
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.load_middleware()
def __call__(self, environ, start_response):
set_script_prefix(get_script_name(environ))
signals.request_started.send(sender=self.__class__, environ=environ)
request = self.request_class(environ)
response = self.get_response(request)
response._handler_class = self.__class__
status = '%d %s' % (response.status_code, response.reason_phrase)
response_headers = list(response.items())
for c in response.cookies.values():
response_headers.append(('Set-Cookie', c.output(header='')))
start_response(status, response_headers)
if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'):
response = environ['wsgi.file_wrapper'](response.file_to_stream)
return response
WSGI 服务器
从 WSGI APP 的写法上就基本能推测出 WSGI 服务器做了什么,因此可以尝试实现一个简陋的 WSGI 服务器:
代码语言:javascript复制def run_wsgi_app(app, environ):
from io import StringIO
body = StringIO()
def start_response(status, headers):
body.write('Status: {}rn'.format(status))
for header in headers:
body.write("{}: {}".format(*header))
return body.write
iterable = app(environ, start_response)
try:
if not body.getvalue():
raise RuntimeError("No exec start_response")
body.write("rn{}rn".format('rn'.join(line for line in iterable)))
finally:
if hasattr(iterable, "close") and callable(iterable.close):
iterable.close()
# 这里瞎扯
return body.getvalue()
对于真正(可用)的 WSGI 服务器,常用的比如 Gunicorn,在不同的 Worker(gunicorn.worker 模块中)中,都实现了一个叫 handle_request
的类方法,这个方法便是调用 WSGI APP,并完成 Response 拼装的,虽然不同的 Worker 的实现略有差异,但比较共通的代码:
respiter = self.wsgi(environ, resp.start_response)
try:
if isinstance(respiter, environ['wsgi.file_wrapper']):
resp.write_file(respiter)
else:
for item in respiter:
resp.write(item)
resp.close()
request_time = datetime.now() - request_start
self.log.access(resp, req, environ, request_time)
finally:
if hasattr(respiter, "close"):
respiter.close()
这段代码便是调用 WSGI APP,并通过循环把 Body 写入到 resp 中。
中间件
因为 WSGI 的定义方式,可以写多个 WSGI APP 进行嵌套并处理不同的逻辑,比如:
代码语言:javascript复制def first_wsgi_app(environ, start_response):
import logging
logging.info("new request")
rsp = second_wsgi_app(environ, start_response)
logging.info("finish request")
return rsp
def second_wsgi_app(environ, start_response):
if environ.get("HTTP_ROLE") == "ADMIN":
return third_wsgi_app(environ, start_response)
status = '200 OK'
headers = [('Content-type', 'text/plain')]
start_response(status, headers)
yield "Hello User!"
def third_wsgi_app(environ, start_response):
status = '200 OK'
headers = [('Content-type', 'text/plain')]
start_response(status, headers)
yield "Hello Admin!"
这时候我们把第一个 WSGI APP first_wsgi_app
传给 Server。在执行时, first_wsgi_app
可以完成日志的记录, second_wsgi_app
可以完成鉴权, third_wsgi_app
来真正的处理请求。
这种 App 的洋葱结构,被正伦亲切的称为俄罗斯套娃。