浅谈 WSGI

2018/09/21 16:29 pm posted in  Python 黑魔法

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:

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 中找到,可以看到,这也是在做相同的事情:

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 服务器:

def run_wsgi_app(app, environ):
    from io import StringIO
    body = StringIO()

    def start_response(status, headers):
        body.write('Status: {}\r\n'.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("\r\n{}\r\n".format('\r\n'.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 进行嵌套并处理不同的逻辑,比如:

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 的洋葱结构,被正伦亲切的称为俄罗斯套娃。