沉思WSGI
Limodou 问题
limodou <limodou@gmail.com> reply-to python-cn@googlegroups.com to "Python.cn@google" <python-cn@googlegroups.com> date Thu, Dec 25, 2008 at 10:59 subject [CPyUG:74762] WSGI的问题集
希望对wsgi了解的介绍下:
- WSGI的app之间如何传递消息?在environ中添加吗?
- 多个app之间通过wsgi方式来工作,一般是怎么被调用的?它们的调用如何实现配置化?
- 对于request, response这样的东西如何在wsgi中处理,如何传递?是每次通过environ直接生成吗?如:
request = Request(environ)
这样做,会不会有效率问题。另外如何处理request中动态添加的属性,丢失吗?
讨论集锦
WSGI的app之间如何传递消息
- Limodou
def configure(app): return ErrorHandlerMiddleware( SessionMiddleware( IdentificationMiddleware( AuthenticationMiddleware( UrlParserMiddleware(app))))))
因此出现了paster,在每个app程序中添加:
def app_factory(global_config, **local_config): return application
- 然后通过paster来管理这些app的执行顺序。这样每个app还可以有自已的配置参数。这样就可以理解为什么tg和pylons要使用paster了。不过对我来说的确是理解复杂。甚至象pkg_resources的使用也是最近才一点点理解并开始使用的。
- Qiangning Hong
WSGI的处理模式为 WSGI Server -> (WSGI Middleware)* -> WSGI Application
- 一次请求只有一个app。middleware和app之间通过environ交换消息。
- YoungKing(Zopen.cn)
- 每个wsgi app是一个callable对象,传递两个参数,一个是environment ,一个是start_response 函数,
- start_response这个函数用来response,
- 你在这里设置status和header,最后返回一个iterator(通常是字符串list)
- 潘俊勇 <panjunyong@gmail.com
- filter/app之间使用environment的消息机制了交换数据的,这种基于消息,而非API的通讯方式,更加底层,可以减少依赖。
多个app之间的调用
- Qiangning Hong
- 同上,一个request最终落到一个app上。
- 不过每一个middleware对于调用者而言,接口和app是一样的。
- 一般在前端有一个appdispatcher通过URL选择执行哪一个app,可以是直接在WSGI Server里的代码指定,也可以这个dispatcher本身就是一个middleware。
- Pylons使用的Routes这两种方式都支持。WSGI有一个route标准的草案。
- 潘俊勇 <panjunyong@gmail.com
- paste支持ini文件方式的部署配置,如:
[app:main] use = egg:wo filter-with = fs [filter:fs] paste.filter_factory = zopen.frs4wsgi.filter:filter_factory directory = var/uploadtemp [server:main] use = egg:Paste#http host = 0.0.0.0 port = 8092
- 可以看到,这里不仅支持wsgi组件关系的部署,还支持各个组件的参数配置
request/response的WSGI 处置
- Qiangning Hong
- 是的,webob其实就是怎么干的。
- webob好像是用的lazy方式,初始化一个对象很轻量。如果要考虑效率问题,可以把生成的request对象也放在environ里面,之后的middleware和app就都可以直接使用了。Pylons是这样做的。
Limodou: 对于第三个问题,可能就与具体的框架相关了。目前不管是django也好,还是uliweb也好,其本上只有一个主wsgi应用在运行。不过因为uliweb的主要处理就是一个wsgi的app,所以是可以放在wsgi中进行使用的。
- Pylons是每一个Controller就是一个WSGI app。通过Routes来dispatch到某一个特定的controller运行
- YoungKing(Zopen.cn)
- 在wsgi中,environment对应request,start_response对应response.envrionment中可以传任意python对象,这个比request方便多了.你可以修改envionment,把属性放到这里来
- 潘俊勇 <panjunyong@gmail.com
关于dispatch, 在zope中叫做对象漫游traversal,bfg已经将这个转换成了一个可替换的中间件,在这里有说明和比较:
WSGI中间件是如何复用的?
- 潘俊勇 <panjunyong@gmail.com
- 关于profile的middleware,repoze社区有了repoze.profile,这里有个可用的中间件清单:
- 其中repoze.what作为认证的中间件,功能很强。
- 好像paste社区也有一个wsgi middleware清单的,刚刚没找到。
- 目前paste社区和repoze社区关系非常紧密的,一些中间件开发力量和项目也合并起来的
- Qiangning Hong
- Pylons推进Routes成为一个WSGI middleware是一个创举,这样使得框架中的dispatcher也变成可替换的了。
以前Pylons和TurboGears的最大的区别就在于一个是基于Routes一个是基于CherryPy,不可调和。
- 现在有了WSGI route标准,连dispatcher也可以替换了。
- WSGI middleware的好处是可重用性好,所有的WSGI-aware框架都可以使用。
PASTE本身是一些WSGI组件的集合和工具,用不着的话当然完全可以不使用它。
- Pylons是从RoR抄过来的,所以有Controller/View的概念。喜欢用函数的话,完全也可以写成一个函数就是一个WSGI
- Pylons推进Routes成为一个WSGI middleware是一个创举,这样使得框架中的dispatcher也变成可替换的了。
application的。比如写成这样:
def say_hello(environ, start_response): start_response("200 OK", [('Content-type', 'text/plain')]) return ["Hello %s!" %environ['selector.vars']['name']] from selector import Selector s = Selector() s.add('/myapp/hello/{name}', GET=say_hello) from flup.server.scgi import WSGIServer WSGIServer(s).run()
selector是一个WSGI URL Dispatcher Middleware,根据URL选择一个WSGI app运行,
用Routes也可以实现类似效果。
顺便说一句,paste.debug.profile.ProfileMiddleware就是一个用来做 profile 的 WSGI middleware
meilanweb
paste分成三个部分,
- 一个就叫Paste,是一个组件库,包含几个WSGI server和很多WSGI middleware。
- Paste Script提供一个命令行界面,便于创建项目、启动服务等等。
- Paste Deploy提供一个配置方法,便于系统配置和部署。你如果用不到这些东西,自然不需要。
比如我自己为了在GAE上做东西玩,自己做了一个framework玩,由于只是自己用,不需要考虑配置、互换性等问题,就完全没有使用pastescript/deploy,只用了他一个middleware,代码很简单,一共142行,但基本的功能(URL Dispatch、Template、Authentication)已经可以用了。
- 但uliweb如果要做成一个通用的框架满足更多人的需要的话,可能还是看一下paste比较好。
1 #meilanweb.py
2 import os
3 import sys
4 import wsgiref.simple_server
5 import wsgiref.handlers
6 from types import TypeType, ClassType
7 import threading
8
9 import webob
10 import webob.exc
11 from routes import Mapper as _Mapper
12 from routes.middleware import RoutesMiddleware
13 from paste.exceptions.errormiddleware import ErrorMiddleware
14 import webhelpers as h
15 from mako.lookup import TemplateLookup
16 from google.appengine.api import users
17
18 c = threading.local()
19
20 def Mapper(*a, **kw):
21 prefix = kw.pop('prefix', None)
22 m = _Mapper(*a, **kw)
23 if prefix:
24 m.prefix = prefix
25 return m
26
27 def make_app(mapper, controllers, template_dir=None, debug=False):
28 app = Application(controllers, template_dir=template_dir)
29 app = AuthenticateMiddleware(app)
30 app = RoutesMiddleware(app, mapper)
31 app = ErrorDocumentMiddleware(app)
32 app = ErrorMiddleware(app, debug=debug)
33 return app
34
35 def run(app, host='', port=8080):
36 server = wsgiref.simple_server.make_server(host, port, app)
37 server.serve_forever()
38
39 def run_cgi(app):
40 wsgiref.handlers.CGIHandler().run(app)
41
42 class Application(object):
43 def __init__(self, controllers, template_dir=None):
44 self.controllers = controllers
45 if template_dir:
46 self.lookup = TemplateLookup(template_dir, input_encoding='utf8',
47 output_encoding='utf8')
48
49 def __call__(self, environ, start_response):
50 controller = self.resolve(environ)
51 environ['meilanweb.render'] = self.render
52 s = controller(environ, start_response)
53 return s
54
55 def resolve(self, environ):
56 """Get the controller app from the environ."""
57 match = environ['wsgiorg.routing_args'][1]
58 try:
59 controller_name = match['controller']
60 except KeyError:
61 raise webob.exc.HTTPNotFound()
62 controller_class_name = \
63 class_name_from_controller_name(controller_name)
64 controller = self.controllers[controller_class_name]
65 if isinstance(controller, (TypeType, ClassType)):
66 controller = controller()
67 return controller
68
69 def render(self, template, **kwargs):
70 return self.lookup.get_template(template).render(**kwargs)
71
72
73 def class_name_from_controller_name(controller_name):
74 # the code is stolen from Pylons
75 """
76 Excample::
77
78 >>> class_name_from_controller_name('with-dashes')
79 'WithDashes'
80 >>> class_name_from_controller_name('with_underscores')
81 'WithUnderscores'
82 >>> class_name_from_controller_name('oneword')
83 'Oneword'
84 """
85 words = controller_name.replace('-', '_').split('_')
86 return ''.join(w.title() for w in words)
87
88
89 class Controller(object):
90 def __call__(self, environ, start_response):
91 c.__dict__.clear()
92 self.environ = environ
93 match = environ['wsgiorg.routing_args'][1]
94 action_name = match['action'].replace('-', '_')
95 action = getattr(self, action_name)
96 kwargs = match.copy()
97 del kwargs['controller']
98 del kwargs['action']
99 self.request = webob.Request(environ)
100 self.response = webob.Response(request=self.request)
101 retval = action(**kwargs)
102 if retval:
103 self.response.write(retval)
104 return self.response(environ, start_response)
105
106 def render(self, template, **kwargs):
107 return self.environ['meilanweb.render'](template, c=c, h=h, **kwargs)
108
109
110 def default_template_dir(filepath):
111 here = os.path.dirname(os.path.abspath(filepath))
112 return os.path.join(here, 'templates')
113
114
115 class ErrorDocumentMiddleware(object):
116 def __init__(self, app):
117 self.app = app
118
119 def __call__(self, environ, start_response):
120 try:
121 return self.app(environ, start_response)
122 except webob.exc.WSGIHTTPException, e:
123 e.environ = environ
124 return e(environ, start_response)
125
126
127 class AuthenticateMiddleware(object):
128 def __init__(self, app):
129 self.app = app
130
131 def __call__(self, environ, start_response):
132 user = users.get_current_user()
133 if user:
134 environ['REMOTE_USER'] = user
135 try:
136 return self.app(environ, start_response)
137 except webob.exc.HTTPUnauthorized, e:
138 req = webob.Request(environ)
139 url = users.create_login_url(req.url)
140 raise webob.exc.HTTPTemporaryRedirect(location=url)
141
142 h.create_logout_url = users.create_logout_url
整体
- Gu Yingbo <tensiongyb@gmail.com
- 我认为wsgi的中间件倾向于做一些简单独立事情,每个中间件app 的依赖性比较低,
像werkzeug的DebuggedApplication这种风格就很好,一条语句就可以为一个wsgi应用添加一个调试界面。
- 我自己也就用中间件做一个请求的计时,sql语句调试开关这样简单的事情。
我觉得wsgi的最大好处是:
- 分离了服务器和应用的实现,
- 写一个wsgi应用就有一大把的部署方式,自己可以根据需要选择合适的。
for UliWeb
- Limodou
- 谢谢,这下就清楚许多了。
- 按上面的回答,uliweb中的是Dispatcher(类名都是这么起的),所以它本身是wsgi的。
- 在uliweb的前段时间,还可以通过config.py来添加在执行Dispatcher前的wsgi序列,不过现在去了,因为修改了许多地方,加回来也很容易。
- 因此我想uliweb会同时支持wsgi middleware和django-like middleware。
- 因为django-like middleware更多是在针对request, response的处理,目标比较统一。
- 对于wsgi的互用,我提第4个问题:
不同的wsgi middleware在不同的项目中是如何互用的?
- 是不是目前都是需要基于paster才可以?有共它的方式,还是根本不存在问题。
- 因为我看到paster是可以在创建wsgi middleware时引入一些local_conf的,这个倒是可以继续用,自已写一个简单的装入器。其它的有没有类似要依赖于paster的东西呢?
复用vs易用
- UliWeb 对策
- 现在想一想wsgi就是提供了一个标准的接口,可以在不同的组件之间进行互相调用。不过把view函数写成app还是感觉麻烦,一个hello在uliweb中只要:
1 @expose('/myapp/hello/<name>')
2 def hello(name):
3 return 'hello, %s!' % name
- 就可以了。按你的做法,say_hello其实是需要知道前面使用了selector的。这种我认为是一种假定的方式,可能会有问题。主要还是显得麻烦,对于开发者来说需要了解的东西太多。做为框架完全可以再简化。
- 有时追求复用与代码简化是冲突的,而我是希望用户用起来简单,在一定程度上的复用。而且与业务相关的处理复用本来难度就很大,倒不如考虑将与业务无关的东西复杂,并且减少对外部环境的假定或依赖更好一些。当然,具体还要看到底是什么东西是可以复用,应该如何被复用,也不是件容易的事。
- Qiangning Hong
- 那个仅仅是selector的一个演示而已,一般情况下view里面是不需要使用selector.vars的。
框架要干的事情就是把问题简化,比如用selector的作者写的另一个工具yaro的话,上面的例子就变成这样:
from yaro import Yaro @Yaro def hello_world(req): return "Hello World!" from selector import Selector s = Selector() s.add('/myapp/hello/{name}', GET=say_hello) from flup.server.scgi import WSGIServer WSGIServer(s).run()
完全还可以更简单。比如我前面贴的meilanweb.py里Controller.__call__里面那一堆就是为了让写应用的时候变简单的,写代码时只要操作webob的对象就可以了,不用涉及WSGI的细节。
反馈
创建 by -- ZoomQuiet [DateTime(2008-12-25T04:10:48Z)]