写点什么

通过 demo 学习 OpenStack 开发 --API 服务 (3)

2016 年 1 月 03 日

编者按:《通过 demo 学习 OpenStack 开发》专栏是刘陈泓的系列文章,专栏通过开发一个 demo 的形式来介绍一些参与 OpenStack 项目开发的必要的基础知识,希望帮助大家入门企业级 Python 项目的开发和 OpenStack 项目的开发。刘陈泓主要关注 OpenStack 的身份认证和计费领域。另外,还对云计算、分布式系统应用和开发感兴趣。

上一篇文章我们了解了一个巨啰嗦的框架:Paste + PasteDeploy + Routes + WebOb。后来,OpenStack 社区的人受不了这么啰嗦的代码了,决定换一个框架,他们最终选中了 Pecan。Pecan 框架相比上一篇文章的啰嗦框架有如下好处:

  • 不用自己写 WSGI application 了
  • 请求路由很容易就可以实现了

总的来说,用上 Pecan 框架以后,很多重复的代码不用写了,开发人员可以专注于业务,也就是实现每个 API 的功能。

Pecan

Pecan 框架的目标是实现一个采用对象分发方式进行 URL 路由的轻量级 Web 框架。它非常专注于自己的目标,它的大部分功能都和 URL 路由以及请求和响应的处理相关,而不去实现模板、安全以及数据库层,这些东西都可以通过其他的库来实现。关于 Pecan 的更多信息,可以查看文档。本文以,OpenStack 的 magnum 项目为例来说明 Pecan 项目在实际中的应用,但是本文不会详细讲解 Pecan 的各个方面,一些细节请读者阅读 Pecan 的文档。

项目中的代码结构

使用 Pecan 框架时,,OpenStack 项目一般会把 API 服务的实现都放在一个 api 目录下,比如 magnum 项目是这样的:

复制代码
➜ ~/openstack/env/p/magnum git:(master) $ tree magnum/api
magnum/api
├── app<span>.py</span>
├── auth<span>.py</span>
├── config<span>.py</span>
├── controllers
│   ├── base<span>.py</span>
│   ├── __init__<span>.py</span>
│   ├── link<span>.py</span>
│   ├── root<span>.py</span>
│   └── v1
│   ├── base<span>.py</span>
│   ├── baymodel<span>.py</span>
│   ├── bay<span>.py</span>
│   ├── certificate<span>.py</span>
│   ├── collection<span>.py</span>
│   ├── container<span>.py</span>
│   ├── __init__<span>.py</span>
│   ├── magnum_services<span>.py</span>
│   ├── node<span>.py</span>
│   ├── pod<span>.py</span>
│   ├── replicationcontroller<span>.py</span>
│   ├── service<span>.py</span>
│   ├── types<span>.py</span>
│   ├── utils<span>.py</span>
│   └── x509keypair<span>.py</span>
├── expose<span>.py</span>
├── hooks<span>.py</span>
├── __init__<span>.py</span>
├── middleware
│   ├── auth_token<span>.py</span>
│   ├── __init__<span>.py</span>
│   └── parsable_error<span>.py</span>
├── servicegroup<span>.py</span>
└── validation<span>.py</span>

你也可以在 Ceilometer 项目中看到类似的结构。介绍一下几个主要的文件,这样你以后看到一个使用 Pecan 的,OpenStack 项目时就会比较容易找到入口。

  • app.py 一般包含了 Pecan 应用的入口,包含应用初始化代码
  • config.py 包含 Pecan 的应用配置,会被 app.py 使用
  • controllers/ 这个目录会包含所有的控制器,也就是 API 具体逻辑的地方
  • controllers/root.py 这个包含根路径对应的控制器
  • controllers/v1/ 这个目录对应 v1 版本的 API 的控制器。如果有多个版本的 API,你一般能看到 v2 等目录。

代码变少了:application 的配置

Pecan 的配置很容易,通过一个 Python 源码式的配置文件就可以完成基本的配置。这个配置的主要目的是指定应用程序的 root,然后用于生成 WSGI application。我们来看 Magnum 项目的例子。Magnum 项目有个 API 服务是用 Pecan 实现的,在 _magnum/api/config.py_ 文件中可以找到这个文件,主要内容如下:

复制代码
app = {
<span>'root'</span>: <span>'magnum.api.controllers.root.RootController'</span>,
<span>'modules'</span>: [<span>'magnum.api'</span>],
<span>'debug'</span>: <span>False</span>,
<span>'hooks'</span>: [
hooks.ContextHook(),
hooks.RPCHook(),
hooks.NoExceptionTracebackHook(),
],
<span>'acl_public_routes'</span>: [
<span>'/'</span>
],
}

上面这个 app 对象就是 Pecan 的配置,每个 Pecan 应用都需要有这么一个名为 app 的配置。app 配置中最主要的就是 root 的值,这个值表示了应用程序的入口,也就是从哪个地方开始解析 HTTP 的根path:/。_hooks_ 对应的配置是一些 Pecan 的 hook,作用类似于 WSGI Middleware。

有了 app 配置后,就可以让 Pecan 生成一个 WSGI application。在 Magnum 项目中,_magnum/api/app.py_ 文件就是生成 WSGI application 的地方,我们来看一下这个的主要内容:

复制代码
<span><span>def</span> <span>get_pecan_config</span><span>()</span>:</span>
filename = api_config.__file__.replace(<span>'.pyc'</span>, <span>'.py'</span>)
<span>return</span> pecan.configuration.conf_from_file(filename)
<span><span>def</span> <span>setup_app</span><span>(config=None)</span>:</span>
<span>if</span> <span>not</span> config:
config = get_pecan_config()
app_conf = dict(config.app)
app = pecan.make_app(
app_conf.pop(<span>'root'</span>),
logging=getattr(config, <span>'logging'</span>, {}),
wrap_app=middleware.ParsableErrorMiddleware,
**app_conf
)
<span>return</span> auth.install(app, CONF, config.app.acl_public_routes)

get_pecan_config()方法读取我们上面提到的 config.py 文件,然后返回一个pecan.configuration.Config对象。setup_app()函数首先调用get_pecan_config()函数获取 application 的配置,然后调用pecan.make_app()函数创建了一个 WSGI application,最后调用了auth.install()函数(也就是magnum.api.auth.install()函数)为刚刚生成的 WSGI application 加上 Keystone 的认证中间件(确保所有的请求都会通过 Keystone 认证)。

到这边为止,一个 Pecan 的 WSGI application 就已经准备好了,只要调用这个setup_app()函数就能获得。至于如何部署这个 WSGI application,请参考 WSGI 简介这篇文章。

从 Magnum 这个实际的例子可以看出,使用了 Pecan 之后,我们不再需要自己写那些冗余的 WSGI application 代码了,直接调用 Pecan 的make_app()函数就能完成这些工作。另外,对于之前使用 PasteDeploy 时用到的很多 WSGI 中间件,可以选择使用 Pecan 的 hooks 机制来实现,也选择使用 WSGI 中间件的方式来实现。在 Magnum 的 API 服务就同时使用了这两种方式。其实,Pecan 还可以和 PasteDeploy 一起使用,Ceilometer 项目就是这么做的,大家可以看看。

确定路由变得容易了:对象分发式的路由

Pecan 不仅缩减了生成 WSGI application 的代码,而且也让开发人员更容易的指定一个 application 的路由。Pecan 采用了一种对象分发风格(object-dispatch style)的路由模式。我们直接通过例子来解释这种路由模式,还是以 Magnum 项目为例。

上面提到了,Magnum 的 API 服务的 root 是 _magnum.api.controllers.root.RootController_。这里的 RootController 的是一个类,我们来看它的代码:

复制代码
<span><span>class</span> <span>RootController</span><span>(rest.RestController)</span>:</span>
_versions = [<span>'v1'</span>]
<span>"""All supported API versions"""</span>
_default_version = <span>'v1'</span>
<span>"""The default API version"""</span>
v1 = v1.Controller()
<span>@expose.expose(Root)</span>
<span><span>def</span> <span>get</span><span>(self)</span>:</span>
<span>return</span> Root.convert()
<span>@pecan.expose()</span>
<span><span>def</span> <span>_route</span><span>(self, args)</span>:</span>
<span>"""Overrides the default routing behavior.
{1}
It redirects the request to the default version of the magnum API
if the version number is not specified in the url.
"""</span>
<span>if</span> args[<span>0</span>] <span>and</span> args[<span>0</span>] <span>not</span> <span>in</span> self._versions:
args = [self._default_version] + args
<span>return</span> super(RootController, self)._route(args)

别看这个类这么长,我来解释一下你就懂了。首先,你可以先忽略掉_route()函数,这个函数是用来覆盖 Pecan 的默认路由实现的,在这里去掉它不妨碍我们理解 Pecan(这里的_route()函数的作用把所有请求重定向到默认的 API 版本去)。去掉_route()和其他的东西后,整个类就变成这么短:

复制代码
<span><span>class</span> <span>RootController</span><span>(rest.RestController)</span>:</span>
v1 = v1.Controller()
<span>@expose.expose(Root)</span>
<span><span>def</span> <span>get</span><span>(self)</span>:</span>
<span>return</span> Root.convert()
  • 首先,你要记住,这个 RootController 对应的是 URL 中根路径,也就是 path 中最左边的 /。
  • RootController 继承自 rest.RestController,是 Pecan 实现的 RESTful 控制器。这里的 get() 函数表示,当访问的是 GET / 时,由该函数处理。get() 函数会返回一个 WSME 对象,表示一个形式化的 HTTP Response,这个下面再讲。get() 函数上面的 expose 装饰器是 Pecan 实现路由控制的一个方式,被 expose 的函数才会被路由处理。
  • 这里的 v1 = v1.Controller() 表示,当访问的是 GET /v1 或者 GET /v1/…时,请求由一个 v1.Controller 实例来处理。

为了加深大家的理解,我们再来看下v1.Controller的实现:

复制代码
<span><span>class</span> <span>Controller</span><span>(rest.RestController)</span>:</span>
<span>"""Version 1 API controller root."""</span>
bays = bay.BaysController()
baymodels = baymodel.BayModelsController()
containers = container.ContainersController()
nodes = node.NodesController()
pods = pod.PodsController()
rcs = rc.ReplicationControllersController()
services = service.ServicesController()
x509keypairs = x509keypair.X509KeyPairController()
certificates = certificate.CertificateController()
<span>@expose.expose(V1)</span>
<span><span>def</span> <span>get</span><span>(self)</span>:</span>
<span>return</span> V1.convert()
...

上面这个 Controller 也是继承自rest.RestController。所以它的 get 函数表示,当访问的是 _GET /v1_ 的时候,要做的处理。然后,它还有很多类属性,这些属性分别表示不同 URL 路径的控制器:

  • /v1/bays 由 bays 处理
  • /v1/baymodels 由 baymodels 处理
  • /v1/containers 由 containers 处理

其他的都是类似的。我们再继续看bay.BaysController的代码:

复制代码
<span><span>class</span> <span>BaysController</span><span>(rest.RestController)</span>:</span>
<span>"""REST controller for Bays."""</span>
<span><span>def</span> <span>__init__</span><span>(self)</span>:</span>
super(BaysController, self).__init__()
_custom_actions = {
<span>'detail'</span>: [<span>'GET'</span>],
}
<span><span>def</span> <span>get_all</span><span>(...)</span>:</span>
<span><span>def</span> <span>detail</span><span>(...)</span>:</span>
<span><span>def</span> <span>get_one</span><span>(...)</span>:</span>
<span><span>def</span> <span>post</span><span>(...)</span>:</span>
<span><span>def</span> <span>patch</span><span>(...)</span>:</span>
<span><span>def</span> <span>delete</span><span>(...)</span>:</span>

这个 controller 中只有函数,没有任何类属性,而且没有实现任何特殊方法,所以 _/v1/bays_ 开头的 URL 处理都在这个 controller 中终结。这个类会处理如下请求:

  • GET /v1/bays
  • GET /v1/bays/{UUID}
  • POST /v1/bays
  • PATCH /v1/bays/{UUID}
  • DELETE /v1/bays/{UUID}
  • GET /v1/bays/detail/{UUID}

看了上面的 3 个 controller 之后,你应该能大概明白 Pecan 是如何对 URL 进行路由的。这种路由方式就是对象分发:根据类属性,包括数据属性和方法属性来决定如何路由一个 HTTP 请求。Pecan 的文档中对请求的路由有专门的描述,要想掌握 Pecan 的路由还是要完整的看一下官方文档。

内置 RESTful 支持

我们上面举例的 controller 都是继承自pecan.rest.RestController,这种 controller 称为RESTful controller,专门用于实现 RESTful API 的,因此在,OpenStack 中使用特别多。Pecan 还支持普通的 controller,称为 Generic controller。Generic controller 继承自 object 对象,默认没有实现对 RESTful 请求的方法。简单的说,RESTful controller 帮我们规定好了get_one()get_all()get()post()等方法对应的 HTTP 请求,而 Generic controller 则没有。关于这两种 controller 的区别,可以看官方文档《Writing RESTful Web Services with Generic Controllers》,有很清楚的示例。

对于RestController 中没有预先定义好的方法,我们可以通过控制器的 _custom_actions属性来指定其能处理的方法。

复制代码
<span><span>class</span> <span>RootController</span><span>(rest.RestController)</span>:</span>
_custom_actions = {
<span>'test'</span>: [<span>'GET'</span>],
}
<span>@expose()</span>
<span><span>def</span> <span>test</span><span>(self)</span>:</span>
<span>return</span> <span>'hello'</span>

上面这个控制器是一个根控制器,指定了 /test 路径支持 GET 方法,效果如下:

复制代码
<span>$ </span>curl <span>http:</span>/<span>/localhost:8080/test</span>
hello%

那么 HTTP 请求和 HTTP 响应呢?

上面讲了这么多,我们都没有说明在 Pecan 中如何处理请求和如何返回响应。这个将在下一章中说明,同时我们会引入一个新的库 WSME。

WSME

Pecan 对请求和响应的处理

在开始提到 WSME 之前,我们先来看下 Pecan 自己对 HTTP 请求和响应的处理。这样你能更好的理解为什么会再引入一个 WSME 库。

Pecan 框架为每个线程维护了单独的请求和响应对象,你可以直接在请求处理函数中访问。_pecan.request_ 和 _pecan.response_ 分别代表当前需要处理的请求和响应对象。你可以直接操作这两个对象,比如指定响应的状态码,就像下面这个例子一样(例子来自官方文档):

复制代码
<span>@pecan.expose()</span>
<span><span>def</span> <span>login</span><span>(self)</span>:</span>
<span>assert</span> pecan.request.path == <span>'/login'</span>
username = pecan.request.POST.get(<span>'username'</span>)
password = pecan.request.POST.get(<span>'password'</span>)
pecan.response.status = <span>403</span>
pecan.response.text = <span>'Bad Login!'</span>

这个例子演示了访问 POST 请求的参数以及返回 403。你也可以重新构造一个pecan.Response对象作为返回值(例子来自官方文档):

复制代码
<span>from</span> pecan <span>import</span> expose, Response
<span><span>class</span> <span>RootController</span><span>(object)</span>:</span>
<span>@expose()</span>
<span><span>def</span> <span>hello</span><span>(self)</span>:</span>
<span>return</span> Response(<span>'Hello, World!'</span>, <span>202</span>)

另外,HTTP 请求的参数也会可以作为控制器方法的参数,还是来看几个官方文档的例子:

复制代码
<span><span>class</span> <span>RootController</span><span>(object)</span>:</span>
<span>@expose()</span>
<span><span>def</span> <span>index</span><span>(self, arg)</span>:</span>
<span>return</span> arg
<span>@expose()</span>
<span><span>def</span> <span>kwargs</span><span>(self, **kwargs)</span>:</span>
<span>return</span> str(kwargs)

这个控制器中的方法直接返回了参数,演示了对 GET 请求参数的处理,效果是这样的:

复制代码
<span>$ </span>curl <span>http:</span>/<span>/localhost:8080/</span>?arg=foo
foo
<span>$ </span>curl <span>http:</span>/<span>/localhost:8080/kwargs</span>?a=<span>1</span>&b=<span>2</span>&c=<span>3</span>
{u<span>'a'</span><span>:</span> u<span>'1'</span>, u<span>'c'</span><span>:</span> u<span>'3'</span>, u<span>'b'</span><span>:</span> u<span>'2'</span>}

有时候,参数也可能是 URL 的一部分,比如最后的一段 path 作为参数,就像下面这样:

复制代码
<span><span>class</span> <span>RootController</span><span>(object)</span>:</span>
<span>@expose()</span>
<span><span>def</span> <span>args</span><span>(self, *args)</span>:</span>
<span>return</span> <span>','</span>.join(args)

效果是这样的:

复制代码
$ curl <span>http</span>://localhost:<span>8080</span>/args/<span>one</span>/<span>two</span>/<span>three</span>
<span>one</span>,<span>two</span>,<span>three</span>

另外,我们还要看一下 POST 方法的参数如何处理(例子来自官方文档):

复制代码
<span><span>class</span> <span>RootController</span><span>(object)</span>:</span>
<span>@expose()</span>
<span><span>def</span> <span>index</span><span>(self, arg)</span>:</span>
<span>return</span> arg

效果如下,就是把 HTTP body 解析成了控制器方法的参数:

复制代码
$ curl -X POST "http://localhost:8080/" -H "Content-Type:
application/x-www-form-urlencoded" -d "arg=foo" foo

返回 JSON 还是 HTML?

如果你不是明确的返回一个 Response 对象,那么 Pecan 中方法的返回内容类型就是由 expose() 装饰器决定的。默认情况下,控制器的方法返回的 content-type 是 HTML。

复制代码
<span><span>class</span> <span>RootController</span><span>(rest.RestController)</span>:</span>
_custom_actions = {
<span>'test'</span>: [<span>'GET'</span>],
}
<span>@expose()</span>
<span><span>def</span> <span>test</span><span>(self)</span>:</span>
<span>return</span> <span>'hello'</span>

效果如下:

复制代码
$ curl -v http://localhost:<span>8080</span>/test
* Hostname was NOT found <span>in</span> DNS cache
* Trying <span>127.0</span><span>.0</span><span>.1</span><span>...</span>
* Connected to localhost (<span>127.0</span><span>.0</span><span>.1</span>) port <span>8080</span> (
> GET /test HTTP/<span>1.1</span>
> User-Agent: curl/<span>7.38</span><span>.0</span>
> Host: localhost:<span>8080</span>
> Accept: */*
>
* HTTP <span>1.0</span>, assume close after body
< HTTP/<span>1.0</span> <span>200</span> OK
< Date: Tue, <span>15</span> Sep <span>2015</span> <span>14</span>:<span>31</span>:<span>28</span> GMT
< Server: WSGIServer/<span>0.1</span> Python/<span>2.7</span><span>.9</span>
< Content-Length: <span>5</span>
< Content-Type: text/html; charset=UTF-<span>8</span>
<
* Closing connection <span>0</span>
hello%

也可以让它返回 JSON:

复制代码
<span><span>class</span> <span>RootController</span><span>(rest.RestController)</span>:</span>
_custom_actions = {
<span>'test'</span>: [<span>'GET'</span>],
}
<span>@expose('json')</span>
<span><span>def</span> <span>test</span><span>(self)</span>:</span>
<span>return</span> <span>'hello'</span>

效果如下:

复制代码
curl -v http://localhost:<span>8080</span>/test
* Hostname was NOT found <span>in</span> DNS cache
* Trying <span>127.0</span><span>.0</span><span>.1</span><span>...</span>
* Connected to localhost (<span>127.0</span><span>.0</span><span>.1</span>) port <span>8080</span> (
> GET /test HTTP/<span>1.1</span>
> User-Agent: curl/<span>7.38</span><span>.0</span>
> Host: localhost:<span>8080</span>
> Accept: */*
>
* HTTP <span>1.0</span>, assume close after body
< HTTP/<span>1.0</span> <span>200</span> OK
< Date: Tue, <span>15</span> Sep <span>2015</span> <span>14</span>:<span>33</span>:<span>27</span> GMT
< Server: WSGIServer/<span>0.1</span> Python/<span>2.7</span><span>.9</span>
< Content-Length: <span>18</span>
< Content-Type: application/json; charset=UTF-<span>8</span>
<
* Closing connection <span>0</span>
{<span>"hello"</span>: <span>"world"</span>}%

甚至,你还可以让一个控制器方法根据 URL path 的来决定是返回 HTML 还是 JSON:

复制代码
<span><span>class</span> <span>RootController</span><span>(rest.RestController)</span>:</span>
_custom_actions = {
<span>'test'</span>: [<span>'GET'</span>],
}
<span>@expose()</span>
<span>@expose('json')</span>
<span><span>def</span> <span>test</span><span>(self)</span>:</span>
<span>return</span> json.dumps({<span>'hello'</span>: <span>'world'</span>})

返回 JSON:

复制代码
$ curl -v http://localhost:<span>8080</span>/test.json
* Hostname was NOT found <span>in</span> DNS cache
* Trying <span>127.0</span><span>.0</span><span>.1</span><span>...</span>
* Connected to localhost (<span>127.0</span><span>.0</span><span>.1</span>) port <span>8080</span> (
> GET /test.json HTTP/<span>1.1</span>
> User-Agent: curl/<span>7.38</span><span>.0</span>
> Host: localhost:<span>8080</span>
> Accept: */*
>
* HTTP <span>1.0</span>, assume close after body
< HTTP/<span>1.0</span> <span>200</span> OK
< Date: Wed, <span>16</span> Sep <span>2015</span> <span>14</span>:<span>26</span>:<span>27</span> GMT
< Server: WSGIServer/<span>0.1</span> Python/<span>2.7</span><span>.9</span>
< Content-Length: <span>24</span>
< Content-Type: application/json; charset=UTF-<span>8</span>
<
* Closing connection <span>0</span>
<span>"{\"hello\": \"world\"}"</span>%

返回 HTML:

复制代码
$ curl -v http://localhost:<span>8080</span>/test.html
* Hostname was NOT found <span>in</span> DNS cache
* Trying <span>127.0</span><span>.0</span><span>.1</span><span>...</span>
* Connected to localhost (<span>127.0</span><span>.0</span><span>.1</span>) port <span>8080</span> (
> GET /test.html HTTP/<span>1.1</span>
> User-Agent: curl/<span>7.38</span><span>.0</span>
> Host: localhost:<span>8080</span>
> Accept: */*
>
* HTTP <span>1.0</span>, assume close after body
< HTTP/<span>1.0</span> <span>200</span> OK
< Date: Wed, <span>16</span> Sep <span>2015</span> <span>14</span>:<span>26</span>:<span>24</span> GMT
< Server: WSGIServer/<span>0.1</span> Python/<span>2.7</span><span>.9</span>
< Content-Length: <span>18</span>
< Content-Type: text/html; charset=UTF-<span>8</span>
<
* Closing connection <span>0</span>
{<span>"hello"</span>: <span>"world"</span>}%

这里要注意一下:

  1. 同一个字符串作为 JSON 返回和作为 HTML 返回是不一样的,仔细看一下 HTTP 响应的内容。
  2. 我们的例子中在 URL 的最后加上了.html 后缀或者.json 后缀,请尝试一下不加后缀的化是返回什么?然后,调换一下两个expose()的顺序再试一下。

从上面的例子可以看出,决定响应类型的主要是传递给expose()函数的参数,我们看下expose()函数的完整声明:

复制代码
<span><span><span>pecan.decorators.expose</span>(<span>template</span>=<span>None,</span>
<span>content_type</span>=<span>'text/html'</span><span>,</span>
<span>generic</span>=<span>False)</span></span></span>
  • template 参数用来指定返回值的模板,如果是’json’就会返回 JSON 内容,这里可以指定一个 HTML 文件,或者指定一个 mako 模板。
  • content_type 指定响应的 content-type,默认值是’text/html’。
  • generic 参数表明该方法是一个“泛型”方法,可以指定多个不同的函数对应同一个路径的不同的 HTTP 方法。

看过参数的解释后,你应该能大概了解expose()函数是如何控制 HTTP 响应的内容和类型的。

用 WSME 来做什么?

上面两节已经说明了 Pecan 可以比较好的处理 HTTP 请求中的参数以及控制 HTTP 返回值。那么为什么我们还需要 WSME 呢?因为 Pecan 在做下面这个事情的时候比较麻烦:请求参数和响应内容的类型检查(英文简称就是 typing)。当然,做是可以做的,不过你需要自己访问 _pecan.request_ 和 _pecan.response_,然后检查指定的值的类型。WSME 就是为解决这个问题而生的,而且适用场景就是 RESTful API。

WSME 简介

WSME 的全称是Web Service Made Easy,是专门用于实现 REST 服务的 typing 库,让你不需要直接操作请求和响应,而且刚好和 Pecan 结合得非常好,所以,OpenStack 的很多项目都使用了 Pecan + WSME 的组合来实现 API(好吧,我看过的项目,用了 Pecan 的都用了 WSME)。WSME 的理念是:在大部分情况下,Web 服务的输入和输出对数据类型的要求都是严格的。所以它就专门解决了这个事情,然后把其他事情都交给其他框架去实现。因此,一般 WSME 都是和其他框架配合使用的,支持 Pecan、Flask 等。WSME 的文档地址

WSME 的使用

用了 WSME 后的好处是什么呢?WSME 会自动帮你检查 HTTP 请求和响应中的数据是否符合预先设定好的要求。WSME 的主要方式是通过装饰器来控制 controller 方法的输入和输出。WSME 中主要使用两个控制器:

  • @signature: 这个装饰器用来描述一个函数的输入和输出。
  • @wsexpose: 这个装饰器包含 @signature 的功能,同时会把函数的路由信息暴露给 Web 框架,效果就像 Pecan 的 expose 装饰器。

这里我们结合 Pecan 来讲解 WSME 的使用。先来看一个原始类型的例子:

复制代码
<span>from</span> wsmeext.pecan <span>import</span> wsexpose
<span><span>class</span> <span>RootController</span><span>(rest.RestController)</span>:</span>
_custom_actions = {
<span>'test'</span>: [<span>'GET'</span>],
}
<span>@wsexpose(int, int)</span>
<span><span>def</span> <span>test</span><span>(self, number)</span>:</span>
<span>return</span> number

如果不提供参数,访问会失败:

复制代码
$ curl http:
{<span>"debuginfo"</span>: <span>null</span>, <span>"faultcode"</span>: <span>"Client"</span>, <span>"faultstring"</span>: <span>"Missing argument: \"number\""</span>}%

如果提供的参数不是整型,访问也会失败:

复制代码
$ curl http://localhost:8080/test\?number\=a
{"debuginfo": null, "faultcode": "Client", "faultstring": "Invalid
input for field/attribute number. Value: 'a'. unable to convert to int"}%

上面这些错误信息都是由 WSME 框架直接返回的,还没有执行到你写的方法。如果请求正确,那么会是这样的:

复制代码
$ curl -v http://localhost:<span>8080</span>/test\?number\=<span>1</span>
* Hostname was NOT found <span>in</span> DNS cache
* Trying <span>127.0</span><span>.0</span><span>.1</span><span>...</span>
* Connected to localhost (<span>127.0</span><span>.0</span><span>.1</span>) port <span>8080</span> (
> GET /test?number=<span>1</span> HTTP/<span>1.1</span>
> User-Agent: curl/<span>7.38</span><span>.0</span>
> Host: localhost:<span>8080</span>
> Accept: */*
>
* HTTP <span>1.0</span>, assume close after body
< HTTP/<span>1.0</span> <span>200</span> OK
< Date: Wed, <span>16</span> Sep <span>2015</span> <span>15</span>:<span>06</span>:<span>35</span> GMT
< Server: WSGIServer/<span>0.1</span> Python/<span>2.7</span><span>.9</span>
< Content-Length: <span>1</span>
< Content-Type: application/json; charset=UTF-<span>8</span>
<
* Closing connection <span>0</span>
<span>1</span>%

请注意返回的 content-type,这里返回 JSON 是因为我们使用的wsexpose设置的返回类型是 XML 和 JSON,并且 JSON 是默认值。上面这个例子就是 WSME 最简单的应用了。

那么现在有下面这些问题需要思考一下:

  • 如果想用 POST 的方式来传递参数,要怎么做呢?提示:要阅读 WSME 中 @signature 装饰器的文档。
  • 如果我希望使用 /test/1 这种方式来传递参数要怎么做呢?提示:要阅读 Pecan 文档中关于路由的部分。
  • WSME 中支持对哪些类型的检查呢?WSME 支持整型、浮点型、字符串、布尔型、日期时间等,甚至还支持用户自定义类型。提示:要阅读 WSME 文档中关于类型的部分。
  • WSME 支持数组类型么?支持。

上面的问题其实也是很多人使用 WSME 的时候经常问的问题。我们将在下一篇文章中使用 Pecan + WSME 来继续开发我们的 demo,并且用代码来回答上面所有的问题。


感谢魏星对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。

2016 年 1 月 03 日 17:263031

评论

发布
暂无评论
发现更多内容

面向对象设计原则--开放关闭原则(OCP)

张荣召

架构师训练营 - 第二周总结

一个节点

极客大学架构师训练营

作业-2020-09-27

芝麻酱

基于 iOS14 系统的游戏卡顿问题解决方案

白开水

typescript 游戏开发 iOS14 游戏卡顿 ios开发

区块链交易所系统开发源码,交易所搭建app

WX13823153201

编程语言的本质

张荣召

架构训练营-week2-作业

于成龙

作业 架构训练营

架构师训练营第二周作业

文智

极客大学架构师训练营

案例分析--反应式编程框架Flower的设计

张荣召

第二周总结

等燕归

第二周 框架学习-作业

刘希文

看动画学算法之:排序-基数排序

程序那些事

算法 数据结构和算法 看动画学算法 算法和数据结构

揭秘开源项目 Apache Pulsar 如何挑战 Kafka

Apache Pulsar

kafka 开源 云原生 Apache Pulsar 消息中间件

2.框架设计-依赖倒置原则,接口隔离原则

博古通今小虾米

架构师训练营第二周学习总结

张荣召

面向对象设计原则----里氏替换原则(LSP)

张荣召

第二周

等燕归

Serverless 简介

木易杨

云计算 Serverless AWS

用家谱链记录家族信息

WX13823153201

使用Spring Cloud Stream玩转RabbitMQ,RocketMQ和Kafka

Barry的异想世界

kafka RocketMQ RabbitMQ 消息队列 spring cloud stream

第二周

scorpion

架构师训练营-第二周作业

一个节点

极客大学架构师训练营

面向对象设计原则----依赖倒置原则(DIP)

张荣召

面向对象设计原则----单一职责原则(SRP)

张荣召

面向对象设计原则----接口分离原则(ISP)

张荣召

TensorFlow 篇 | TensorFlow 2.x 基于 Keras 的多节点分布式训练

Alex

tensorflow keras 分布式训练 AllReduce

依赖倒置原则和接口隔离原则练习

知行合一

家谱链-家谱族谱制作

WX13823153201

通过女朋友来通俗易懂讲解“接口回调”,一不小心就被绿

小松漫步

Java 编程 接口 代码

优化Banner广告收入的7种策略

易观大数据

架构师训练营第二周学习总结

成长者

极客大学架构师训练营

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

通过demo学习OpenStack开发--API服务(3)-InfoQ