花了两个星期,我终于把 WSGI 整明白了

在 三百六十行,行行转 IT 的现状下,很多来自各行各业的同学,都选择 Python 这门胶水语言做为踏入互联网大门的第一块敲门砖,在这些人里,又有相当大比例的同学选择了 Web 开发这个方向(包括我)。而从事 web 开发,绕不过一个知识点,就是 WSGI。

不管你是否是这些如上同学中的一员,都应该好好地学习一下这个知识点。

由于我本人不从事专业的 python web 开发,所以在写这篇文章的时候,借鉴了许多优秀的网络博客,并花了很多的精力阅读了大量的 OpenStack 代码。

为了写这篇文章,零零散散地花了大概两个星期。本来可以拆成多篇文章,写成一个系列的,经过一番思虑,还是准备一篇讲完,这就是本篇文章这么长的原因。

另外,一篇文章是不能吃透一个知识点的,本篇涉及的背景知识也比较多的,若我有讲得不到位的,还请你多多查阅其他人的网络博客进一步学习。

在你往下看之前,我先问你几个问题,你带着这些问题往下看,可能更有目的性,学习可能更有效果。

问 1:一个 HTTP 请求到达对应的 application 处理函数要经过怎样的过程?

问 2:如何不通过流行的 web 框架来写一个简单的 web 服务?

一个 HTTP 请求的过程可以分为两个阶段,第一阶段是从客户端到 WSGI Server,第二阶段是从 WSGI Server 到 WSGI Application

null

今天主要是讲第二阶段,主要内容有以下几点:

  1. WSGI 是什么,因何而生?
  2. HTTP 请求是如何到应用程序的?
  3. 实现一个简单的 WSGI Server
  4. 实现“高并发”的 WSGI Server
  5. 第一次路由:PasteDeploy
  6. PasteDeploy 使用说明
  7. webob.dec.wsgify 装饰器
  8. 第二次路由:中间件 routes 路由

01. WSGI 是什么,因何而生?##

WSGI 是 Web Server Gateway Interface 的缩写。

它是 Python 应用程序(application)或框架(如 Django)和 Web 服务器之间的一种接口,已经被广泛接受。

它是一种协议,一种规范,其是在 PEP 333 提出的,并在PEP 3333进行补充(主要是为了支持 Python3.x)。这个协议旨在解决众多 web 框架和 web server 软件的兼容问题。有了 WSGI,你不用再因为你使用的 web 框架而去选择特定的 web server 软件。

常见的 web 应用框架有:Django,Flask 等

常用的 web 服务器软件有:uWSGI,Gunicorn 等

那这个 WSGI 协议内容是什么呢?知乎上有人将 PEP 3333 翻译成中文,写得非常好,我将这段协议的内容搬运过来。

WSGI 接口有服务端和应用端两部分,服务端也可以叫网关端,应用端也叫框架端。服务端调用一个由应用端提供的可调用对象。如何提供这个对象,由服务端决定。例如某些服务器或者网关需要应用的部署者写一段脚本,以创建服务器或者网关的实例,并且为这个实例提供一个应用实例。另一些服务器或者网关则可能使用配置文件或其他方法以指定应用实例应该从哪里导入或获取。

WSGI 对于 application 对象有如下三点要求

  1. 必须是一个可调用的对象

  2. 接收两个必选参数 environ、start_response。

  3. 返回值必须是可迭代对象,用来表示 http body。

  4. HTTP 请求是如何到应用程序的?#


当客户端发出一个 HTTP 请求后,是如何转到我们的应用程序处理并返回的呢?

关于这个过程,细节的点这里没法细讲,只能讲个大概。

我根据其架构组成的不同将这个过程的实现分为两种:

null

1、两级结构
在这种结构里,uWSGI 作为服务器,它用到了 HTTP 协议以及 wsgi 协议,flask 应用作为 application,实现了 wsgi 协议。当有客户端发来请求,uWSGI 接受请求,调用 flask app 得到相应,之后相应给客户端。
这里说一点,通常来说,Flask 等 web 框架会自己附带一个 wsgi 服务器 (这就是 flask 应用可以直接启动的原因),但是这只是在开发阶段用到的,在生产环境是不够用的,所以用到了 uwsgi 这个性能高的 wsgi 服务器。

2、三级结构
这种结构里,uWSGI 作为中间件,它用到了 uwsgi 协议 (与 nginx 通信),wsgi 协议 (调用 Flask app)。当有客户端发来请求,nginx 先做处理 (静态资源是 nginx 的强项),无法处理的请求 (uWSGI),最后的相应也是 nginx 回复给客户端的。
多了一层反向代理有什么好处?

提高 web server 性能 (uWSGI 处理静态资源不如 nginx;nginx 会在收到一个完整的 http 请求后再转发给 wWSGI)

nginx 可以做负载均衡 (前提是有多个服务器),保护了实际的 web 服务器 (客户端是和 nginx 交互而不是 uWSGI)

  1. 实现一个简单的 WSGI Server#

在上面的架构图里,不知道你发现没有,有个库叫做wsgiref,它是 Python 自带的一个 wsgi 服务器模块。

从其名字上就看出,它是用纯 Python 编写的 WSGI 服务器的参考实现。所谓“参考实现”是指该实现完全符合 WSGI 标准,但是不考虑任何运行效率,仅供开发和测试使用。

有了 wsgiref 这个模块,你就可以很快速的启动一个 wsgi server。

from wsgiref.simple_server import make_server

# 这里的 appclass 暂且不说,后面会讲到
app = appclass()
server = make_server('', 64570, app)
server.serve_forever()

当你运行这段代码后,就会开启一个 wsgi server,监听0.0.0.0:64570,并接收请求。

使用 lsof 命令可以查到确实开启了这个端口

null

以上使用 wsgiref 写了一个 demo,让你对 wsgi 有个初步的了解。其由于只适合在学习测试使用,在生产环境中应该另寻他道。

  1. 实现“高并发”的 WSGI Server#

上面我们说不能在生产中使用 wsgiref ,那在生产中应该使用什么呢?选择有挺多的,比如优秀的 uWSGI,Gunicore 等。但是今天我并不准备讲这些,一是因为我不怎么熟悉,二是因为我本人从事 OpenStack 的二次开发,对它比较熟悉。

所以下面,是我花了几天时间阅读 OpenStack 中的 Nova 组件代码的实现,刚好可以拿过来学习记录一下,若有理解偏差,还望你批评指出。

在 nova 组件里有不少服务,比如 nova-api,nova-compute,nova-conductor,nova-scheduler 等等。

其中,只有 nova-api 有对外开启 http 接口。

要了解这个 http 接口是如何实现的,从服务启动入口开始看代码,肯定能找到一些线索。

从 Service 文件可以得知 nova-api 的入口是nova.cmd.api:main()

null

null

打开nova.cmd.api:main(),一起看看是 OpenStack Nova 的代码。

在如下的黄框里,可以看到在这里使用了 service.WSGIService 启动了一个 server,就是我们所说的的 wsgi server

null

那这里的 WSGI Server 是依靠什么实现的呢?让我们继续深入源代码。

null

wsgi.py 可以看到这里使用了 eventlet 这个网络并发框架,它先开启了一个绿色线程池,从配置里可以看到这个服务器可以接收的请求并发量是 1000 。

null

可是我们还没有看到 WSGI Server 的身影,上面使用 eventlet 开启了线程池,那线程池里的每个线程应该都是一个服务器吧?它是如何接收请求的?

再继续往下,可以发现,每个线程都是使用 eventlet.wsgi.server 开启的 WSGI Server,还是使用的 eventlet。

由于源代码比较多,我提取了主要的代码,精简如下

# 创建绿色线程池
self._pool = eventlet.GreenPool(self.pool_size)

# 创建 socket:监听的ip,端口
bind_addr = (host, port)
self._socket = eventlet.listen(bind_addr, family, backlog=backlog)
dup_socket = self._socket.dup()

# 整理孵化协程所需的各项参数
wsgi_kwargs = {
    'func': eventlet.wsgi.server,
    'sock': dup_socket,
    'site': self.app, # 这个就是 wsgi 的 application 函数
    'protocol': self._protocol,
    'custom_pool': self._pool,
    'log': self._logger,
    'log_format': CONF.wsgi.wsgi_log_format,
    'debug': False,
    'keepalive': CONF.wsgi.keep_alive,
    'socket_timeout': self.client_socket_timeout
}

# 孵化协程
self._server = utils.spawn(**wsgi_kwargs)

null

就这样,nova 开启了一个可以接受 1000 个绿色协程并发的 WSGI Server。

  1. 第一次路由:PasteDeploy#

上面我们提到 WSGI Server 的创建要传入一个 Application,用来处理接收到的请求,对于一个有多个 app 的项目。

比如,你有一个个人网站提供了如下几个模块

/blog  # 博客 app
/wiki  # wiki app

如何根据 请求的 url 地址,将请求转发到对应的 application 上呢?

答案是,使用 PasteDeploy 这个库(在 OpenStack 中各组件被广泛使用)。

PasteDeploy 到底是做什么的呢?

根据官方文档的说明,翻译如下

PasteDeploy 是用来寻找和配置 WSGI 应用和服务的系统。PasteDeploy 给开发者提供了一个简单的函数 loadapp。通过这个函数,可以从一个配置文件或者 Python egg 中加载一个 WSGI 应用。

使用 PasteDeploy 的其中一个重要意义在于,系统管理员可以安装和管理 WSGI 应用,而无需掌握与 Python 和 WSGI 相关知识。

由于 PasteDeploy 原来是属于 Paste 的,现在独立出来了,但是安装的时候还是会安装到 paste 目录(site-packages\paste\deploy)下。

我会先讲下在 Nova 中,是如何借助 PasteDeploy 实现对 url 的路由转发。

还记得在上面创建 WSGI Server 的时候,传入了一个 self.app 参数,这个 app 并不是一个固定的 app,而是使用 PasteDeploy 中提供的 loadapp 函数从 paste.ini 配置文件中加载 application。

具体可以,看下 nova 的实现。

null

通过打印的 DEBUG 内容得知 config_url 和 app name 的值

app: osapi_compute
config_url: /etc/nova/api-paste.inia

通过查看/etc/nova/api-paste.ini,在 composite 段里找到了osapi_compute这个 app(这里的 app 和 wsgi app 是两个概念,需要注意区分) ,可以看出 nova 目前有两个版本的 api,一个是 v2,一个是 v2.1,目前我们在用的是 v2.1,从配置文件中,可以得到其指定的 application 的路径是nova.api.openstack.compute这个模块下的 APIRouterV21 类 的 factory 方法,这是一个工厂函数,返回 APIRouterV21 实例。

[composite:osapi_compute]
use = call:nova.api.openstack.urlmap:urlmap_factory
/: oscomputeversions
/v2: openstack_compute_api_v21_legacy_v2_compatible
/v2.1: openstack_compute_api_v21
 [app:osapi_compute_app_v21]
paste.app_factory = nova.api.openstack.compute:APIRouterV21.factory

这是 OpenStack 使用 PasteDeploy 实现的第一层的路由,如果你不感兴趣,可以直接略过本节,进入下一节,下一节是 介绍 PasteDeploy 的使用,教你实现一个简易的 web server demo。推荐一定要看。

  1. PasteDeploy 使用说明#

到上一步,我已经得到了 application 的有用的线索。考虑到很多人是第一次接触 PasteDeploy,所以这里结合网上博客做了下总结。对你入门会有帮助。

掌握 PasteDeploy ,你只要按照以下三个步骤逐个完成即可。

1、配置 PasteDeploy 使用的 ini 文件;

2、定义 WSGI 应用;

3、通过 loadapp 函数加载 WSGI 应用;

第一步:写 paste.ini 文件

在写之前,咱得知道 ini 文件的格式吧。

首先,像下面这样一个段叫做section

[type:name]
key = value
...

其上的 type,主要有如下几种

  1. composite(组合):多个 app 的路由分发;

    [composite:main]
    use = egg:Paste#urlmap
    / = home
    /blog = blog
    /wiki = wiki
    
  2. app(应用):指明 WSGI 应用的路径;

    [app:home]
    paste.app_factory = example:Home.factory
    
  3. pipeline(管道):给一个 app 绑定多个过滤器。将多个 filter 和最后一个 WSGI 应用串联起来。

    [pipeline:main]
    pipeline = filter1 filter2 filter3 myapp
     [filter:filter1]
    ...
     [filter:filter2]
    ...
     [app:myapp]
    ...
    
  4. filter(过滤器):以 app 做为唯一参数的函数,并返回一个“过滤”后的 app。通过键值 next 可以指定需要将请求传递给谁。next 指定的可以是一个普通的 WSGI 应用,也可以是另一个过滤器。虽然名称上是过滤器,但是功能上不局限于过滤功能,可以是其它功能,例如日志功能,即将认为重要的请求数据记录下来。

    [app-filter:filter_name]
    use = egg:...
    next = next_app
     [app:next_app]
    ... 
    

对 ini 文件有了一定的了解后,就可以看懂下面这个 ini 配置文件了


[composite:main]
use = egg:Paste#urlmap
/blog = blog
/wiki = wiki
 [app:blog]
paste.app_factory = example:Blog.factory
 [app:wiki]
paste.app_factory = example:Wiki.factory 

第二步是定义一个符合 WSGI 规范的 applicaiton 对象。

符合 WSGI 规范的 application 对象,可以有多种形式,函数,方法,类,实例对象。这里仅以实例对象为例(需要实现__call__方法),做一个演示。

import os
from paste import deploy
from wsgiref.simple_server import make_server

class Blog(object):
    def __init__(self):
        print("Init Blog.")

    def __call__(self, environ, start_response):
        status_code = "200 OK"
        response_headers = [("Content-Type", "text/plain")]
        response_body = "This is Blog's response body.".encode('utf-8')

        start_response(status_code, response_headers)
        return [response_body]

 @classmethod
    def factory(cls, global_conf, **kwargs):
        print("Blog factory.")
        return Blog() 

最后,第三步是使用 loadapp 函数加载 WSGI 应用。

loadapp 是 PasteDeploy 提供的一个函数,使用它可以很方便地从第一步的 ini 配置文件里加载 app

loadapp 函数可以接收两个实参:

  • URI:"config:< 配置文件的全路径 >"
  • name:WSGI 应用的名称

conf_path = os.path.abspath('paste.ini')

# 加载 app
applications = deploy.loadapp("config:{}".format(conf_path) , "main")

# 启动 server, 监听 localhost:22800 
server = make_server("localhost", "22800", applications)
server.serve_forever() 

applications 是 URLMap 对象。

null

完善并整合第二步和第三步的内容,写成一个 Python 文件 (wsgi_server.py)。内容如下


import os
from paste import deploy
from wsgiref.simple_server import make_server

class Blog(object):
    def __init__(self):
        print("Init Blog.")

    def __call__(self, environ, start_response):
        status_code = "200 OK"
        response_headers = [("Content-Type", "text/plain")]
        response_body = "This is Blog's response body.".encode('utf-8')

        start_response(status_code, response_headers)
        return [response_body]

 @classmethod
    def factory(cls, global_conf, **kwargs):
        print("Blog factory.")
        return Blog()

class Wiki(object):
    def __init__(self):
        print("Init Wiki.")

    def __call__(self, environ, start_response):
        status_code = "200 OK"
        response_headers = [("Content-Type", "text/plain")]
        response_body = "This is Wiki's response body.".encode('utf-8')

        start_response(status_code, response_headers)
        return [response_body]

 @classmethod
    def factory(cls, global_conf, **kwargs):
        print("Wiki factory.")
        return Wiki()
      

if __name__ == "__main__":
    app = "main"
    port = 22800
    conf_path = os.path.abspath('paste.ini')

    # 加载 app
    applications = deploy.loadapp("config:{}".format(conf_path) , app)
    server = make_server("localhost", port, applications)

    print('Started web server at port {}'.format(port))
    server.serve_forever() 

一切都准备好后,在终端执行python wsgi_server.py来启动 web server

null

如果像上图一样一切正常,那么打开浏览器

注意:urlmap 对 url 的大小写是敏感的,例如如果访问http://127.0.0.1:8000/BLOG,在 url 映射中未能找到大写的 BLOG。

到此,你学会了使用 PasteDeploy 的简单使用。

  1. webob.dec.wsgify 装饰器#

经过了 PasteDeploy 的路由调度,我们找到了nova.api.openstack.compute:APIRouterV21.factory这个 application 的入口,看代码知道它其实返回了 APIRouterV21 类的一个实例。

null

WSGI 规定 application 必须是一个 callable 的对象,函数、方法、类、实例,若是一个类实例,就要求这个实例所属的类实现__call__的方法。

APIRouterV21 本身没有实现__call__,但它的父类 Router 实现了__call__

null

我们知道,application 必须遵丛 WSGI 的规范

  1. 必须接收environ,start_response两个参数;
  2. 必须返回 「可迭代的对象」。

但从 Router 的__call__代码来看,它并没有遵从这个规范,它不接收这两个参数,也不返回 response,而只是返回另一个 callable 的对象,就这样我们的视线被一次又一次的转移,但没有关系,这些__call__都是外衣,只要扒掉这些外衣,我们就能看到核心 app。

而负责扒掉这层外衣的,就是其头上的装饰器@webob.dec.wsgify,wsgify 是一个类,其__call__源码实现如下:null

可以看出,wsgify 在这里,会将 req 这个原始请求(dict 对象)封装成 Request 对象(就是规范 1 里提到的 environ)。然后会一层一层地往里地执行被 wsgify 装饰的函数(self._route), 得到最内部的核心 application。

上面提到了规范 1 里的第一个参数,补充下第二个参数 start_response,它是在哪定义并传入的呢?

其实这个无需我们操心,它是由 wsgi server 提供的,如果我们使用的是 wsgiref 库做为 server 的话。那这时的 start_response 就由 wsgiref 提供。

再回到 wsgify,它的作用主要是对 WSGI app 进行封装,简化 wsgi app 的定义与编写,它可以很方便的将一个 callable 的函数或对象,封装成一个 WSGI app。

上面,其实留下了一个问题,self._route(routes 中间件 RoutesMiddleware 对象)是如何找到真正的 application 呢?

带着这个问题,我们了解下 routes 是如何为我们实现第二次路由。

  1. 第二次路由:中间件 routes 路由#

在文章最开始处,我们给大家画了一张图。

null

这张图把一个 HTTP 请求粗略简单地划分为两个过程。但事实上,整个过程远比这个过程要复杂得多。

实际上在 WSGI Server 到 WSGI Application 这个过程中,我们加很多的功能(比如鉴权、URL 路由),而这些功能的实现方式,我们称之为中间件。

中间件,对服务器而言,它是一个应用程序,是一个可调用对象, 有两个参数,返回一个可调用对象。而对应用程序而言,它是一个服务器,为应用程序提供了参数,并且调用了应用程序。

今天以 URL 路由为例,来讲讲中间件在实际生产中是如何起作用的。

当服务器拿到了客户端请求的 URL,不同的 URL 需要交由不同的函数处理,这个功能叫做 URL Routing。

在 Nova 中是用 routes 这个库来实现对 URL 的的路由调度。接下来,我将从源代码处分析一下这个过程。

在 routes 模块里有个中间件,叫routes.middleware.RoutesMiddleware,它将接受到的 url,自动调用map.match()方法,对 url 进行路由匹配,并将匹配的结果存入 request 请求的环境变量['wsgiorg.routing_args'],最后会调用self._dispatch(dispatch 返回真正的 application)返回 response,最后会将这个 response 返回给 WSGI Server。

null

这个中间件的原理,看起来是挺简单的。并没有很复杂的逻辑。

但是,我在阅读 routes 代码的时候,却发现了另一个令我困惑的点。

self._dispatch(也就上图中的 self.app)函数里,我们看到了 app,controller 这几个很重要的字眼,其是否是我苦苦追寻的 application 对象呢?

null

要搞明白这个问题,只要看清 match 到是什么东西?

这个 match 对象 是在RoutesMiddleware.__call__()里塞进req.environ的,它是什么东西呢,我将其打印出来。

{'action': u'detail', 'controller': <nova.api.openstack.wsgi.ResourceV21 object at 0x667bad0>, 'project_id': u'2ac17c7c792d45eaa764c30bac37fad9'}

{'action': u'index', 'controller': <nova.api.openstack.wsgi.ResourceV21 object at 0x6ec8910>, 'project_id': u'2ac17c7c792d45eaa764c30bac37fad9'}

{'action': u'show', 'controller': <nova.api.openstack.wsgi.ResourceV21 object at 0x6ed9710>, 'project_id': u'2ac17c7c792d45eaa764c30bac37fad9', 'id': u'68323d9c-ebe5-499a-92e9-32fea900a892'} 

结果令人在失所望呀,这个 app 并不是我们要寻找的 Controller 对象。而是 nova.api.openstack.wsgi.ResourceV21 类的实例对象,说白了就是 Resource 对象。

看到这里,我有心态有点要崩了,怎么还没到 Controller?OpenStack 框架的代码绕来绕去的,没有点耐心还真的很难读下去。

既然已经开了头,没办法还得硬着头皮继续读了下去。

终于我发现,在 APIRouter 初始化的时候,它会去注册所有的 Resource,同时将这些 Resource 交由 routes.Mapper 来管理、创建路由映射,所以上面提到的 routes.middleware.RoutesMiddleware 才能根据 url 通过 mapper.match 获取到相应的 Resource。

从 Nova 代码中看出每个 Resource 对应一个 Controller 对象,因为 Controller 对象本身就是对一种资源的操作集合。

null

通过日志的打印,可以发现 nova 管理的 Resource 对象有多么的多而杂

os-server-groups
os-keypairs
os-availability-zone
remote-consoles
os-simple-tenant-usage
os-instance-actions
os-migrations
os-hypervisors
diagnostics
os-agents
images
os-fixed-ips
os-networks
os-security-groups
os-security-groups
os-security-group-rules
flavors
os-floating-ips-bulk
os-console-auth-tokens
os-baremetal-nodes
os-cloudpipe
os-server-external-events
os-instance_usage_audit_log
os-floating-ips
os-security-group-default-rules
os-tenant-networks
os-certificates
os-quota-class-sets
os-floating-ip-pools
os-floating-ip-dns
entries
os-aggregates
os-fping
os-server-password
os-flavor-access
consoles
os-extra_specs
os-interface
os-services
servers
extensions
metadata
metadata
limits
ips
os-cells
versions
tags
migrations
os-hosts
os-virtual-interfaces
os-assisted-volume-snapshots
os-quota-sets
os-volumes
os-volumes_boot
os-volume_attachments
os-snapshots
os-server-groups
os-keypairs
os-availability-zone
remote-consoles
os-simple-tenant-usage
os-instance-actions
os-migrations
os-hypervisors
diagnostics
os-agents
images
os-fixed-ips
os-networks
os-security-groups
os-security-groups
os-security-group-rules
flavors
os-floating-ips-bulk
os-console-auth-tokens
os-baremetal-nodes
os-cloudpipe
os-server-external-events
os-instance_usage_audit_log
os-floating-ips
os-security-group-default-rules
os-tenant-networks
os-certificates
os-quota-class-sets
os-floating-ip-pools
os-floating-ip-dns
entries
os-aggregates
os-fping
os-server-password
os-flavor-access
consoles
os-extra_specs
os-interface
os-services
servers
extensions
metadata
metadata
limits
ips
os-cells
versions
tags
migrations
os-hosts
os-virtual-interfaces
os-assisted-volume-snapshots
os-quota-sets
os-volumes
os-volumes_boot
os-volume_attachments
os-snapshots

你一定很好奇,这路由是如何创建的吧,关键代码就是如下一行。如果你想要了解更多路由的创建过程,可以看一下这篇文章(Python Route 总结),写得不错。

routes.mapper.connect("server",
               "/{project_id}/servers/list_vm_state",
               controller=self.resources['servers'],
               action='list_vm_state',
               conditions={'list_vm_state': 'GET'}) 

历尽了千辛万苦,我终于找到了 Controller 对象,知道了请求发出后,wsgi server 是如何根据 url 找到对应的 Controller(根据 routes.Mapper 路由映射)。

但是很快,你又会问。对于一个资源的操作(action),有很多,比如新增,删除,更新等

不同的操作要执行 Controller 里不同的函数。

如果是新增资源,就调用 create()

如果是删除资源,就调用 delete()

如果是更新资源,就调用 update()

那代码如何怎样知道要执行哪个函数呢?

以 /servers/xxx/action 请求为例,请求调用的函数实际包含在请求的 body 中。

经过 routes.middleware.RoutesMiddleware 的__call__函数解析后,此时即将调用的 Resource 已经确定为哪个模块中的 Controller 所构建的 Resource,而 action 参数为 "action",接下来在 Resource 的__call__函数里面会因为 action=="action" 从而开始解析 body 的内容,找出 Controller 中所对应的方法。

Controller 在构建的过程中会由于 MetaClass 的影响将其所有 action 类型的方法填入一个字典中,key 由每个_action_xxx方法前的@wsgi.action('xxx')装饰函数给出,value 为每个 _action_xxx 方法的名字(从中可以看出规律,在 body 里面请求的方法名前加上 _aciton_ 即为 Controller 中对应调用的方法)。

之后在使用 Controller 构建 Resource 对象的过程中会向 Resource 注册该 Controller 的这个字典中的内容。这样,只需在请求的 body 中给出调用方法的 key,然后就可以找到这个 key 所映射的方法,最后在 Resource 的 __call__ 函数中会调用 Controller 类的这个函数!

其实我在上面我们打印 match 对象时,就已经将对应的函数打印出来了。

这边以 nova show(展示资源为例),来理解一下。

当你调用 nova show [uuid] 命令,novaclient 就会给 nova-api 发送一个 http 的请求

nova show 1c250b15-a346-43c5-9b41-20767ec7c94b 

通过打印得到的 match 对象如下

{'action': u'show', 'controller': <nova.api.openstack.wsgi.ResourceV21 object at 0x667bad0>, 'project_id': u'2ac17c7c792d45eaa764c30bac37fad9'} 

其中 action 就是对应的处理函数,而 controller 就对应的 Resource 对象,project_id 是租户 id(你可以不理会)。

继续看 ResourceV21 类里的__call__函数的代码。

图示地方,会从 environ 里获取中看到获取 action 的具体代码

null

我将这边的 action_args 打印出来

{'action': 'show', 'project_id': '2ac17c7c792d45eaa764c30bac37fad9', 'id': '1c250b15-a346-43c5-9b41-20767ec7c94b'} 

其中 action 还是是函数名,id 是要操作的资源的唯一 id 标识。

__call__的最后,会 调用_process_stack方法

null

在图标处,get_method 会根据 action(函数名) 取得处理函数对象。

meth :<bound method ServersController.show of <nova.api.openstack.compute.servers.ServersController object at 0x7be3750>> 

最后,再执行这个函数,取得 action_result,在_process_stack会对 response 进行初步封装。

null

然后将 response 再返回到 wsgify ,由这个专业的工具函数,进行 response 的最后封装和返回给客户端。

null

至此,一个请求从发出到响应就结束了。


附录:参考文章#

作者: MING - Python 编程时光
原文: https://www.cnblogs.com/wongbingming/p/11002978.html
版权声明: 本文为原博主文章,转载请附上原来博文链接!