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 X10.9安装的Python2.7升级Python3.3步骤详解
Dec 04 Python
Python中单例模式总结
Feb 20 Python
Python cookbook(数据结构与算法)筛选及提取序列中元素的方法
Mar 19 Python
通过Python 接口使用OpenCV的方法
Apr 02 Python
用Python实现数据的透视表的方法
Nov 16 Python
CentOS7安装Python3的教程详解
Apr 10 Python
Python如何使用k-means方法将列表中相似的句子归类
Aug 08 Python
Python 类的私有属性和私有方法实例分析
Sep 29 Python
Python 实现try重新执行
Dec 21 Python
tensorflow实现二维平面模拟三维数据教程
Feb 11 Python
python 一维二维插值实例
Apr 22 Python
python如何利用cv2模块读取显示保存图片
Jun 04 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
Windows下利用Gvim写PHP产生中文乱码问题解决方法
2011/04/20 PHP
php 文件上传类代码
2011/08/06 PHP
解析PHP对现有搜索引擎的调用
2013/06/25 PHP
PHP遍历数组的三种方法及效率对比分析
2015/02/12 PHP
php对文件进行hash运算的方法
2015/04/03 PHP
PHP中文字符串截断无乱码解决方法
2016/10/10 PHP
PHP实现下载远程图片保存到本地的方法
2017/06/19 PHP
laravel 实现根据字段不同值做不同查询
2019/10/23 PHP
日期 时间js控件
2009/05/07 Javascript
锋利的jQuery 要点归纳(二) jQuery中的DOM操作(下)
2010/03/23 Javascript
一个简单的网站访问JS计数器 刷新1次加1次访问
2012/09/20 Javascript
javascript中判断一个值是否在数组中并没有直接使用
2012/12/17 Javascript
Javascript new Date().valueOf()的作用与时间戳由来详解
2013/04/24 Javascript
JS实现浏览器状态栏文字从右向左弹出效果代码
2015/10/27 Javascript
JS常用倒计时代码实例总结
2017/02/07 Javascript
Angular在一个页面中使用两个ng-app的方法(二)
2017/02/20 Javascript
JQuery属性操作与循环用法示例
2019/05/15 jQuery
javaScript把其它类型转换为Number类型
2019/10/13 Javascript
VUE中使用HTTP库Axios方法详解
2020/02/05 Javascript
如何解决vue在ios微信"复制链接"功能问题
2020/03/26 Javascript
python3生成随机数实例
2014/10/20 Python
Python实现抓取百度搜索结果页的网站标题信息
2015/01/22 Python
python检测是文件还是目录的方法
2015/07/03 Python
Python简单生成8位随机密码的方法
2017/05/24 Python
详解python中executemany和序列的使用方法
2017/08/12 Python
pandas值替换方法
2018/07/10 Python
python实现反转部分单向链表
2018/09/27 Python
对Pycharm创建py文件时自定义头部模板的方法详解
2019/02/12 Python
市级绿色学校申报材料
2014/08/25 职场文书
医药公司采购员岗位职责
2014/09/12 职场文书
大学生个人学习总结
2015/02/15 职场文书
工作收入证明模板
2015/06/12 职场文书
毕业班班主任工作总结2015
2015/07/23 职场文书
Vue3 Composition API的使用简介
2021/03/29 Vue.js
pytorch中[..., 0]的用法说明
2021/05/20 Python
Pytorch使用shuffle打乱数据的操作
2021/05/20 Python