WSGI

目录:
TOC #
1. WSGI 简介
2. webob
3. Paste Deploy

一、WSGI 简介

官网:http://wsgi.readthedocs.io/en/latest/what.html
http://wsgi.readthedocs.io/en/latest/learn.html

原文地址:http://blog.csdn.net/sraing/article/details/8455242

wsgi 是一个 web 组件的接口规范(WSGI is the Web Server Gateway Interface. It is a specification that describes how a web server communicates with web applications, and how web applications can be chained together to process one request.),wsgi将 web 组件分为三类: web服务器,web中间件,web应用程序

下面分别来看这三个组件

1.WSGI Server/gateway :

wsgi server可以理解为一个符合wsgi规范的web server,接收request请求,封装一系列环境变量,按照wsgi规范调用注册的wsgi app,最后将response返回给客户端。文字很难解释清楚wsgi server到底是什么东西,以及做些什么事情,最直观的方式还是看wsgi server的实现代码。以python自带的wsgiref为例,wsgiref是按照wsgi规范实现的一个简单wsgi server。它的代码也不复杂,下图是我读wsgiref代码后整理的。

wsgi server 基本工作流程
1. 服务器创建socket,监听端口,等待客户端连接。
2. 当有请求来时,服务器解析客户端信息放到环境变量environ中,并调用绑定的handler来处理请求。
3. handler解析这个http请求,将请求信息例如method,path等放到environ中。
4. wsgi handler再将一些服务器端信息也放到environ中,最后服务器信息,客户端信息,本次请求信息全部都保存到了环境变量environ中。
5. wsgi handler 调用注册的wsgi app,并将environ和回调函数传给wsgi app
6. wsgi app 将reponse header/status/body 回传给wsgi handler
7. 最终handler还是通过socket将response信息塞回给客户端。

2.WSGI Application

wsgi application就是一个普通的callable对象,当有请求到来时,wsgi server会调用这个wsgi app。这个对象接收两个参数,通常为environ,start_response。environ就像前面介绍的,可以理解为环境变量,跟一次请求相关的所有信息都保存在了这个环境变量中,包括服务器信息,客户端信息,请求信息。start_response是一个callback函数,wsgi application通过调用start_response,将response headers/status 返回给wsgi server。此外这个wsgi app会return 一个iterator对象 ,这个iterator就是response body。这么空讲感觉很虚,对着下面这个简单的例子看就明白很多了。

#environ :包含有CGI 式环境变量的字典,由server负责提供内容
# start_response:由server提供的回调函数,其作用是将状态码和响应头返回给server
# 构造响应体,以可迭代字符串形式封装返回
def simple_app(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [u"This is hello wsgi app".encode('utf8')]

我们再用wsgiref 作为wsgi server ,然后调用这个wsgi app,就能直观看到一次request,response的效果,简单修改代码如下:

from wsgiref.simple_server import make_server

def simple_app(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [u"This is hello wsgi app".encode('utf8')]

httpd = make_server('', 8000, simple_app)
print "Serving on port 8000..."
httpd.serve_forever()

访问http://127.0.0.1:8000 就能看到效果了。

此外,上面讲到了wsgi app只要是一个callable对象就可以了,因此不一定要是函数,一个实现了call方法的实例也可以,示例代码如下:

from wsgiref.simple_server import make_server

class AppClass:

    def __call__(self,environ, start_response):
        status = '200 OK'
        response_headers = [('Content-type', 'text/plain')]
        start_response(status, response_headers)
        return ["hello world!"]

app = AppClass()
httpd = make_server('', 8000, app)
print "Serving on port 8000..."
httpd.serve_forever()

3.WSGI MiddleWare

上面的application看起来没什么意思,感觉没有太大用,但加上一层层的middleware包装之后就不一样了。一堆文字解释可能还没有一个demo更容易说明白,我写了一个简单Dispatcher Middleware,用来实现URL 路由:

from wsgiref.simple_server import make_server

URL_PATTERNS= (
    ('hi/','say_hi'),
    ('hello/','say_hello'),
    )

class Dispatcher(object):

    def _match(self,path):
        path = path.split('/')[1]
        for url,app in URL_PATTERNS:
            if path in url:
                return app

    def __call__(self,environ, start_response):
        path = environ.get('PATH_INFO','/')
        app = self._match(path)
        if app :
            app = globals()[app]
            return app(environ, start_response)
        else:
            start_response("404 NOT FOUND",[('Content-type', 'text/plain')])
            return ["Page dose not exists!"]

def say_hi(environ, start_response):
    start_response("200 OK",[('Content-type', 'text/html')])
    return ["kenshin say hi to you!"]

def say_hello(environ, start_response):
    start_response("200 OK",[('Content-type', 'text/html')])
    return ["kenshin say hello to you!"]

app = Dispatcher()

httpd = make_server('', 8000, app)
print "Serving on port 8000..."
httpd.serve_forever()

上面的例子可以看出来,middleware 包装之后,一个简单wsgi app就有了URL dispatch功能。然后我还可以在这个app外面再加上其它的middleware来包装它,例如加一个权限认证的middleware:

class Auth(object):
    def __init__(self,app):
        self.app = app

    def __call__(self,environ, start_response):
        #TODO
        return self.app(environ, start_response)

app = Dispatcher()
auth_app = Auth(app)

httpd = make_server('', 8000, auth_app)
print "Serving on port 8000..."
httpd.serve_forever()

经过这些middleware的包装,已经有点框架的感觉了。其实基于wsgi的框架,例如paste,pylons就是这样通过一层层middleware组合起来的。

二、 webob

官网:http://webob.org/
http://docs.webob.org/en/stable/

1. 简介

WebOb是一个python工具包,可根据WSGI中的environment变量,封装request参数,同时同一个Response对象,用来创建WSGI响应。

一个简单的使用例子:

# -*- coding: utf-8 -*-

from wsgiref.simple_server import make_server  
from webob import Request, Response    #后面介绍这个模块  

class APPTest(object):  
    def __call__(self, environ, start_response):  
        urll =  ['%s : %s' % (key, value) for key,value in environ.items()]  #传递进来的environ环境变量  

        print('\n'.join(urll))
        print('\n\n\n')  

        req = Request(environ) #处理环境变量,生成Request对象,代表客户端HTTP请求传递而来的环境变量  
        print(req)  

        print('\n\n\n')  
        return self.test(environ, start_response)  

    def test(self, environ, start_response):  

        urll =  ['%s : %s' % (key, value) for key,value in environ.items()]  

        print('\n'.join(urll) )  
        print('\n\n\n')  

        res =  Response(content_type='text/plain', charset='utf8') #Response类类型的实例对象res,实现__call__函数可以直接作为函数调用,对于HTTP响应 header和body的封装  
        f = res.body_file
        f.write('hey')
        print('\n\n\n')  
        print(type(res))  

        return res(environ, start_response)  

application = APPTest()  

httpd = make_server('127.0.0.1', 8000, application)  
print('Listen on port 8000....')  
httpd.serve_forever()  

2. @wsgify装饰器

作用:wsgify装饰器将一个普通函数转变成WSGI应用程序。

from wsgiref.simple_server import make_server  
from webob import Request, Response  
from webob.dec import *  

@wsgify  
def test(req):  
        res =  Response(content_type='text/plain', charset='utf8') #Response类类型的实例对象res,实现__call__函数可以直接作为函数调用,对于HTTP响应 header和body的封装  
        f = res.body_file
        f.write('hey')
        #return res(environ, start_response)
        return res  

class MyApp:  
    def __call__(self, environ, start_response):             
        return test(environ, start_response)  

application = MyApp()  

httpd = make_server('localhost', 8081, application)    
httpd.serve_forever()   

三、Paste Deploy

官网:http://pythonpaste.org/deploy/

http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/paste.html
http://pythonpaste.org/deploy/

下面转载:http://www.cnblogs.com/Security-Darren/p/4087587.html

1.Paste Deploy简介

PasteDeploy defines a way to declare WSGI application configuration in an .ini file.

paste deploy是用来发现和配置WSGI应用的一套系统,对于WSGI应用的使用者而言,可以方便地从配置文件汇总加载WSGI应用(loadapp);对于WSGI应用的开发人员而言,只需要给自己的应用提供一套简单的入口点即可。

2 .PasteDeploy配置文件格式

PasteDeploy配置文件由若干section组成,section的声明格式如: [type:name]

其中,方括号括起的section声明一个新section的开始,section的声明由两部分组成,section的类型(type)和section的名称(name),如:[app:main]等。section的type可以有:app、composite、filter、pipeline、filter-app等。

每个section中具体配置项的格式就是基本的ini格式: key = value ,所有从PasteDeploy配置文件中提取的参数键、值都以字符串的形式传入底层实现。

  此外,PasteDeploy的配置文件中使用“#”标注注释。

  在基本了解PasteDeploy配置文件的书写格式后,我们不妨看一个实例,来具体了解不同type的section。
  

[composite:main]
  use = egg:Paste#urlmap
  / = home
  /blog = blog
  /wiki = wiki
  /cms = config:cms.ini

  [app:home]
  use = egg:Paste#static
  document_root = %(here)s/htdocs

  [app:wiki] 
  use = call:mywiki.main:application 
  database = sqlite:/home/me/wiki.db

  [filter-app:blog] 
  use = egg:Authentication#auth 
  next = blogapp 
  roles = admin 
  htpasswd = /home/me/users.htpasswd 

  [app:blogapp]
  use = egg:BlogApp
  database = sqlite:/home/me/blog.db

  [app:main]
  use = egg:MyEgg
  filter-with = printdebug

  [filter:printdebug] 
  use = egg:Paste#printdebug 

  [pipeline:main]
  pipeline = filter1 filter2 filter3 app
  ...

上面的示例文件列出了若干不同type的section示意,下面就一一探讨PasteDeploy可定义的section type.

2.1  Type = composite(组合应用)
  顾名思义,组合应用由若干WSGI应用组成,composite为这些应用提供更高一层的分配工作。
  下面具体分析示例1中的如下部分:

[composite:main]
use = egg:Paste#urlmap
/ = home
/blog = blog
/wiki = wiki
/cms = config:cms.ini

该段配置文件定义了一个名为main、类型为composite的section,方括号的声明以下是该section的具体配置,遵循 key = value 的统一格式。Composite类型的section将URL请求分配给其他的WSGI应用。 use = egg:Paste#urlmap 意味着使用 Paste 包中的 urlmap 应用。urlmap是Paste提供的一套通用的composite应用,作用就是根据用户请求的URL前缀,将用户请求映射到对应的WSGI应用上去。这里的WSGI应用有:”home”, “blog”, “wiki” 和 “config:cms.ini”。

  最后一项仅仅是参考了同一个目录中的另一个文件”cms.ini”

2.2  Type = app(WSGI应用)

  回到示例1中的下一部分:
  

[app:home]
use = egg:Paste#static
document_root = %(here)s/htdocs

[app:wiki] 
use = call:mywiki.main:application 
database = sqlite:/home/me/wiki.db

app类型的section声明一个具体的WSGI应用。调用哪个python module中的app代码则由的use后的值指定。这里的 egg:Paste#static 是另一个简单应用,作用仅仅是呈现静态页面。它接收了一个配置项: document_root ,后面的值可以从全局配置DEFAULT中提取,提取方法s是使用变量替换:比如 %(var_name)s 的形式。这里 %(here)s 的意思是这个示例配置文件所在的目录,因为相对路径在不同服务器中的解释方法不同,出于移植性的考虑,官方文档上推荐当前这种写法。

  示例中定义了多个app类型的section,因为PasteDeploy的配置文件中允许定义多个app类型的section,同时要求每个WSGI应用也都应该拥有自己的section。这样,每一个WSGI应用在配置文件中都有一个app类型的section与之对应,默认地,”main”应用对应于 app:main 或 app 。应用的具体实现要在section中配置,有两种方法专门用于指出应用对应的代码:使用URI(用use标识)或 直接指向实现代码(用protocol标识)。
  (1)使用另一个URI
  采用该方法的特点是指出应用的实现代码的那一条 key = value 配置项采用”use”作为键,该方法也有许多变种,官方的示例中给出了一些介绍:
  

[app:myapp]
use = config:another_config_file.ini#app_name

# 或任意URI:
[app:myotherapp]
use = egg:MyApp

# 或指明某个模块中的可调用:
[app:mythirdapp]
use = call:my.project:myapplication

# 甚至是其他的section:
[app:mylastapp]
use = myotherapp

 最后指向其他的section的那个例子,看起来似乎没有什么意义,似乎只是两个相同的WSGI应用。但是这样的定义允许我们在 [app:mylastapp] 这个应用中定义一些局部的配置项,从而在重用代码的同时覆写它引用的应用配置。

(2)直接指向应用的实现代码
采用该方法的特点是指出实现代码的那一条 key = value 配置项采用“协议”作为键,所谓“协议”即protocol,告诉PasteDeploy即将加载的对象类型,如:

[app:myapp]
paste.app_factory = myapp.modulename:app_factory

该例的protocol paste.app_factory 是一个应用的工厂函数,指明import对象的类型;值 myapp.modulename:app_factory 指明具体加载的模块和方法。

  关于PasteDeploy的协议,进而可以定义的工厂函数类型,我们在下文对所有section可选的type有一定的了解后再进行探讨。

2.3  Type = filter(过滤器)

  filter是作用于WSGI应用上的函数或方法,以app为唯一的参数,并返回一个“过滤”后的app。归功于WSGI接口的规范,不同的filter可以依次“过滤”某一app,事实上多个filter处理一个app也就是下文中提到的管道(pipeline)。在PasteDeploy的配置文件中有多种方法来“过滤”应用,比如示例1中:
  

[app:main]
use = egg:MyEgg
filter-with = printdebug

[filter:printdebug] 
use = egg:Paste#printdebug

在 [app:main] 的 filter-with 字段指明用来处理该应用的filter,就指定了名为”printdebug”的filter来处理应用”main”。在 [filter:printdebug] 中还可以定义新的 filter-with 字段,从而将处理关系延续下去。
 2.4  Type = filter-app

  同样是处理应用,在PasteDeploy配置文件中可以有着不同的写法,比如示例1中的下面部分,就是使用filter-app类型的section来声明一个filter:
  

[filter-app:blog] 
use = egg:Authentication#auth 
next = blogapp 
roles = admin 
htpasswd = /home/me/users.htpasswd 

[app:blogapp]
use = egg:BlogApp
database = sqlite:/home/me/blog.db

该部分采用了[filter-app:NAME]类型的section声明了一个filter,指定使用的代码,以及要处理的应用: next 字段的值。从而PasteDeploy会自动地将过滤器”blog”作用在应用”blogapp”上。

 2.5  Type = pipeline

  pipeline便于对一个应用添加多个过滤器,比如示例1中:
  

[pipeline:main]
pipeline = filter1 filter2 filter3 app

就指定了在app上施加三个filter进行处理。

  总结起来,想要在某个应用前添加多个filter,共有 [filter-app:…] [pipeline:…] 和 [app:…] filter-with = … 等方法。

3 .局部配置与全局配置

 PasteDeploy配置文件的所有配置项均使用 key = value 格式,但是局部配置项和全局配置项定义的位置不同。如:

[app:blog]
use = egg:MyBlog
database = mysql://localhost/blogdb
blogname = This Is My Blog!

[app:otherblog]
use = blog
blogname = The other face of my blog

每一个section内设置具体的键值关系,构成这些section自己的局部配置。

  为了便于不同的应用读取某些固定的系统信息,PasteDeploy允许设置全局配置变量,所有的全局配置必须放在[DEFAULT]字段下设置,如:
  

[DEFAULT]
admin_email = webmaster@example.com

[app:main]
use = ...
set admin_email = bob@example.com

 注意[DEFAULT]段名是大小写敏感的,因此必须严格大写。
 
 局部配置和全局配置的覆写
 

[app:blog]
use = egg:MyBlog
database = mysql://localhost/blogdb
blogname = This Is My Blog!

[app:otherblog]
use = blog
blogname = The other face of my blog

 从2.2.1中已经知道,一些section可以直接复用其他section的代码,并定制配置信息,这里 [app:otherblog] 就采用了 [app:blog] 的代码,同时将配置项 blogname 改为自己特定的。

  另一方面,应用在本地可以修改全局配置项的值:

[DEFAULT]
admin_email = webmaster@example.com

[app:main]
use = ...
set admin_email = bob@example.com

只需要在要覆写的键前加 set 即可。

  至于为什么要探讨局部配置与全局配置,是因为二者在传递给不同类型的factory function时对应的参数不同,这些将在下文详细探讨。

4 .实现factory函数

 本文的第二部分探讨了PasteDeploy中的若干种“协议”,事实上对应了实现时的不同类型,包括 paste.app_factory paste.composite_factory paste.filter_factory paste.server_factory 等。这些“协议”的 value 都必须是一个可调用(函数、方法、类、可调用对象等)。这些“协议”封装WSGI应用,使其成为app、composite、filter等类型的组件,这些factory对应的格式有

4.1  paste.app_factory
这是最常见的factory,接收配置参数,用来返回一个WSGI应用,第三部分中介绍的全局配置以字典的形式传入,局部配置则以关键字参数(keyword arguments)的形式传入。

def app_factory(global_config, **local_conf):
    return wsgi_app

 4.2  paste.composite_factory
 

def composite_factory(loader, global_config, **local_conf):
    return wsgi_app

是不是与 app_factory 非常相似,composite的factory函数就是在app factory的基础上增加了 loader 参数,该参数有一些方法,比如 get_app(name_or_uri, global_conf=None) 返回指定名称的WSGI应用, get_filter 、 get_server 也有着类似的作用。
  下面是官方给出的一个composite_factory的示例:
  

def pipeline_factory(loader, global_config, pipeline):
    # space-separated list of filter and app names:
    pipeline = pipeline.split()
    filters = [loader.get_filter(n) for n in pipeline[:-1]]
    app = loader.get_app(pipeline[-1])
    filters.reverse() # apply in reverse order!
    for filter in filters:
        app = filter(app)
    return app

事实上,这也是一个pipeline factory,这个例子需要搭配PasteDeploy配置文件的具体配置工作,其中的filter、app等也都需要额外实现:

[composite:main]
use = <pipeline_factory_uri>
pipeline = egg:Paste#printdebug session myapp

[filter:session]
use = egg:Paste#session
store = memory

[app:myapp]
use = egg:MyApp

 这个factory将PasteDeploy配置文件中某个pipeline section中的filter与app分离,将位于管道末端的app(myapp)作为参数依次传递给filter(printdebug, session),由于filter一旦处理成功也返回一个app,否则拦截用户请求,所以这个过程可以一直持续下去,直至所有的filter对app进行完处理。

 4.3  paste.filter_factory

  filter factory与app factory非常相似,只是返回filter而不是WSGI app。filter必须是可调用的,接收WSGI app为唯一的参数,返回处理过的该app。paste deploy官方也给出了一个这样的例子:
  

def auth_filter_factory(global_conf, req_usernames):
    # space-separated list of usernames:
    req_usernames = req_usernames.split()
    def filter(app):
        return AuthFilter(app, req_usernames)
    return filter

class AuthFilter(object):
    def __init__(self, app, req_usernames):
        self.app = app
        self.req_usernames = req_usernames

    def __call__(self, environ, start_response):
        if environ.get('REMOTE_USER') in self.req_usernames:
            return self.app(environ, start_response)
        start_response(
            '403 Forbidden', [('Content-type', 'text/html')])
        return ['You are forbidden to view this resource']

 该filter factory产生一个简单的认证过滤器,过滤器工厂函数接收配置参数,进行处理后和app一起传给真正实现过滤器功能的类 AuthFilter 。auth_filter_factory 中的 filter 方法就是封装了具体实现的过滤器,它只接收WSGI app作为自己的参数,具体的配置参数和认证逻辑则由 class AuthFilter 的实例完成。

   class AuthFilter 的实例是一个可调用对象,事实上其 call 方法就是实现了一个符合WSGI 规范的app。WSGI app以CGI形式的环境变量environ和回调函数为参数。本例的逻辑很简单:如果在server发来的环境信息中包含了外部认证时设置的”REMOTE_USER”,则直接返回传进来的app,否则拒绝用户的认证请求。

  WSGI app通过 return 语句返回HTTP Response body,由参数中的回调函数返回HTTP 响应状态码和HTTP Response header,这都属于WSGI的规范,这里不做深入探讨。

 4.4  paste.filter_app_factory

  与filter_factory 非常相似,只是接收和返回的均为WSGI app,没有filter。

  4.3示例中如果进行如下修改:

class AuthFilter(object):
    def __init__(self, app, global_conf, req_usernames):
        ....

  那么 class AuthFilter 就是一个filter_app_factory, 对比4.1的 paste.app_factory 和4.3的 paste.filter_app_factory 即可理解。

  4.5  paste.server_factory
  返回WSGI server。WSGI server接收唯一的参数——WSGI app,并为其服务。一个简单的 paste.server_factory 例子如下:

def server_factory(global_conf, host, port):
    port = int(port)
    def serve(app):
        s = Server(app, host=host, port=port)
        s.serve_forever()
    return serve

  该工厂函数同样封装了配置参数等信息,返回一个只接收WSGI app做参数的可调用,至于 class Server 就留待具体实现。

  4.6  paste.server_runner
  与paste.server_factory 类似,只是WSGI app应该以第一参数传入,同时返回的server要立即运行。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值