WSGI web服务网关协议(1)

序言

本文译自:https://www.python.org/dev/peps/pep-0333

注意:如果需要查看支持Python 3.x版本的更新(包括社区勘误,附录和说明),请移步查看PEP3333

摘要

为了提高网络应用在一系列web服务器之间的可移植性,这篇文档给出了web服务端和python web应用或框架的标准接口。

理由和目标

Python语言中目前涌现了一大批的网络应用框架, 比如Zope, Quixote, Webware, SkunkWeb, PSO 和 Twisted Web, 这里仅仅列出了其中的一部分Python Wiki。如此多的选择对于Python的初学者来说也许是个难题,因为一般来说,Web框架的选择一定程度上限制了Web服务器的选择, 反之亦然。

相比之下,尽管Java有如此多的Web应用框架可以使用,但是Servlet API的使用使得所有支持Servlet API的网络应用框架可以跑在任何同样支持Servlet的网络服务器上。

这样一个支持Python的API如果也能得到广泛使用的话(不管服务器是用Python-Medusa,还是嵌入式Python - mod_python, 亦或是通过网关协议来调用Python - CGI,FastCGI 等),那么就可以将框架与Web服务器的选择分开,使用户能够随意搭配他们想要的服务器或者web框架,同时也释放了框架和服务器开发人员,使其专注于他们偏好的专业领域。

这篇PEP文档,提出了一种用于服务器端和应用框架之间简单且通用的接口:Python Web 服务网关协议接口(WSGI)。

但是WSGI规范的存在并不能改变现有的服务器及应用框架的状态,服务器及框架作者和维护者必须切实的实现WSGI才可以使它生效。

不过,因为目前没有支持WSGI规范的服务器和框架,而对于准备实施及支持WSGI规范的作者来说也没有即时的报酬。因此,WSGI必须很容易实现,开发者在实施起来时的初始投资也能在一个很低的合理范围。

因此,服务端和框架端的实施的简单性相比于WSGI接口的实用性来说要显得更为重要,这也是在作任何设计决定时的首要原则。

但框架作者去实现一个框架的简单性并不意味应用程序作者的简单性,这是两回事。WSGI提供给框架作者的是不加任何修饰的接口,因为像响应对象和cookie处理这样花里胡哨的东西只会阻碍现有框架。需要强调的是,WSGI的目标是促进现有服务器和应用框架的互连,而不是创建一个新的Web框架。

另外,这个目标也避免了一些已经部署的Python版本中不可用的东西。因此,新的标准库模块不是这个规范里必须的,同时也不要求Python的版本大于2.2.2。(这将是一个好主意,未来Python版本中的标准库中Web server也会包含对这个接口的支持)。

除了让现有的和未来的框架及服务器容易实施以外, 它也应该很容易的创建请求预处理器,响应处理器以及其他基于WSGI的中间件组件,中间件组件对于包含这个中间件的服务器来说它是一个应用 ,对于中间件包含的应用来说的话,它是一个服务器。

如果中间件不仅简单而且足够健壮,且WSGI在服务端和框架中被广泛应用,那么会出现一种全新的Python web应用框架:一个由松散耦合的WSGI中间件组成的框架结构。显然, 现有的框架作者甚至可以选择重构他们现有的服务,变得更像一个使用WSGI的库,而不是一整个的整体框架。这也允许应用开发者选择最好的组件组合去提供一些特定的功能,而不是必须提交单一框架的的所有利弊。

当然,截至本文,距离那一个目标还有很远,目前的WSGI短期目标是使任何的框架可以跑在任何的服务器上。

最后要提到的一点是,目前版本的WSGI并没有规定用一个服务器或网关部署一个应用的规范,现在这些都必须在服务端定义并实现,在有足够多的服务器及框架实现了WSGI并在服务部署上积累了一定经验后,可以创建另一个PEP文档,描述WSGI服务器及应用框架的部署标准。

概述

WSGI接口包含两个方面 :服务或网关,应用或框架。服务端调用应用端提供的一个可以调用的对象,如何提供对象的细节取决于服务器或网关。某些服务器或网关可能需要应用程序的部署者编写一个简短的脚本来创建服务器或网关的一个实例并为其提供一个应用对象。也可能有其他的服务器需要通过配置文件或其他机制来定义从哪里引入应用对象或者获得。

除了单一化服务端和应用端的以外,还可以创建服从两端规范的中间件。这些组件可以充当包含它们的服务器的应用程序,也可以作为包含应用程序的服务器,另外也可以用来提供扩展API,内容转换,导航和其他有用的功能。

在整个这篇规范中,我们将使用术语“可调用”来表示“函数,方法,类或具有call方法的实例”。为了满足服务端、网关或应用程序的需求,它可以选择用适合的技术去实现。反过来也就是说,调用可调用对象的服务器,网关或应用程序对可调用的对象类型不能有任何依赖,可调用对象只能被调用,而不能内省。

应用端/框架端

应用对象是一个简单的可接收两个参数的可调用对象,对象这个词不应该误导你认为需要一个真正的对象实例:一个函数,方法,类或拥有call方法的实例都可以作为一个应用对象被接受。应用对象也必须可以被多次调用,因为几乎所有的服务器或者网关(CGI除外)都会做出重复的请求。

注意:尽管我们把它称作为“应用”对象, 这也并不意味着应用开发者应该直接使用WSGI作为Web开发编程的API,我们是假设应用开发人员是基于现有的,更高抽象层次的框架上去进行开发的。WSGI是一个服务器和框架开发者的工具,并不会直接支持应用开发人员。

这里有两个应用对象的示例,一个为函数,另外一个是一个可调用的类。

def simple_app(environ, start_response):
    """Simplest possible application object"""
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return ['Hello world!\n']


class AppClass:
    """Produce the same output, but using a class

    (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!\n"
服务/网关端

针对应用程序的每个HTTP请求, 服务器都会调用一次应用程序去处理。为了说明,这里有一个简单的CGI网关,实现了接收应用对象的函数。注意,这个简单的例子只有有限的错误处理,实际上对于未捕捉的异常默认是应该通过sys.stderr输出并被web服务器记录。

import os, sys

def run_with_cgi(application):

    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'

    headers_set = []
    headers_sent = []

    def write(data):
        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
             sys.stdout.write('Status: %s\r\n' % status)
             for header in response_headers:
                 sys.stdout.write('%s: %s\r\n' % header)
             sys.stdout.write('\r\n')

        sys.stdout.write(data)
        sys.stdout.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[0], exc_info[1], 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

    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将请求路由到不同的应用程序对象。
  • 允许多个应用程序或框架在同一个进程中并行
  • 通过请求转发和响应,实现负载均衡和远程处理
  • 对内容进行后处理,比如应用XSL样式表

一般而言,中间件的存在对于接口的“服务器/网关”和“应用/框架”是透明的,不应该需要特别的支持。一个用户想整合中间件,只需要把其作为应用程序提供给服务器,并把中间件看作是服务器,利用它调用真正的应用程序。当然,“应用程序”也可能是另一个包装了应用程序的中间件,这里由多个中间件组成的棧就叫做中间件堆棧。

大部分情况下,中间件都必须同时遵守服务端和应用端两方面的限制及满足两方面的需求。不过在一些情况下,中间件的需求甚至比单纯的服务器或应用程序更严格,这些必须在中间件的文档中提出来。

这个(不认真的)中间件组件是把响应类型是text/plain的文本用Joe Strout的piglatin.py转化为pig latin文,(注意:一个“真正”的中间件可能会用更健壮的方法来检查内容类型,同时检查内容编码。另外,这个简单的例子也忽略一个单词在边界被分割开的情况)

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 string, 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())
        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))
                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)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容