REST
REST 简介
Roy Fielding论文:Representational State Transfer (REST)
Roy Fielding 在其博士论文中介绍了Web 服务的REST 架构方式,并列出了6 个符合这一架构定义的特征。
- 客户端−服务器:客户端和服务器之间必须有明确的界线。
- 无状态:客户端发出的请求中必须包含所有必要的信息。服务器不能在两次请求之间保存客户端的任何状态。
- 缓存:服务器发出的响应可以标记为可缓存或不可缓存,这样出于优化目的,客户端(或客户端和服务器之间的中间服务)可以使用缓存。
- 接口统一:客户端访问服务器资源时使用的协议必须一致,定义良好,且已经标准化。REST Web服务最常使用的统一接口是HTTP 协议。
- 系统分层:在客户端和服务器之间可以按需插入代理服务器、缓存或网关,以提高性能、稳定性和伸缩性。
- 按需代码:客户端可以选择从服务器上下载代码,在客户端的环境中执行。
资源就是一切
资源是REST 架构方式的核心概念。在REST 架构中,资源是程序中你要着重关注的事物。例如,在博客程序中,用户、博客文章和评论都是资源。
每个资源都要使用唯一的URL 表示。还是以博客程序为例,一篇博客文章可以使用URL /api/posts/12345 表示,其中12345 是这篇文章的唯一标识符,使用文章在数据库中的主键表示。URL 的格式或内容无关紧要,只要资源的URL 只表示唯一的一个资源即可。
某一类资源的集合也要有一个URL。博客文章集合的URL 可以是/api/posts/,评论集合的URL 可以是/api/comments/。
API 还可以为某一类资源的逻辑子集定义集合URL。例如,编号为12345 的博客文章,其中的所有评论可以使用URL /api/posts/12345/comments/ 表示。表示资源集合的URL 习惯在末端加上一个斜线,代表一种“文件夹”结构。
请求方法
客户端程序在建立起的资源URL 上发送请求,使用请求方法表示期望的操作。若要从博客API 中获取现有博客文章的列表,客户端可以向http://www.exam-ple.com/api/posts/ 发送GET 请求。若要插入一篇新博客文章,客户端可以向同一地址发送POST 请求,而且请求主体中要包含博客文章的内容。若要获取编号为12345 的博客文章,客户端可以向http://www.example.com/api/posts/12345 发送GET 请求。
REST架构API中使用的HTTP请求方法
请求方法 | 目标 | 说明 | HTTP状态码 |
---|---|---|---|
GET | 单个资源的URL | 获取目标资源 | 200 |
GET | 资源集合的URL | 获取资源的集合(如果服务器实现了分页,就是一页中的资源) | 200 |
POST | 资源集合的URL | 创建新资源,并将其加入目标集合。服务器为新资源指派URL,并在响应的Location 首部中返回 | 201 |
PUT | 单个资源的URL | 修改一个现有资源。如果客户端能为资源指派URL,还可用来创建新资源 | 200 |
DELETE | 单个资源的URL | 删除一个资源 | 200 |
DELETE | 资源集合的URL | 删除目标集合中的所有资源 | 200 |
REST 架构不要求必须为一个资源实现所有的请求方法。如果资源不支持客户端使用的请求方法,响应的状态码为405,返回“不允许使用的方法”。Flask 会自动处理这种错误。
请求和响应主体
在请求和响应的主体中,资源在客户端和服务器之间来回传送,但REST 没有指定编码资源的方式。请求和响应中的Content-Type 首部用于指明主体中资源的编码方式。使用HTTP 协议中的内容协商机制,可以找到一种客户端和服务器都支持的编码方式。
REST Web 服务常用的两种编码方式是JavaScript 对象表示法(JavaScript Object Notation,JSON)和可扩展标记语言(Extensible Markup Language,XML)。对基于Web 的RIA 来说,JSON 更具吸引力,因为JSON 和JavaScript 联系紧密,而JavaScript 是Web 浏览器使用的客户端脚本语言。继续以博客API 为例,一篇博客文章对应的资源可以使用如下的JSON 表示:
{
"url": "http://www.example.com/api/posts/12345",
"title": "Writing RESTful APIs in Python",
"author": "http://www.example.com/api/users/2",
"body": "... text of the article here ...",
"comments": "http://www.example.com/api/posts/12345/comments"
}
注意,在这篇博客文章中,url、author 和comments 字段都是完整的资源URL。这是很重要的表示方法,因为客户端可以通过这些URL 发掘新资源。
在设计良好的REST API 中,客户端只需知道几个顶级资源的URL,其他资源的URL 则从响应中包含的链接上发掘。这就好比浏览网络时,你在自己知道的网页中点击链接发掘新网页。
版本
在传统的以服务器为中心的Web 程序中,服务器完全掌控程序。更新程序时,只需在服务器上部署新版本就可更新所有的用户,因为运行在用户Web 浏览器中的那部分程序也是从服务器上下载的。
但升级RIA 和Web 服务要复杂得多,因为客户端程序和服务器上的程序是独立开发的,有时甚至由不同的人进行开发。你可以考虑一下这种情况,即一个程序的REST Web 服务被很多客户端使用,其中包括Web 浏览器和智能手机原生应用。服务器可以随时更新Web 浏览器中的客户端,但无法强制更新智能手机中的应用,更新前先要获得机主的许可。即便机主想进行更新,也不能保证新版应用上传到所有应用商店的时机都完全吻合新服务器端版本的部署。
基于以上原因,Web 服务的容错能力要比一般的Web 程序强,而且还要保证旧版客户端能继续使用。这一问题的常见解决办法是使用版本区分Web 服务所处理的URL。例如,首次发布的博客Web 服务可以通过/api/v1.0/posts/提供博客文章的集合。
在URL 中加入Web 服务的版本有助于条理化管理新旧功能,让服务器能为新客户端提供新功能,同时继续支持旧版客户端。博客服务可能会修改博客文章使用的JSON 格式,同时通过/api/v1.1/posts/ 提供修改后的博客文章,而客户端仍能通过/api/v1.0/posts/ 获取旧的JSON 格式。在一段时间内,服务器要同时处理v1.1 和v1.0 这两个版本的URL。
提供多版本支持会增加服务器的维护负担,但在某些情况下,这是不破坏现有部署且能让程序不断发展的唯一方式。
使用Flask提供REST Web服务
使用Flask 创建REST Web 服务很简单。使用熟悉的route() 修饰器及其methods 可选参数可以声明服务所提供资源URL 的路由。处理JSON 数据同样简单,因为请求中包含的JSON 数据可通过request.json 这个Python 字典获取,并且需要包含JSON 的响应可以使用Flask 提供的辅助函数jsonify() 从Python 字典中生成。
创建API蓝本
REST API 相关的路由是一个自成一体的程序子集,所以为了更好地组织代码,我们最好把这些路由放到独立的蓝本中。这个程序API 蓝本的基本结构如示例14-1 所示。
示例1 API 蓝本的结构
|-flasky
|-app/
|-api_1_0
|-__init__.py
|-users.py
|-posts.py
|-comments.py
|-authentication.py
|-errors.py
|-decorators.py
注意,API 包的名字中有一个版本号。如果需要创建一个向前兼容的API 版本,可以添加一个版本号不同的包,让程序同时支持两个版本的API。
在这个API 蓝本中,各资源分别在不同的模块中实现。蓝本中还包含处理认证、错误以及提供自定义修饰器的模块。蓝本的构造文件如示例2 所示。
示例2 app/api_1_0/__init__.py
:API 蓝本的构造文件
from flask import Blueprint
api = Blueprint('api', __name__)
from . import authentication, posts, users, comments, errors
注册API 蓝本的代码如示例3 所示。
示例3 app/__init__.py
:注册API 蓝本
def create_app(config_name):
# ...
from .api_1_0 import api as api_1_0_blueprint
app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0')
# ...
错误处理
REST Web 服务将请求的状态告知客户端时,会在响应中发送适当的HTTP 状态码,并将额外信息放入响应主体。客户端能从Web 服务得到的常见状态码如表2 所示。
表2 API返回的常见HTTP状态码
HTTP状态码 | 名称 | 说明 |
---|---|---|
200 | OK(成功) | 请求成功完成 |
201 | Created(已创建) | 请求成功完成并创建了一个新资源 |
400 | Bad request(坏请求) | 请求不可用或不一致 |
401 | Unauthorized(未授权) | 请求未包含认证信息 |
403 | Forbidden(禁止) | 请求中发送的认证密令无权访问目标 |
404 | Notfound(未找到) | URL 对应的资源不存在 |
405 | Method not allowed(不允许使用的方法) | 指定资源不支持请求使用的方法 |
500 | Internal server error(内部服务器错误) | 处理请求的过程中发生意外错误 |
处理404 和500 状态码时会有点小麻烦,因为这两个错误是由Flask 自己生成的,而且一般会返回HTML 响应,这很可能会让API 客户端困惑。
为所有客户端生成适当响应的一种方法是,在错误处理程序中根据客户端请求的格式改写响应,这种技术称为内容协商。示例4 是改进后的404 错误处理程序,它向Web 服务客户端发送JSON 格式响应,除此之外都发送HTML 格式响应。500 错误处理程序的写法类似。
示例4 app/main/errors.py
:使用HTTP 内容协商处理错误
@main.app_errorhandler(404)
def page_not_found(e):
if request.accept_mimetypes.accept_json and
not request.accept_mimetypes.accept_html:
response = jsonify({'error': 'not found'})
response.status_code = 404
return response
return render_template('404.html'), 404
这个新版错误处理程序检查Accept 请求首部(Werkzeug 将其解码为request.accept_mimetypes),根据首部的值决定客户端期望接收的响应格式。浏览器一般不限制响应的格式,所以只为接受JSON 格式而不接受HTML 格式的客户端生成JSON 格式响应。
其他状态码都由Web 服务生成,因此可在蓝本的errors.py 模块作为辅助函数实现。示例5 是403 错误的处理程序,其他错误处理程序的写法类似。
示例5 app/api_1_0/errors.py
:API 蓝本中403 状态码的错误处理程序
def forbidden(message):
response = jsonify({'error': 'forbidden', 'message': message})
response.status_code = 403
return response
构建 API 的一种简易方式 —— MethodView
Flask
为 RESTful APIs
提供了一种简易的实现方式,可以针对不同的HTTP方法提供不同的函数。使用也很简单,只需要继承 flask.views.MethodView
然后重写需要的方法:GET POST PUT
等。
下面是官方给的程序示例:
from flask.views import MethodView
class UserAPI(MethodView):
def get(self):
users = User.query.all()
...
def post(self):
user = User.from_form_data(request.form)
...
app.add_url_rule('/users/', view_func=UserAPI.as_view('users'))
最下面的 add_url_rule
是添加路由,和用装饰器的效果一样:
@app.route('/users/')
def users(page):
users = User.query.all()
return render_template('users.html', users=users)
因为现在是视图类而不是视图函数,对类进行装饰器操作没有意义,如果想对视图函数添加装饰器,比如用户登陆检测,可以对 .as_view()
返回的函数进行装饰:
def user_required(f):
"""Checks whether user is logged in or raises error 401."""
def decorator(*args, **kwargs):
if not g.user:
abort(401)
return f(*args, **kwargs)
return decorator
view = user_required(UserAPI.as_view('users'))
app.add_url_rule('/users/', view_func=view)
如何应对多种请求方法,比如用户信息管理:
URL | Method | Description |
---|---|---|
/users/ | GET | Gives a list of all users |
/users/ | POST | Creates a new user |
/users/id | GET | Shows a single user |
/users/id | PUT | Updates a single user |
/users/id | DELETE | Deletes a single user |
官方给的实用小例子:
class UserAPI(MethodView):
def get(self, user_id):
if user_id is None:
# return a list of users
pass
else:
# expose a single user
pass
def post(self):
# create a new user
pass
def delete(self, user_id):
# delete a single user
pass
def put(self, user_id):
# update a single user
pass
user_view = UserAPI.as_view('user_api')
app.add_url_rule('/users/', defaults={'user_id': None},
view_func=user_view, methods=['GET',])
app.add_url_rule('/users/', view_func=user_view, methods=['POST',])
app.add_url_rule('/users/<int:user_id>', view_func=user_view,
methods=['GET', 'PUT', 'DELETE'])