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 相关文章推荐
Python数据结构之Array用法实例
Oct 09 Python
python自定义类并使用的方法
May 07 Python
Python入门之后再看点什么好?
Mar 05 Python
Python设计模式之桥接模式原理与用法实例分析
Jan 10 Python
PythonWeb项目Django部署在Ubuntu18.04腾讯云主机上
Apr 01 Python
Python实现图片裁剪的两种方式(Pillow和OpenCV)
Oct 30 Python
TensorFlow tf.nn.conv2d实现卷积的方式
Jan 03 Python
python字符串替换re.sub()实例解析
Feb 09 Python
Python3中configparser模块读写ini文件并解析配置的用法详解
Feb 18 Python
VScode连接远程服务器上的jupyter notebook的实现
Apr 23 Python
tensorflow2.0教程之Keras快速入门
Feb 20 Python
教你怎么用Python处理excel实现自动化办公
Apr 30 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
关于访问控制的一首PHP面试题(对属性或方法的访问控制)
2012/09/13 PHP
php使用高斯算法实现图片的模糊处理功能示例
2016/11/11 PHP
用JavaScript 处理 URL 的两个函数代码
2007/08/13 Javascript
Js动态创建div
2008/09/25 Javascript
基于jQuery的图片大小自动适应实现代码
2010/11/17 Javascript
js判断运行jsp页面的浏览器类型以及版本示例
2013/10/30 Javascript
js实现简单登录功能的实例代码
2013/11/09 Javascript
用nodejs实现PHP的print_r函数代码
2014/03/14 NodeJs
jQuery基于图层模仿五星星评价功能的方法
2015/05/07 Javascript
JS获取月份最后天数、最大天数与某日周数的方法
2015/12/08 Javascript
jQuery实现移动端滑块拖动选择数字效果
2015/12/24 Javascript
复杂的javascript窗口分帧解析
2016/02/19 Javascript
jQuery实现磁力图片跟随效果完整示例
2016/09/16 Javascript
dul无法加载bootstrap实现unload table/user恢复
2016/09/29 Javascript
easyui取消表单实时验证,提交时统一验证的简单实例
2016/11/07 Javascript
Bootstrap轮播图学习使用
2017/02/10 Javascript
nodejs入门教程二:创建一个简单应用示例
2017/04/24 NodeJs
Angular.js项目中使用gulp实现自动化构建以及压缩打包详解
2017/07/19 Javascript
Vue批量图片显示时遇到的路径被解析问题
2019/03/28 Javascript
微信小程序实现蓝牙打印
2019/09/23 Javascript
vue keep-alive实现多组件嵌套中个别组件存活不销毁的操作
2020/10/30 Javascript
把大数据数字口语化(python与js)两种实现
2013/02/21 Python
python基础教程之基本数据类型和变量声明介绍
2014/08/29 Python
Python遍历文件夹和读写文件的实现代码
2016/08/28 Python
基于Python Numpy的数组array和矩阵matrix详解
2018/04/04 Python
Python实现的字典排序操作示例【按键名key与键值value排序】
2018/12/21 Python
如何实现更换Jupyter Notebook内核Python版本
2020/05/18 Python
python 字符串格式化的示例
2020/09/21 Python
CSS3用@font-face实现自定义英文字体
2013/09/23 HTML / CSS
数组越界问题
2015/10/21 面试题
假日旅行社实习自我鉴定
2013/09/24 职场文书
毕业生在校学习的自我评价分享
2013/10/08 职场文书
自主实习接收函
2014/01/13 职场文书
党的群众路线教育实践活动批评与自我批评发言稿
2014/10/16 职场文书
大学生青年志愿者活动总结
2015/05/06 职场文书
检讨书怎么写?
2019/06/21 职场文书