WSGI规范(PEP 3333) 第一部分(概述)

WSGI规范(PEP 3333) 第一部分(概述)

概述

WSGI 的全称是:Web Server Gateway Interface,介于 web server 和应用 server 之间的接口规范。当使用 Python 进行 web 开发时,要深刻理解 Django、Flask、Tornado等 web 框架,WSGI是你绕不过去的槛儿。为了方便中文世界的同行,我粗略地将规范进行翻译。不周之处,请联系我,定当不遗余力地予以修正。

要查看原始文档,请点击PEP 3333 -- Python Web Server Gateway Interface v1.0.1

以下是翻译内容。



为了解 PEP 333的读者准备的前言

PEP 3333 是 PEP 333的升级版本,进行了略微修改以提高在 Python 3 下的可用性。同时,合并了几点长期存在的、实际的修改。(代码示例已经兼容 Python 3)


然而,当使用 Python 3时,你的 App 或者 server 必须遵守以下规则:【有关字符串类型的说明】和【Unicode 】。下文有这两点的详细描述。


摘要

这篇文章提出了一个介于 web 服务器和 Python web 应用或者框架之间的、建议的标准接口,以推进 python web 应用和各种 web 服务器之间的良好兼容。


基本原理和目标(PEP 333)

在Python中出现了各种 Web 应用框架,如 Zope、Quixote、Webware、SkunkWeb、PSO、和 Twisted Web等。对 Python 新手来说,众多的框架是一个问题。通常来说,他们的选择会限制他们对 Web 服务器等的选择。

相比之下,尽管 Java 也存在众多 web 开发框架,servlet API 出现之后,使得用任何 Java web框架编写的应用运行在任何支持 servlet API 的 web 服务器上成为可能。

The availability and widespread use of such an API in web servers for Python -- whether those servers are written in Python (e.g. Medusa), embed Python (e.g. mod_python), or invoke Python via a gateway protocol (e.g. CGI, FastCGI, etc.) -- would separate choice of framework from choice of web server, freeing users to choose a pairing that suits them, while freeing framework and server developers to focus on their preferred area of specialization.

因此,这个 PEP 提出了一套简单而通用的、介于 web 服务器和 web 应用或框架之间的接口:Python WSGI。

但是,仅仅存在一个 WSGI 规范起不到什么作用,只有web 服务器和框架的作者们和维护者们实际去实现 WSGI 规范才行。

由于现在没有支持 WSGI 的web服务器和框架,对实现 WSGI 支持的作者来说,几乎没有快速的回报。因而,WSGI 必须易于实现,实现此接口的作者的早期投入才会比较低。


因此,此接口在 web 服务器和框架中实现的简单性就极为重要了,也是做任何设计决策的重要原则。

需要指出,对框架作者的实现简单性和 web 应用的开发者的实现简单性不是同一件事。对框架作者来说,WSGI 是一个完全没有余饰的接口(不多也不少),因为像 Response 对象和 cookie 处理等用框架已经存在的方式处理即可。再次说明,WSGI 的目标是辅助已经存在的 web 服务器和 web 应用或框架交互,而不是发明一个新的 web 框架。


这个目标也是 WSGI 排除了从部署的 Python 版本中获取其他信息的需要。因此新标准库模块,也不需要从版本高于2.2.2的Python 中获取其他信息(对以后版本的 Python来说, 标准库提供的 web 服务器支持 WSGI 接口会是一个好主意)


除了使已经存在和以后的框架和服务器易于实现此规范,WSGI 接口对于创建请求预处理器(request preprocessors),响应后处理器(response postprocessors)以及其他基于 WSGI 的中间件组件(middleware)也是容易实现的。这些中间件组件对包含他们的服务器来说就像应用,对他们包含的应用来说就像服务器。

如果中间件可以做到既简单又稳定,WSGI也广泛地被服务器和框架实现,那么完全崭新的 python web 框架的出现便有了可能,这种框架由松耦合的 WSGI 中间件组成。确实,已存在框架的作者甚至可能用这种新的方式重构框架,使其变得更像使用 WSGI 的库,而不是一个庞大的框架。这种变化使应用开发者为特定的功能选择最佳方案,而不需要提交一个简单框架的所有优势和劣势。

当然,到写这篇文稿的时间,那一天无疑还是遥远的。WSGI 的最短期目标就是:可以在任何 web 服务器上使用任何框架。

最后, 必须提及,使用任何 web 服务器或者网关部署应用,当前版本的 WSGI没有规定任何特殊的机制。截止目前,这是被服务器或网关实现定义的必要条件。足够多的服务器和框架实现 WSGI 以提供多样化部署需求的经验之后,创建另一个 PEP 来描述 WSGI 服务器和应用框架的部署标准才变得有意义。

规范概述

WSGI 接口有服务端和应用端两部分,服务端也可以叫网关端,应用端也叫框架端。服务端调用一个由应用端提供的可调用对象。如何提供这个对象,由服务端决定。例如某些服务器或者网关需要应用的部署者写一段脚本,以创建服务器或者网关的实例,并且为这个实例提供一个应用实例。另一些服务器或者网关则可能使用配置文件或其他方法以指定应用实例应该从哪里导入或获取。

除了这些比较纯的服务器/网关 和应用/框架,我们还可以创建实现了此规范两端的中间件组件。这个中间件对服务器来说像是应用,对它所包含的应用来说像是服务器。中间件可以用来提供扩展 API、内容转换、导航等其他有用的功能。

纵观整个规范,术语『可调用对象』可能代表函数、方法、类或者实现了__call__方法的实例。这取决于服务器、网关或者应用选择哪种实现技术。相反地,调用这个可调用对象的服务器、网关或者应用不能依赖提供给它的是哪种可调用对象。可调用对象仅仅是被调用,不会内省自己。

字符串类型

一般来说,HTTP 处理的是字节,这意味着 WSGI 规范也要处理字节。然而,这些字节内容往往有某种文本解释。在 Python 中,字符串是处理文本最方便的方式。

但是在很多 Python 版本和实现中,字符串是 Unicode,不是字节。这就需要小心平衡在 HTTP 的上下文中如何正确转换字节和文本,并提供有用的 API。尤其是需要支持 python 实现中不同 str 类型的转换代码。

因此 WSGI 定义了两种字符串:

  • 原生字符串(总是使用 str 来实现)用于请求/响应 的头部(headers)和元数据(metadata)
  • 字节字符串(在 Python3中用 bytes实现,其他版本中用 str 实现)用于请求/响应的数据部分(如 POST/PUT的输入数据,HTML 的输出内容等)。


不要弄混了:即使 Python 的 str 实际上是 unicode 字符,原生字符串内容也必须支持通过 Latin-1转换成字节码。

一句话:当你在本文档中看到 string 时,它代表『原生』字符串,例如一个 str 类型的对象,不管它内部实现上用的是字节还是 unicode。当你看到 bytestring,应该视作一个在 python3中的bytes对象, 在 python2中的 str 对象。

所以,尽管在某种意义上,http就是很像字节,有多个方便的API可以将其转换成 Python 的默认 str 类型。


应用/框架端

应用对象(application object)就是一个简单的接受两个参数的可调用对象。不要混淆术语"object"就真的是一个对象实例。python 中的函数、方法、类、实现了__call__的实例都是可以接受的。应用对象必须可以被多次调用,因为实际上所有服务器/网关(除了 CGI 网关)都会重复地调用它。

注意:我们总是讲 应用对象,不要误解为应用开发者需要使用 WSGI 作为 web 编程 API!应用开发者可以继续使用已经存在的、高级框架服务去开发他们的应用。WSGI 是一个为框架开发者和服务器开发者准备的工具,应用开发者不需要直接使用 WSGI。

下面是两个应用对象(application object)的示例。一个是函数(function),一个是类(class):


HELLO_WORLD = b"Hello world!\n"

def simple_app(environ, start_response):
    """最简单的应用对象"""
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [HELLO_WORLD]

class AppClass:
    """产生相同的输出,但是用类实现。

    (Note: 'AppClass' is the "application" here, so calling it
    returns an instance of 'AppClass', which is then the iterable
    return value of the "application callable" as required by
    the spec.

    If we wanted to use *instances* of 'AppClass' as application
    objects instead, we would have to implement a '__call__'
    method, which would be invoked to execute the application,
    and we would need to create an instance for use by the
    server or gateway.
    """

    def __init__(self, environ, start_response):
        self.environ = environ
        self.start = start_response

    def __iter__(self):
        status = '200 OK'
        response_headers = [('Content-type', 'text/plain')]
        self.start(status, response_headers)
        yield HELLO_WORLD

服务器/网关端

服务器或者网关每次从 HTTP 客户端收到一个请求,就调用一次应用对象。为了描述方便,以下是一个简单的 CGI 网关,用Python函数实现,接收应用对象。注意这个简单的示例在错误处理方面相当简单,因为默认情况下,未捕获的异常会被 dump 到 sys.stderr,并且被 web 服务器记入日志。

import os, sys

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(application):
    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]

        # Note: error checking on the headers should happen here,
        # *after* the headers are set.  That way, if an error
        # occurs, start_response can only be re-called with
        # exc_info set.

        return write

    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()

中间件:可以与两端交互的组件

中间件就是一个简单对象:既可以作为服务端角色,响应应用对象;也可以作为应用对象,与服务器交互。除此之外,还有一些其他功能:

  • 重写environ,然后基于 URL,将请求对象路由给不同的应用对象。
  • 支持多个应用或者框架顺序地运行于同一个进程中。
  • 通过转发请求和响应,支持负载均衡和远程处理。
  • 支持对内容做后处理(postprocessing),比如处理一个 XSL 样式表文件。


中间件的灵魂是:对 WSGI 接口的服务器/网关端和 应用/框架端是透明的,不需要其他条件。

希望将中间件合并进应用的用户,将这个中间件传递给服务器即可,就好像这个中间件是一个应用对象;或者让中间件去调用应用对象,好像这个中间件就是服务器。当然,被中间件包装(wrap)的应用对象,实际上可能是另一个包装了另一个应用的中间件,以此类推,就创建了一个中间件栈(middleware stack)。

最重要的,中间件必须同时满足服务端和应用端的限制和条件。然而,在有些情况下,中间件需要的条件比单纯的服务端或者应用端更严格,这些点会在下面予以说明。

以下是一个中间件示例。它转换 text/plain 响应为 pig Lain 响应,使用Joe Strout的脚本piglatin.py。(真正的中间件组件会用一种更完善的方法来检查内容类型和内容编码。这个例子也忽略了一个单词可能跨越块边界(block boundary)的情况的处理。)


from piglatin import piglatin

class LatinIter:

    """Transform iterated output to piglatin, if it's okay to do so

    Note that the "okayness" can change until the application yields
    its first non-empty bytestring, so 'transform_ok' has to be a mutable
    truth value.
    """

    def __init__(self, result, transform_ok):
        if hasattr(result, 'close'):
            self.close = result.close
        self._next = iter(result).__next__
        self.transform_ok = transform_ok

    def __iter__(self):
        return self

    def __next__(self):
        if self.transform_ok:
            return piglatin(self._next())   # call must be byte-safe on Py3
        else:
            return self._next()

class Latinator:

    # by default, don't transform output
    transform = False

    def __init__(self, application):
        self.application = application

    def __call__(self, environ, start_response):

        transform_ok = []

        def start_latin(status, response_headers, exc_info=None):

            # Reset ok flag, in case this is a repeat call
            del transform_ok[:]

            for name, value in response_headers:
                if name.lower() == 'content-type' and value == 'text/plain':
                    transform_ok.append(True)
                    # Strip content-length if present, else it'll be wrong
                    response_headers = [(name, value)
                        for name, value in response_headers
                            if name.lower() != 'content-length'
                    ]
                    break

            write = start_response(status, response_headers, exc_info)

            if transform_ok:
                def write_latin(data):
                    write(piglatin(data))   # call must be byte-safe on Py3
                return write_latin
            else:
                return write

        return LatinIter(self.application(environ, start_latin), transform_ok)


# Run foo_app under a Latinator's control, using the example CGI gateway
from foo_app import foo_app
run_with_cgi(Latinator(foo_app))


WSGI规范(PEP 3333) 第二部分(细节)

WSGI规范(PEP 3333) 第三部分(实现)

WSGI规范(PEP 3333) 第四部分(常见问题)

编辑于 2017-07-29

文章被以下专栏收录