Django 源码阅读(六):深入理解WSGI协议

随笔 2018-06-20

起步

惭愧啊,惭愧啊,距离上一篇这个系列的文章已经是半年前的了,随着 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 请求,应用程序对象调用时需要传入 environstart_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 中的中间件都是符合这些要求的。

参考


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

如果对您有用,您的支持将鼓励我继续创作!