WSGI(Web Server Gateway Interface,Web 服务器网关接口)是一个 Python Web Application 和 Web Server 之间的标准交互接口规范,定义了 Web Application 如何集成到不同的 Web Server(e.g. Apache、Nginx 等)、或高并发的网络框架(e.g. Eventlet)中的交互标准,包括调用接口函数、请求和响应的数据结构以及环境变量等等,使得它们能够协同工作。类似于 Java 的 Servlet。
WSGI 的核心思想就是将 Web Application 和 Web Server 进行解耦,在解耦之后,还可以在它们两者之间再加入一个 “中间层" 来完成灵活的功能组合:
通过这样的解耦,使得开发者可以选用理想的 Web 框架进行开发而无需关注 Web Server 的实现细节,同时也能够将这些 Web Application 部署到不同类型的 Web Server 上运行,例如:在 Apache 上安装 wsgi_mod 模块、在 Nginx 上安装 uWSGI 模块以支持 WSGI 规范。定义这些规范的目的是为了定义统一的标准,提升程序的可移植性。
从 WSGI Server 的角度看,它首先需要知道如何调用 WSGI Application,并将 HTTP Request 传递给它,然后再从 Application 处接收到返回的 HTTP Response,再最终返回给 Client。
所以 WSGI 规范为 Server 和 Application 之间的交互定义了一个唯一且统一的 application() 接口,并由 Application 实现、由 Server 调用。接口的原型如下:
def application(environ, start_response):
pass
Application 通常会以 Python Module 或 app 实例对象的方式来提供这个 application() 接口。以 web.py 框架为例,它本身是一个 Python Module 实现了 application(),可以被 Server 导入并调用。application() 作为 Application 的唯一入口,所以 Server 会将所有 HTTP Requests 都调用这个接口来进行处理。
import web
urls = (
'/.*', 'hello',
)
class hello(object):
def GET(self):
return "Hello, world."
app = web.application(urls, globals()).wsgifunc()
environ(环境参数)是一个 Dict 类型对象,用于存放下列数据:
其中,CGI 环境变量信息包括:
WSGI 环境变量信息包括:
start_resposne(回调函数参数)指向一个 Server 实现的回调函数,Application 通过这个回调函数返回 HTTP Response 给 Server。
WSGI 规范定义了这个由 Server 实现的回调函数可以接受 2 个必选参数和 1 个可选参数:
start_resposne(status, resposne_headers, exc_info=None)
def application(environ, start_response):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return ['hello, world']
如下图所示,WSGI Middleware 本身兼具了 WSGI Applicant 和 WSGI Server 的角色。因此它可以在两端之间起协调作用,经过不同的 Middleware,便拥有了不同的功能,例如:URL 路由转发、身份权限认证。
Paste + PasteDeploy + Routes + WebOb 是一个常见的 WSGI Web 开发框架。
Paste:是一个 WSGI 工具库,用于处理 Python WSGI Application 和 WSGI Server 之间的通信标准,同时提供了许多 WSGI Middleware 组件,用于处理 Request 和 Response,包括:会话管理,身份验证等等。满足 WSGI 规范的解耦架构设计。
PasteDeploy:是 Paste 的一个用于加载 WSGI Application 到 WSGI Server 的插件模块,支持通过 api-paste.ini 配置文件的方式和机制来定义与管理 WSGI Middleware 和 WSGI Application 在 Web Server 上的部署形态。通过 api-paste.ini 配置文件,开发者可以通过可插拔的方式来组装一个 Web Application。
Routes:是一个基于 URL 的 HTTP Request 路由分发库,用于将不同的 HTTP URLs/Methods(API Resources)与 API Controllers/Functions(资源表现层状态转移控制器)映射起来,实现将 HTTP Request 转发到对应的 Functions 进行处理并返回。
WebOb:是一个将 HTTP 协议中的 Request 和 Response 标准进行 Python Object 封装的库。以 Python Object 的风格和形式提供了对 Request URL、Request Header、Request Body、Request Filter & Query Parameter 以及 Response Header、Response Status Codes、Response Message、Response Body 的操作方式。此外,WebOb 还提供了一些简便的方法来处理 Cookie、会话管理、文件上传等常见的 Web 功能。
Paste + PasteDeploy + Routes + WebOb 能够开发符合 WSGI 的 Web Application。但该框架的弊端是实现复杂,代码量大,所以在 OpenStack 中只有 Neutron 等几个初始项目在使用。后来的新项目都采用了更简单高效的 Pecan 框架。
NOTE:下文中以早期的 neutron-icehouse-eol 版本的代码来进行说明,介绍 Neutron Server 启动 API Service(Web Application)的流程。
# /Users/fanguiju/workspace/neutron-icehouse-eol/setup.cfg
[entry_points]
console_scripts =
......
neutron-server = neutron.server:main
# /Users/fanguiju/workspace/neutron-icehouse-eol/neutron/server/__init__.py
# 实例化 API Service
neutron_api = service.serve_wsgi(service.NeutronApiService)
......
# 实例化 RPC Worker
neutron_rpc = service.serve_rpc()
# /Users/fanguiju/workspace/neutron-icehouse-eol/neutron/common/config.py
def load_paste_app(app_name):
"""Builds and returns a WSGI app from a paste config file.
:param app_name: Name of the application to load
:raises ConfigFilesNotFoundError when config file cannot be located
:raises RuntimeError when application cannot be loaded from config file
"""
config_path = cfg.CONF.find_file(cfg.CONF.api_paste_config)
......
app = deploy.loadapp("config:%s" % config_path, name=app_name)
此时,PasteDeploy 通过 api-paste.ini 文件来配置 WSGI Middleware 和 WSGI Application 的组合。
Neutron 的 api-paste.ini 文件如下:
# /Users/fanguiju/workspace/neutron-icehouse-eol/etc/api-paste.ini
[composite:neutron]
use = egg:Paste#urlmap
/: neutronversions
/v2.0: neutronapi_v2_0
[composite:neutronapi_v2_0]
use = call:neutron.auth:pipeline_factory
noauth = request_id catch_errors extensions neutronapiapp_v2_0
keystone = request_id catch_errors authtoken keystonecontext extensions neutronapiapp_v2_0
[filter:request_id]
paste.filter_factory = neutron.openstack.common.middleware.request_id:RequestIdMiddleware.factory
[filter:catch_errors]
paste.filter_factory = neutron.openstack.common.middleware.catch_errors:CatchErrorsMiddleware.factory
[filter:keystonecontext]
paste.filter_factory = neutron.auth:NeutronKeystoneContext.factory
[filter:authtoken]
paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory
[filter:extensions]
paste.filter_factory = neutron.api.extensions:plugin_aware_extension_middleware_factory
[app:neutronversions]
paste.app_factory = neutron.api.versions:Versions.factory
[app:neutronapiapp_v2_0]
paste.app_factory = neutron.api.v2.router:APIRouter.factory
入口是 composite Section,用于将 HTTP Request URL 分发到指定的 Middleware 和 Application。
这里以 /v2.0 为例,urlmap_factory() 返回了 neutronapi_v2_0 composite Section,它包含了一个 Middleware Pipeline。Pipeline 用于把一系列 Middleware 和最终的 Application 串联起来,并遵守以下 2 点要求:
在 filter 和 app Section 中定义了 paste.filter_factory 实际的入口函数。
Routes 的常规用法是首先构建一个 Mapper 实例对象,然后调用 Mapper 对象的 connect() 或 collection() 方法把相应的 URL、HTTP Method 映射到一个 Controller 的某个 Action 上。
Controller 是一个自定义的类实例,每一种 API Resource 都实现了一个 Controller,内含了多个与 HTTP Methods 对应的 Actions 成员方法,例如:list、show、create、update、delete 等。
通过 Paste/PasteDeploy 的配置处理之后进入到 neutronapiapp_v2_0 Application 的 factory() 函数,Paste 最终会调用它。在 APIRouter 类的实例化过程中会完成 Routes Mapper 的映射工作。
class APIRouter(wsgi.Router):
@classmethod
def factory(cls, global_config, **local_config):
return cls(**local_config)
def __init__(self, **local_config):
mapper = routes_mapper.Mapper()
plugin = manager.NeutronManager.get_plugin() # Core Plugin
ext_mgr = extensions.PluginAwareExtensionManager.get_instance() # Service Plugins
ext_mgr.extend_resources("2.0", attributes.RESOURCE_ATTRIBUTE_MAP)
col_kwargs = dict(collection_actions=COLLECTION_ACTIONS, # 集合操作类型:index、create
member_actions=MEMBER_ACTIONS) # 成员操作类型:show、update、delete
def _map_resource(collection, resource, params, parent=None):
allow_bulk = cfg.CONF.allow_bulk
allow_pagination = cfg.CONF.allow_pagination
allow_sorting = cfg.CONF.allow_sorting
controller = base.create_resource(
collection, resource, plugin, params, allow_bulk=allow_bulk,
parent=parent, allow_pagination=allow_pagination,
allow_sorting=allow_sorting)
path_prefix = None
if parent:
path_prefix = "/%s/{%s_id}/%s" % (parent['collection_name'],
parent['member_name'],
collection)
mapper_kwargs = dict(controller=controller,
requirements=REQUIREMENTS,
path_prefix=path_prefix,
**col_kwargs)
return mapper.collection(collection, resource,
**mapper_kwargs)
# 顶级资源
mapper.connect('index', '/', controller=Index(RESOURCES))
for resource in RESOURCES:
_map_resource(RESOURCES[resource], resource,
attributes.RESOURCE_ATTRIBUTE_MAP.get(
RESOURCES[resource], dict()))
for resource in SUB_RESOURCES:
_map_resource(SUB_RESOURCES[resource]['collection_name'], resource,
attributes.RESOURCE_ATTRIBUTE_MAP.get(
SUB_RESOURCES[resource]['collection_name'],
dict()),
SUB_RESOURCES[resource]['parent'])
# Certain policy checks require that the extensions are loaded
# and the RESOURCE_ATTRIBUTE_MAP populated before they can be
# properly initialized. This can only be claimed with certainty
# once this point in the code has been reached. In the event
# that the policies have been initialized before this point,
# calling reset will cause the next policy check to
# re-initialize with all of the required data in place.
policy.reset()
super(APIRouter, self).__init__(mapper)
WebOb 有 3 个重要的对象:
此外,WebOb 还提供了一个 webob.dec.wsgify 装饰器,以便我们可以不使用原始的 WSGI 参数传递和返回格式,而全部使用 WebOb 替代。
@wsgify
def myfunc(req):
return webob.Response('Hey!')
WSGI 原始调用方式:
app_iter = myfunc(environ, start_response)
WebOb 调用方式:
resp = myfunc(req)
# /Users/fanguiju/workspace/neutron-icehouse-eol/neutron/service.py
def _run_wsgi(app_name):
......
server = wsgi.Server("Neutron")
server.start(app, cfg.CONF.bind_port, cfg.CONF.bind_host,
workers=cfg.CONF.api_workers)