Python 的演示平台支持 WSGI 接口的应用


Posted in Python onApril 20, 2022

前言

今天在 git.oschina 的首页上看到他们推出演示平台,其中,Python 的演示平台支持 WSGI 接口的应用。虽然,这个演示平台连它自己提供的示例都跑不起来,但是,它还是成功的勾起了我对 WSGI 的好奇心。一番了解,对该机制的认识,总结如下。如有不妥,还望斧正。

为什么是 WSGI?

写过网页应用的各位亲,应该对 CGI 有了解,我们知道,CGI 的全程是“Common Gateway Interface”,即 “通用 Gateway Interface“。没错,这里的 WSGI,就是只针对 Python的网页应用接口“Python Web Server Gateway Interface”。通过这样的类比,想必大家对他的地位就有所了解了。

它只是一个接口定义:它不负责服务器的实现,也不负责网页应用的实现,它只是一个两边接口方式的约定。所以,它并不是另一个 WEB 应用框架。通常意义上的 WEB 应用框架,也只相当于 WSGI 网页应用端的一种实现。

这样做的好处是?PEP 0333 中的解释是,为了实现一个类似于 Java Servelet 的 API,使得遵循该接口的应用拥有更广泛的适用性。是的,有了该接口,你就不用去考虑,服务器对 Python 的支持到底是如何实现——无论是“ 直接用 Python 实现的服务器”,还是“服务器嵌入 Python”,或者是 “ 通过网关接口(CGI, Fastcgi...)”——应用程序都有很好的适用性。就像是今天故事的开始,我们遇到了云平台,它提供了对 WSGI 接口的支持,那么,只要应用是基于 WSGI 的,那么应用就可以直接跑起来。

此外,WSGI 的设计,也提供了另外一种可能性,那就是中间件(middleware)。或者说,我们可以写一些对 server 和 application 都兼容的模块,我们可以把他们部署在 Server 端,也可以部署在 Application 端,完成比如缓存、字符编码转换、根据 url 做应用 routing 等功能。这种设计模式,是 WSGI 降低了 server 和 application 耦合度之后的产物,同时,它从另一个角度大大提升了设计的灵活性。

WSGI 实施概略

上一小节,简要对 WSGI 做了介绍。这里从 application、server、middleware 三个角度对 WSGI 稍微进行深入,使我们对它有一个更具体的印象。

1)Application 端

WSGI 要求,应用端必须提供一个可被调用的实体(PEP 0333 使用的是 Object,文档还特别解释这有别于Object instance),该实体可以是:一个函数(function)、一个方法(method)、一个类(class)、或者是有__call__方法的对象(Object instance)。

这里有两个网页应用端的实现示例,一个是 function object,一个 class object:

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

上面的 function 只是直接对请求直接做了 “200 ok” 回应,并没有处理传进来的参数 environ——里面是由 WSGI Server 端提供的各种 HTTP 请求参数。需要特别注意的是,这个函数在最后,返回的一个 list(用“[]”包含在内)以保证结果的 iterable。下面的 class 类似。

在下面例子中,AppClass 作为应用实体。当调用发生时,其实是对 class 进行了例化( python 固有特性,可以参考后面 server 端的实现代码进一步理解),正如我们看到,这次调用(call)的返回值也是可迭代的——虽然只迭代一次(yield)。

class AppClass:
    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"
        """ In fact, the interator ‘ends‘ here because of no more yield field"""

与上面两种情形不同,使用 object instance 作为应用实体时,需要为类定义添加 __call__ 方法,同时,参考上面使用 function 作为实体时情形,__call__ 方法的返回值需为 iterable(比如 return [ something ])。

最后,不管我们的 app 是 function 还是 class, application 都需要处理两个参数,而且是两个位置相关的参数(不是命名参数),分别是:一个存放了 CGI 环境变量的 dictionary object,和一个可调用实体(需要给它三个位置相关的参数,两个必须,一个可选)。

其中,可调用实体(前例中的 start_response)必须调用一次,两个必须的参数分别为“ HTTP Response的状态(str 类型)“ 和 “HTTP Response Header(list of tuples)“;

一个可选的参数exc_info,必须是 Python sys.exc_info() tuple,只有在出错需要显示错误信息时使用。完整调用:start_response(status, response_headers,exc_info).

2)Server 端

下面是从 PEP 0333 拿来的一个简单的 WSGI 容器,适用于 Python 作为某 WEB Server 上 CGI 时的应用情形。

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

上面的容器,大概实现了:

  • a)将 CGI 环境变量放入 dictionary object (environ)中,供 Application 实体使用;
  • b)定义了 start_response 方法,供 Application 实体调用;
  • c)调用 application 实体,对 web 请求进行处理;
  • d)将 application 的返回结果,以及通过 start_response 设置的 HTTP Response HEADER,写到 stdout ——像其他 CGI 一样,实际上是被发往网页。

3) 作为 middleware

因为 WSGI 的宽松耦合的特性,我们可以轻松的在 Application 和 Server 之前插入任何的中间插件,在不需要改动 Server 和 Application 的前提下,实现一些特殊功能。但是,这种放在 Server 和 Application “中间”的模块,并不是这里要讲的 middleware ;或者,这只能算是一种特殊的 middleware,因为它仅仅是实现了 PEP 0333 中 middleware 定义的 Application 侧的功能。这种仅实施在一侧的 middleware,需要在发布时,特别的声明。

PEP 0333 中约定,中间件是一些即可以在 Server 端实施,又可以在 Application 端实施的模块。所以,在设计的时候,对两边的特性都要做适当考虑。幸好,WSGI 接口设计的足够简单。

class Router():
    def __init__(self):
        self.path_info = {}
    def route(self, environ, start_response):
        application = self.path_info[environ['PATH_INFO']]
        return application(environ, start_response)
    def __call__(self, path):
        def wrapper(application):
            self.path_info[path] = application
        return wrapper
""" The above is the middleware"""
router = Router()
@router('/world')
def world(environ, start_response):
    status = '200 OK'
    output = 'World!'start_response(status, response_headers)  
    return [output] 
@router('/hello') 
def hello(environ, start_response):
    status = '200 OK'
    output = 'Hello'
    response_headers = [('Content-type', 'text/plain'), ('Content-Length', str(len(output)))]
    start_response(status, response_headers)  
    return [output]

简单解释一下:

- 作为 Application 时,我们用 Router 实例化一个对象。然后对 “ PATH-APP “ 进行注册,根据不同的 PATH,我们要进一步选择哪个 App。接着,就是把 router.route() 喂给 Server ,作为 Application 侧的可调用实体。有请求到来时,根据已经注册的 “PATH-APP” 对选择应用并执行。

- Server 端类似,我们要先实例化并完成注册。然后,比如,拿我们上一小节实现的 WSGI 容器为例,我们需要修改 result = router.route(environ, start_response),同样完成了router的功能。

下面是另外一个,实现了 postprocessor 的一个例子,在 Application 返回的 HTTP Header 里面再加一个 Header。

def myapp(environ, start_response):
    response_headers = [('content-type', 'text/plain')]
    start_response('200 OK', response_headers)
    return ['Check the headers!']
class Middleware:
    def __init__(self, app):
        self.wrapped_app = app
    def __call__(self, environ, start_response):
        def custom_start_response(status, headers, exc_info=None):
            headers.append(('X-A-SIMPLE-TOKEN', "1234567890"))
            return start_response(status, headers, exc_info)
        return self.wrapped_app(environ, custom_start_response)
app = Middleware(myapp)

这里通过改写传递给 Application 的实体,实现了 postprocess 的目的。

其他资源:

- WSGI 的一些详细资料,包括应用列表什么的:https://wsgi.readthedocs.io/en/latest/

- 支持 WSGI 的多线程 WEB 服务器,基于SimpleHttpServer:

http://www.owlfish.com/software/wsgiutils/

-Paste为构建以 WSGI 为基础的 WEB 应用程序或框架提供一个良好的基础

- 官方的 WSGI 实现参考:https://pypi.org/project/wsgiref/

- 啄木鸟社区的 WSGI 中文 wiki:https://wiki.woodpecker.org.cn/moin/WSGI

- 和 Paste 一样有名的基本架构:https://pypi.org/project/Pylons/1.0/

- 目前 Python 比较流行的三大 WEB 框架:TurboGears,Django,web2py。+1,代码在 K 级别的服务小框架:webpy。

- 另外三个据说高性能的 App 开发框架:Falcon、Tornado、Bootle.py.

- 还有个价格不错的 vps,恩:https://www.hostwinds.com/

以上就是通过Python中的CGI接口讲解什么是WSGI的详细内容!

Python 相关文章推荐
在Mac OS系统上安装Python的Pillow库的教程
Nov 20 Python
Python字符串格式化的方法(两种)
Sep 19 Python
python删除服务器文件代码示例
Feb 09 Python
在cmd中运行.py文件: python的操作步骤
May 12 Python
Python3实现爬取简书首页文章标题和文章链接的方法【测试可用】
Dec 11 Python
python 基于TCP协议的套接字编程详解
Jun 29 Python
Django框架模型简单介绍与使用分析
Jul 18 Python
使用Python实现图像标记点的坐标输出功能
Aug 14 Python
QML实现钟表效果
Jun 02 Python
Python判断远程服务器上Excel文件是否被人打开的方法
Jul 13 Python
Python使用Opencv实现边缘检测以及轮廓检测的实现
Dec 31 Python
Python开发五子棋小游戏
May 02 Python
python​格式化字符串
Apr 20 #Python
Python编写冷笑话生成器
Apr 20 #Python
Python Django / Flask如何使用Elasticsearch
Apr 19 #Python
python中mongodb包操作数据库
Apr 19 #Python
Elasticsearch 聚合查询和排序
Apr 19 #Python
Elasticsearch 基本查询和组合查询
Apr 19 #Python
Elasticsearch 批量操作
Apr 19 #Python
You might like
zend framework文件上传功能实例代码
2013/12/25 PHP
php筛选不存在的图片资源
2015/04/28 PHP
PHP堆栈调试操作简单示例
2018/06/15 PHP
PHP实现图片压缩
2020/09/09 PHP
thinkphp5 框架结合plupload实现图片批量上传功能示例
2020/04/04 PHP
php中get_object_vars()在数组的实例用法
2021/02/22 PHP
页面中js执行顺序
2009/11/09 Javascript
判断客户端浏览器是否安装了Flash插件的多种方法
2010/08/11 Javascript
使用jquery实现select添加实现后台权限添加的效果
2011/05/28 Javascript
jquery批量控制form禁用的代码
2013/08/06 Javascript
js中cookie的添加、取值、删除示例代码
2013/10/21 Javascript
使用jquery选择器如何获取父级元素、同级元素、子元素
2014/05/14 Javascript
jQuery带箭头提示框tooltips插件集锦
2014/11/17 Javascript
js实现iPhone界面风格的单选框和复选框按钮实例
2015/08/18 Javascript
实例详解JavaScript获取链接参数的方法
2016/01/01 Javascript
浅谈toLowerCase和toLocaleLowerCase的区别
2016/08/15 Javascript
基于jQuery的checkbox全选问题分析
2016/11/18 Javascript
剖析Angular Component的源码示例
2018/03/23 Javascript
JS对象和字符串之间互换操作实例分析
2019/02/02 Javascript
浅谈Vue为什么不能检测数组变动
2019/10/14 Javascript
浅谈v-for 和 v-if 并用时筛选条件方法
2019/11/07 Javascript
浅谈VUE中演示v-for为什么要加key
2020/01/16 Javascript
python记录程序运行时间的三种方法
2017/07/14 Python
python通过opencv实现批量剪切图片
2017/11/13 Python
Django自定义过滤器定义与用法示例
2018/03/22 Python
局域网内python socket实现windows与linux间的消息传送
2019/04/19 Python
django执行原始查询sql,并返回Dict字典例子
2020/04/01 Python
成人大专自我鉴定范文
2013/10/19 职场文书
社区党总支书记先进事迹材料
2014/01/24 职场文书
美德好少年主要事迹
2014/01/29 职场文书
大学生活动策划方案
2014/02/10 职场文书
大学生职业生涯规划书参考模板
2014/03/05 职场文书
班主任个人工作反思
2014/04/28 职场文书
班组长安全工作职责
2014/07/15 职场文书
有关九一八事变的演讲稿
2014/09/14 职场文书
python实现简单的三子棋游戏
2022/04/28 Python