Django 深入理解WSGI协议

 


起步#

惭愧啊,惭愧啊,距离上一篇这个系列的文章已经是半年前的了,随着 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 中的中间件都是符合这些要求的。

posted @   鲸鱼的海老大  阅读(79)  评论(0编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示
CONTENTS