用 Flask 框架写 RESTful API

本文先介绍一些 RESTful 理念 ,并通过一个 KVS 服务 演示 RESTful API 的行为。最后以 KVS 服务实作 为例,介绍如何使用 Flask 框架编写 RESTful API

RESTful 理念

RESTRepresentational State Transfer 的缩写,中文翻译是 表现层状态转换 。这种软件构建风格,通过基于 HTTP 之上的一组 约束属性 ,提供万维网网络服务。其主要特点包括:

  • 统一接口 ( Uniform Interface ),资源通过一致、可预见的 URI 及请求方法来操作;
  • 无状态 ( Stateless ),请求状态由客户端维护,服务端不做保存;
  • 可缓存 ( Cacheable ),可以通过缓存提升服务性能;
  • 分层系统 ( Layered System )

统一接口RESTful 服务最大的特点。统一接口的核心思想是,将服务抽象成各种 资源 ,并通过一套一致、可预见的 URI 以及 请求方法 ( Request Method )来操作这些资源。这样一来,掌握了一种资源的使用方法,便可延伸到其他资源上,达到举一反三的效果。

资源可以是 单个资源 ,也可以是同种类资源组成的集合,即 资源组 。资源、资源组以及对应的操作定义如下:

表格-1 RESTful 统一接口
资源GETPUTPOSTDELETE
一组资源,URI形如:https://example.com/resources/列出每个资源及其详细信息(可选)以给定资源组替换当前资源组(较少用)创建资源并追加到资源组删除整个资源组(较少用)
单个资源,URI形如:https://example.com/resources/142获取指定资源的详细信息替换或创建指定资源创建资源并作为子资源添加到当前资源组删除指定的资源

注意到,创建资源有两种不同的方式,即:

  • 针对资源组的 POST 操作;
  • 针对资源 PUT 操作;

这两种操作是有区别的,主要体现在 幂等性 上。

针对资源组的 POST 操作,数据键(或 ID )一般由服务端分配。因此,同个请求执行两次将创建两个相同的资源。换句话讲,这个操作一般 不具有幂等性

针对资源 PUT 操作,由于客户端显式指定数据键,幂等性是可以保证的。实际上,这个操作在资源已经存在的情况下替换原有资源,在资源还未存在的情况下创建资源。

最后,我们还可以将 RESTful 与传统的 增删改查 一一对应起来:

表格-2 RESTful 增删改查
操作对象URI请求方法
创建资源( )资源组/resourcesPOST
删除资源( )资源/resources/142DELETE
更新资源( )资源/resources/142PUT
查询资源( )资源/resources/142GET
列举资源( )资源组/resourcesGET

KVS 服务

为了演示 RESTful 服务接口的特性,我们特地实现了一个简单的服务—— KVS 服务。

该服务只提供一种资源,名为 kv 。根据资源名, kv 资源组的 URI/kvskv 资源的 URI/kvs/<key>key 是资源的键。

进入源码目录 python/restful/flask/kvs ,可以看到两个文件:

  1. $ cd python/restful/flask/kvs
  2. $ ls
  3. kvs.py requirements.txt

其中, kvs.py 是源码文件,requirements.txtPython 依赖。

如果你的 Python 执行环境还未安装 Flask 框架,请使用 pip 命令安装:

  1. $ pip install -r requirements.txt

依赖安装完毕后,就可以启动 KVS 服务了:

  1. $ FLASK_APP=kvs.py flask run
  2. * Serving Flask app "kvs.py"
  3. * Environment: production
  4. WARNING: Do not use the development server in a production environment.
  5. Use a production WSGI server instead.
  6. * Debug mode: off
  7. * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

服务默认监听本地 5000 端口: http://127.0.0.1:5000/

可以给 flask 指定监听地址和端口:

  1. $ FLASK_APP=kvs.py flask run --host 0.0.0.0 --port 80

服务启动后,资源池是空的,不信发起搜索请求看看:

  1. $ curl http://127.0.0.1:5000/kvs
  2. {"data":[],"meta":{"count":0,"skip":0,"total":0},"result":true}

注意到,返回的数据中, data 字段是一个空的列表。换句话讲,资源池中没有任何可用的键。

接着,我们随便用一个键(如 something )向服务器检索数据。毫不意外,服务器返回一个错误,告诉我们资源不存在:

  1. $ curl http://127.0.0.1:5000/kvs/something
  2. {"message":"resource not exists","result":false}

好吧,那我们创建一个资源呗:

  1. $ curl -X POST \
  2. -H 'Content-Type: application/json' \
  3. -d '{"key": "guangzhou", "name": "广州", "population": 14498400}' \
  4. http://127.0.0.1:5000/kvs
  5. {"data":{"key":"guangzhou","name":"广州","population":14498400},"result":true}

我们新增了一条广州的人口数据,数据键为 guangzhou 。注意到,我们使用 POST 方法,并通过 Content-Type 头部指定数据类型为 json

我们再接再厉,添加杭州的数据:

  1. $ curl -X POST \
  2. -H 'Content-Type: application/json' \
  3. -d '{"key": "hangzhou", "name": "杭州", "population": 946800}' \
  4. http://127.0.0.1:5000/kvs
  5. {"data":{"key":"hangzhou","name":"杭州","population":946800},"result":true}

艾玛呀,人口少了个零咋整?——更新呗:

  1. $ curl -X PUT \
  2. -H 'Content-Type: application/json' \
  3. -d '{"population": 9468000}' \
  4. http://127.0.0.1:5000/kvs/hangzhou
  5. {"data":{"key":"hangzhou","name":"杭州","population":9468000},"result":true}

注意到,这里我们采用 PUT 方法,数据只包括需要修正的人口字段( population )。

好了,现在在资源池可以查询到这两个数据记录了:

  1. $ curl http://127.0.0.1:5000/kvs
  2. {"data":[{"key":"guangzhou","name":"广州","population":14498400},{"key":"hangzhou","name":"杭州","population":9468000}],"meta":{"count":2,"skip":0,"total":2},"result":true}

根据数据键,我们可以查询出对应的数据记录:

  1. $ curl http://localhost:5000/kvs/guangzhou
  2. {"data":{"key":"guangzhou","name":"广州","population":14498400},"result":true}

数据记录不断增长,当存储资源不足时,服务将返回一个错误:

  1. $ curl -X POST \
  2. -H 'Content-Type: application/json' \
  3. -d '{"key": "suzhou", "name": "苏州", "population": 10684000}' \
  4. http://127.0.0.1:5000/kvs
  5. {"message":"out of resources","result":false}

KVS 服务实作

KVS 是一个非常简单的服务,对于有一些编程基础的童鞋,理解起来应该毫无难度。完整源码在这查看: kvs.py

创建资源需在资源组 URI ( /kvs )之上实现 POST 方法:

/_src/python/restful/flask/kvs/kvs.py

  1. @app.route('/kvs', methods=['POST'])
  2. def create():
  3. '''
  4. '''
  5. # 资源不足,返回错误
  6. if len(KVS) >= SIZE_LIMIT:
  7. return jsonify({
  8. 'result': False,
  9. 'message': 'out of resources',
  10. })
  11. # 获取请求数据
  12. result, data = get_request_data()
  13. # 请求数据错误
  14. if not result:
  15. return jsonify({
  16. 'result': False,
  17. 'message': 'bad request data',
  18. })
  19. # 取出数据键
  20. key = data.get('key')
  21. if not key:
  22. return jsonify({
  23. 'result': False,
  24. 'message': 'data key missing',
  25. })
  26. # 插入资源池并判断状态
  27. result = KVS.setdefault(key, data) is data
  28. # 资源已存在
  29. if not result:
  30. return jsonify({
  31. 'result': False,
  32. 'message': 'resource exists',
  33. })
  34. return jsonify({
  35. 'result': result,
  36. 'data': data,
  37. })

创建资源的处理逻辑如下:

  • 检查资源使用情况,在资源不足时返回错误;
  • 获取请求数据,即反序列化请求数据;
  • 检查数据,当数据不符合要求时下返回错误;
  • 检查资源,在资源已存在时返回错误;
  • 创建资源并加入资源组; 注意到,我们通过在请求数据中包含数据键( key 字段 )来保证该操作的幂等性。

获取请求数据 的逻辑在其他接口也会用到,因此需要对其进行封装,以便代码复用。为此,我们实现了一个名为 get_request_data 的函数:

/_src/python/restful/flask/kvs/kvs.py

  1. def get_request_data():
  2. '''
  3. 获取请求数据
  4. 根据请求数据类型反序列化
  5. '''
  6. data = None
  7. # 类型
  8. content_type = request.headers['Content-Type']
  9. try:
  10. # json
  11. if content_type == 'application/json':
  12. data = json.loads(request.data)
  13. except Exception as exc:
  14. print(exc)
  15. return isinstance(data, dict), data

该函数先检查 Content-Type头部 ,然后根据头部指定数据类型,对 请求体 ( Request Body )进行 反序列化

删除资源需要为资源 URI 实现 DELETE 方法。资源 URI 形如 /kvs/<key> ,其中 kvs 是资源名, <key> 是用于唯一标识一个资源的键(或者 ID )。

/_src/python/restful/flask/kvs/kvs.py

  1. @app.route('/kvs/<key>', methods=['DELETE'])
  2. def delete(key):
  3. '''
  4. '''
  5. # 删除资源
  6. data = KVS.pop(key, None)
  7. # 资源不存在
  8. if data is None:
  9. return jsonify({
  10. 'result': False,
  11. 'message': 'resource not exists',
  12. })
  13. return jsonify({
  14. 'result': True,
  15. 'data': data,
  16. })

删除资源前,先检查资源是否存在。当资源不存在时,直接返回错误;当资源存在时,删除该资源并将其返回。

更新资源需要为资源 URI 实现 PUT 方法:

/_src/python/restful/flask/kvs/kvs.py

  1. @app.route('/kvs/<key>', methods=['PUT'])
  2. def update(key):
  3. '''
  4. '''
  5. # 获取请求数据
  6. result, changes = get_request_data()
  7. # 请求数据错误
  8. if not result:
  9. return jsonify({
  10. 'result': False,
  11. 'message': 'bad request data',
  12. })
  13. # 获取资源
  14. data = KVS.get(key)
  15. # 资源不存在
  16. if data is None:
  17. return jsonify({
  18. 'result': False,
  19. 'message': 'resource not exists',
  20. })
  21. # 更新资源
  22. data.update(changes)
  23. return jsonify({
  24. 'result': True,
  25. 'data': data,
  26. })

更新方式由两种, 部分更新 以及 整体替换 ,该例子实现的是前者。

查询操作需要为资源 URI 实现 GET 方法:

/_src/python/restful/flask/kvs/kvs.py

  1. @app.route('/kvs/<key>')
  2. def retrieve(key):
  3. '''
  4. '''
  5. # 获取资源
  6. data = KVS.get(key)
  7. # 资源不存在
  8. if data is None:
  9. return jsonify({
  10. 'result': False,
  11. 'message': 'resource not exists',
  12. })
  13. return jsonify({
  14. 'result': True,
  15. 'data': data,
  16. })

列举给定资源组内的资源,需要为资源组 URI 实现 GET 方法:

/_src/python/restful/flask/kvs/kvs.py

  1. @app.route('/kvs')
  2. def search():
  3. '''
  4. '''
  5. skip = int(request.args.get('skip', 0))
  6. limit = int(request.args.get('limit', 10))
  7. data = list(KVS.values())[skip:skip+limit]
  8. return jsonify({
  9. 'result': True,
  10. 'data': data,
  11. 'meta': {
  12. 'total': len(KVS),
  13. 'skip': skip,
  14. 'count': len(data),
  15. },
  16. })

可以通过 URI 参数指定列举条件,则只返回符合条件的资源。此外,还可以为列举操作实现分页逻辑,这在资源数很多的情况下非常有用。

注意到,我们在返回数据中,用一个名为 meta 字段存放分页信息。分页信息包括三部分:总资源数( total )、当前页资源数( count )以及当前页起点( skip )。

下一步

订阅更新,获取更多学习资料,请关注我们的 微信公众号

../_images/wechat-mp-qrcode.png 小菜学编程

参考文献