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 strip lstrip rstrip使用方法
Sep 06 Python
Python 文件读写操作实例详解
Mar 12 Python
仅用500行Python代码实现一个英文解析器的教程
Apr 02 Python
python通过定义一个类实例作为ftp回调方法
May 04 Python
Windows系统下使用flup搭建Nginx和Python环境的方法
Dec 25 Python
Python使用numpy实现BP神经网络
Mar 10 Python
Python绘制堆叠柱状图的实例
Jul 09 Python
Python(PyS60)实现简单语音整点报时
Nov 18 Python
Python实现动态给类和对象添加属性和方法操作示例
Feb 29 Python
PyQt5的相对布局管理的实现
Aug 07 Python
python使用dlib进行人脸检测和关键点的示例
Dec 05 Python
python反扒机制的5种解决方法
Feb 06 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字符串 ==比较运算符的副作用
2009/10/21 PHP
PHP编码转换
2012/11/05 PHP
利用phpexcel把excel导入数据库和数据库导出excel实现
2014/01/09 PHP
php解压文件代码实现php在线解压
2014/02/13 PHP
PHP goto语句简介和使用实例
2014/03/11 PHP
php验证是否是md5编码的简单代码
2014/04/01 PHP
Thinkphp的volist标签嵌套循环使用教程
2014/07/08 PHP
YII框架批量插入数据的方法
2017/03/18 PHP
PHP基于cookie实现统计在线人数功能示例
2019/01/16 PHP
ThinkPHP3.2框架自带分页功能实现方法示例
2019/05/13 PHP
仿迅雷焦点广告效果(JQuery版)
2008/11/19 Javascript
YUI的Tab切换实现代码
2010/04/11 Javascript
jQuery筛选器children()案例详解(图文)
2013/02/17 Javascript
JS中捕获console.log()输出的方法
2015/04/16 Javascript
jQuery的几个我们必须了解的特点
2015/05/03 Javascript
jQuery树控件zTree使用方法详解(一)
2017/02/28 Javascript
关于vuex的学习实践笔记
2017/04/05 Javascript
JS点击缩略图整屏居中放大图片效果
2017/07/04 Javascript
Node.js学习教程之HTTP/2服务器推送【译】
2017/10/31 Javascript
Vue拖拽组件开发实例详解
2018/05/11 Javascript
vue实现下拉加载其实没那么复杂
2019/08/13 Javascript
vue+layui实现select动态加载后台数据的例子
2019/09/20 Javascript
vue倒计时刷新页面不会从头开始的解决方法
2020/03/03 Javascript
vue 获取url里参数的两种方法小结
2020/11/12 Javascript
python模仿网页版微信发送消息功能
2018/02/24 Python
Python实现的多项式拟合功能示例【基于matplotlib】
2018/05/15 Python
pandas计算最大连续间隔的方法
2019/07/04 Python
Python是什么 Python的用处
2020/05/26 Python
CSS3中动画属性transform、transition和animation属性的区别
2016/09/25 HTML / CSS
乐高积木玩具美国官网:LEGO Shop US
2016/09/16 全球购物
英国蛋糕装饰用品一站式商店:Craft Company
2019/03/18 全球购物
药品业务员岗位职责
2014/04/17 职场文书
班干部演讲稿
2014/04/24 职场文书
大学生就业求职信
2014/06/12 职场文书
授权委托书公证
2014/09/14 职场文书
pytorch通过训练结果的复现设置随机种子
2021/06/01 Python