微服务实战协议篇之REST

作者: 老瓦在霸都 | 来源:发表于2016-11-29 00:03 被阅读2517次

    概述

    微服务所使用的协议自然要根据服务的特点和类型来选择

    微服务类型 推荐协议 推荐理由
    Web Service Restful via HTTP 简单实用, 应用广泛
    VoIP 及 Telephony Service 信令用SIP, 媒体用RTP 支持的终端和媒体网关众多
    多媒体流服务 Multimedia Stream Service RTP/SRTP/RTSP 基于传输延迟考虑
    实时消息服务 Realtime Message Service XMPP, PDU via TCP XMPP 是开源的标准协议, 效率不高,手机应用不推荐
    异步消息服务 Async Message Service JMS/AMQP ActiveMQ 用 JMS, RabbitMQ 用后者

    这里说的协议主要是指应用层协议, 传输层协议一般都是TCP, 除非是媒体传输考虑用低延迟的 UDP

    简单来说, 一般的信令控制协议用基于 HTTP 的 REST 协议就够了, 或者是基于 TCP /WebSocket 的用 Protobuf 来封装应用层消息体也不错.

    SIP/SDP/MGCP 在电话及语音服务领域应用较广

    媒体传输一般用 RTPSRTPRTSP 来承载音频或视频, 在多方会议共享及远程控制应用中也常用如下协议

    REST

    先从应用最广的 REST 说起, REST (Representational State Transfer) 可表�现的状态迁移, 是2000年由 Roy Fielding 在他的关于REST的博士论文中提出的.

    REST准确来说不算是一种协议, 而是一种设计分布式系统的架构风格, 它是指资源在网络中以某种表现形式进行状态转移.

    也就是说它是面向资源的, 每种资源都有相对应的URI, 每个URI 都指向一个资源, 而资源是可展现的(Representational ) 和有状态的(state), 而HTTP 请求则是无状态的, 即它不需要依赖其它的请求, 每个请求都是相对独立的, 超媒体 Hypermedia 可以通过 链接Link 和 URI 把资源连接起来, Web成功的秘诀也就是用链接把世界连接起来.

    这里主要指用 HTTP 和 Json 承载的面向资源的 Restful 风格的协议.
    由于HTTP协议比较简单, 系统对外的接口被分为多个资源 API, 都可以独立地进行测试, 并且符合无状态通信的原则, 天然具有比较好的松耦合性和可伸缩性.

    在介绍完它的特性之后, 我们就会明白它为什么会在分布式系统中大受欢迎

    REST 的特点

    • REpresentational State Transfer 可表现的状态迁移
    • Nouns, not verbs, in endpoints 在各端点中资源是名词而非动词
    • All state the client needs is queryable 客户端所需的所有状态是可查询到的
    • Server has a complete picture of system state 服务端具有完整的系统状态
    • Particularly useful for intermittently-connected clients 对间断性连接的客户端特别有用

    REST 的好处

    • 简单
      HTTP + Json 地球人都知道,HTTP method 表示对于资源的 CRUD 简单明了

    • 可伸缩
      短连接,无状态,易于横向扩展

    • 松耦合
      基于 URL 和 API 的协作,保持接口简单,一致和稳定,避免产生复杂的网状结构和闭环,耦合自然没那么紧

    REST 的风格

    1. 客户-服务器(Client-Server)
      通信只能由客户端单方面发起,表现为请求-响应的形式。

    2. 无状态(Stateless)
      通信的会话状态(Session State)应该全部由客户端负责维护。

    3. 缓存(Cache)
      响应内容可以在通信链的某处被缓存,以改善网络效率。

    4. 统一接口(Uniform Interface)
      通信链的组件之间通过统一的接口相互通信,以提高交互的可见性。

    5. 分层系统(Layered System)
      通过限制组件的行为(即,每个组件只能“看到”与其交互的紧邻层),将架构分解为若干等级的层。

    6. 按需代码(Code-On-Demand,可选)
      支持通过下载并执行一些代码(例如Java Applet、Flash或JavaScript),对客户端的功能进行扩展。

    REST 的特性

    • 面向资源 Resource Oriented
      要考虑合适的粒度, 可缓存性, 修改频率和可变性
    • 可寻址 Addressability
    • 连通性 Connectedness
    • 无状态 Statelessness
    • 统一接口 Uniform Interface
      POST, GET, PUT, DELETE , PATCH, HEAD, OPTIONS, TRACE, Connect
    • 超文本驱动 Hypertext Driven

    REST 的原则

    • 它基于无状态, 客户端-服务器, 可缓存的通讯协议
    • 资源以易于理解的目录结构的URI 来公布
    • 以JSON或XML形式传输来表示数据对象和属性。
    • 消息明确地使用了 HTTP 方法(例如,GET,POST,PUT和DELETE)。
    • 在HTTP请求与请求之间的无状态交互不在服务器上存储客户端上下文。
      状态依赖性限制了可扩展性, 所以在客户端存储会话状态使得横向扩展更加容易

    用 HTTP 方法来表示 CRUD

    格式为 [HTTP Method] https://host/{service}/{apiclass}/v{version}

    | HTTP �方法 | 含义 | 幂等吗? |
    |---|---|
    | POST | �创建资源 Create | N |
    | GET | �获取或查询资源 Retrieve | Y |
    | PUT | �全部替换资源 Update | Y |
    | DELETE | 删除资源 Delete | Y |
    | PATCH | 部分修改资源 | N |
    | HEAD | 类似于 GET, 但是只传输状态行和 HTTP 头 | Y |
    | OPTIONS | 描述目标资源的通信选项 | Y |
    | TRACE | 执行沿目标资源路径的消息环回测试。 | Y |
    | CONNECT | 建立到由给定URI标识的服务器的隧道 | Y |

    所谓幂等性 Idempotence, 它的意思是你调用一次和调用多次的效果是一样的

    简单列举一下一些在 REST 中常用的 Http header

    常见的 Http 头域

    | Header name| Header value example | 备注 |
    |---|---|---|---|
    | Accept | application/json | Respond 406 not acceptable if not support the format |
    | Content-Type | application/json
    | If-Modified-Since/If-None-Match | | Respond 304 not modified if the data is not changed |
    | If-Match/ETag | |412 precondition failed if the ETag is not matched |
    | ETag | |The version of the resource for integrity |
    | Location | | 201 response contains it contains the URI of the new created resource |

    常见的 Http �状态码

    2xx

    • 200 OK with Etag head
    • 201 Created with Location head
    • 204 No content
    • 206 Partial content

    3xx

    • 301 Move Permanently
    • 302 Found
    • 304 Not Modified

    4xx

    • 401 Unauthorized with WWW-Authorizate head
    • 403 Forbidden
    • 404 Not Found
    • 405 Not Allowed with Allow head
    • 406 Not Acceptable
    • 409 Conflict
    • 410 Gone
    • 412 Precondition Failed
    • 413 Request Entity Too Large
    • 415 Unsupported Media Type
    • 451 Unavailable For Legal Reasons

    5xx

    • 500 Internal Server Error
    • 501 Not Implemented
    • 502 Bad Gateway
    • 503 Service Unavailable with Retry-After head
    • 504 Gateway Timeout

    URI 设计

    REST 是面向资源的, 如何定位和寻找资源呢, 就象找人一样, 资源也需要象人那样的身份证号码 URI

    URI

    在设计资源URI 的时候,

    • 一是要注意它们是名词,
    • 二是要注意区分单复数
    • 三是要注意 URI 有长度限制, 建议小于1k
    • 四是要注意在 URI 中不要放未经加密的敏感信息, 即使使用TLS/HTTPS

    我们可以用

    常用方法

    缓存控制

    我们可以利用一些 HTTP Header 来控制资源的缓存以及防止并发问题

    • ETag 实体标签: 一般为资源实体的哈希值
    • Expires 过期的时间: Expires: Thu, 01 Feb 2015 17:00:00 GMT
    • Cache-Control 可以有如下属性
      • public 公有缓存
      • private 私有缓存
      • no-cache 不可缓存
      • max-age 缓存的最大时间, 单位为秒, 一般来说 max-age是相对时间, 比 Expires 的绝对时间要好, 不会有客户端和服务器时间误差的问题, 优先使用它
    • Age 缓存了多少秒
    • Last-Modified 资源的最后修改时间
    • If-None-Match 如果不匹配的话
    • If-Modified-Since 从何时起资源有更新
    • If-Match 如果匹配的话
    • if-Unmodified-Since 从何时起资源无更新

    当服务器发现Http请求的 Header 中有 If-None-Match, 就取出它的值, 与缓存中的资源的Etag 比较, 如果一致的话, 返回 304 Not Modified, 节省从数据库查询和网络传输成本

    当服务器发现Http请求的 Header 中 If-Modified-Since, 就取出它的值, 与缓存中的资源的Last-Modified 比较, 如果 If-Modified-Since中指示的最后修改时间大于或等于资源的Last-Modified时间的话, 也返回 304 Not Modified, 即它是从资源最后一次修改之后获取的, 最近无更改, 无需重新查询

    当然, 如果不一致的话, 则得重新查询数据库并刷新缓存, 返回最新的资源信息, 状态为 200 OK

    并发控制

    如果多个请求对资源进行修改, 会出现丢失修改或者无效删除的情况

    试想, 张三和李四都是公司的会计, 张三管考勤, 发现王二上个月迟到了三次, 要扣王二三百元钱, 李四管绩效, 要给王二增加一千元奖金, 假设王二工资为八千元.

    张三修改王二工资为 8000 - 300 = 7700

         update payroll  set salary=7700 where username="wang2" and salary=8000
    

    李四修改王二工资为 8000 + 1000 = 9000

         update payroll  set salary=9000 where username="wang2" and salary=8000
    

    强一致性的关系数据库使用行级锁, 张三和李四只有一个会成功, 另一个会修改失败, 返回给其中一个用户412错误, 让用户重新修改. 从而使王二的最终薪水为8000-300+1000=8700

    一些不支持强事务的NOSQL存储, 特别是一些KV系统只能根据key - username来修改数据, 就极有可能出现张三和李四都返回成功, 王二工资变成了7700或9000, 而不是正确的8700, 这时候我们就可以用下面的方法来减少这种情况的发生.

    • 更新数据

    当服务器发现Http Header 中有 If-Match, 就取出它的值, 与当前资源的Etag 比较, 如果一致的话, 修改数据返回200 OK, 否则返回 412 Precondition Failed

    当服务器发现Http Header 中有if-Unmodified-Since, 就取出它的值, 与当前资源的 Last-Modified 进行比较, 如果发现if-Unmodified-Since值大于或等于Last-Modified资源的的话, 修改数据返回200 OK, 否则返回 412 Precondition Failed

    • 删除数据

    当服务器发现Http Header 中有 if-Match, 就取出它的值, 与当前资源的Etag 比较, 如果一致的话,删除数据返回 204 No Content, 否则返回 412 Precondition Failed

    当服务器发现Http Header 中有 if-Unmodified-Since, 就取出它的值, 与当前资源的 Last-Modified 进行比较, 如果发现if-Unmodified-Since值大于或等于Last-Modified资源的的话, 修改数据返回 204 No Content, 否则返回 412 Precondition Failed

    批量处理

    例如我们想一次提交多个请求, 可以用这种方法

    Request

    POST /api/v1/batch
    {
            "requests": [
              {
                "method": "POST",
                "path": "/phonenumbers",
               “headers”: [ {“Content-Type”: “application/json”}]
                "body": {
                           "number": "86-01012345678",
                           "type": "mobile"
                            }
              },
              {
                "method": "POST",
                "path": "/telephonydomains/$telephonyDomainID/dialnumbers",
                 "body": {
                           "number": "86-01022345678",
                           "type": "office"
                            }
              }
            ]
    }
    
    

    Response

    HTTP/1.1 200 OK
    {
    “response” [
    {
    “status”: 200,
    “message”: “OK”
     “headers”: [ {“Content-Type”: “application/json”}]
    “body”: {}
    },
    {
    “status”: 412,
    “message”: “Preconditionl Failed”
     “body”: {}
    }
    ]
    }
    
    

    查询条件超长或者查询参数有敏感信息

    用 POST 来代替 GET , 意谓创建一个查询

    Request:

    POST /accounts/queries
    {
    “userIds”: [111, 222, 333]
    }
    

    Response:

    HTTP/1.1200 OK
    [
    …accounts …
    ]
    

    异步请求

    与同步请求不同的是, 不是立即返回结果, 而是先给一个 taskId, 可供稍后查询结果, 或者在请求时给一个回调URL, 稍后把结果通知回去

    Request

    POST https://abc.cde.com/api/v1.0/migrations HTTP 1.1
    {
       pool: "china",
       notifyUri: 'https://abc.cde.com/api/v1/migrations/123'
    }
    

    Response

    {
        "status": 'pending',
        "taskID": 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
    }
    

    实例: 帐号管理的微服务

    光说不练假把式, 先拿python 来写一个微服务原型, 我们平常会使用诸多网站, 帐号密码经常忘记, 所以让我们花一点时间写一个帐号管理的微服务, 基本功能是记录我们常用的帐号和密码, 以免遗忘, 一切从简, 不用id, 而是用sitename 作为主键

    method description
    GET /accounts 帐户列表
    GET /accounts/<siteName> 帐户获取
    POST /accounts 帐户创建
    PUT /accounts/<siteName> 帐户修改
    DELETE /accounts/<siteName> 帐户�删除
    • 客户端用 httpie 来作测试
    • 服务器端用 python flask 框架来实现
    • 页面的UI 暂且省略

    先安装python 和 virtualenv

    brew install python
    brew install pyenv-virtualenv
    
    or
    sudo pip install virtualenv
    
    

    再运行 virtual env

    virtualenv venv
    source venv/bin/activate
    

    再安装所需的类库

    pip install flask
    pip install flask-httpauth
    pip install requests
    pip install httpie
    

    为简单起见, 用 json 文件代替数据库: account.json

    {
    "jianshu":{
      "userName":  "walterfan",
      "password": "password",
      "siteName": "jianshu",
      "siteUrl": "http://www.jianshu.com/users/e0b365801f48"
    },
    
    "weibo":{
      "userName":  "fanyamin",
      "password": "password",
      "siteName": "weibo",
      "siteUrl": "http://weibo.com/fanyamin"
    }
    }
    
    

    源码如下, 不算空行, 100行之内搞定: account.py, 可读写json file, 并对其中的记录进行增删改查, 暂不考虑性能和其他异常及并发处理, 差强人意, 仅供演示, 个人日常使用也行

    import os
    import json
    import requests
    from flask_httpauth import HTTPBasicAuth
    from flask import make_response
    from flask import Flask
    from flask import request
    from werkzeug.exceptions import NotFound, ServiceUnavailable
    
    app = Flask(__name__)
    
    current_path = os.path.dirname(os.path.realpath(__file__))
    
    auth = HTTPBasicAuth()
    
    users = {
        "walter": "pass1"
    }
    
    json_file = "{}/account.json".format(current_path)
    
    def read_data():
        json_fp = open(json_file, "r")
        return json.load(json_fp)
    
    def save_data(accounts):
        json_fp = open(json_file, "w")
        json.dump(accounts, json_fp, sort_keys = True, indent = 4)
    
    @auth.get_password
    def get_pw(username):
        if username in users:
            return users.get(username)
        return None
    
    def generate_response(arg, response_code=200):
        response = make_response(json.dumps(arg, sort_keys = True, indent=4))
        response.headers['Content-type'] = "application/json"
        response.status_code = response_code
        return response
    
    @app.route("/", methods=['GET'])
    @auth.login_required
    def index():
        return generate_response({
            "username": auth.username(),
            "description": "account micro service /accounts"
        })
    
    @auth.login_required
    @app.route("/accounts", methods=['GET'])
    def list_account():
        accounts = read_data()
        return generate_response(accounts)
    
    #Create account
    @auth.login_required
    @app.route('/accounts', methods=['POST'])
    def create_account():
        account = request.json
        sitename = account["siteName"]
        accounts = read_data()
        if sitename in accounts:
            return generate_response({"error": "conflict"}, 409)
        accounts[sitename] = account
        save_data(accounts)
        return generate_response(account)
    
    #Retrieve account
    @auth.login_required
    @app.route('/accounts/<sitename>', methods=['GET'])
    def retrieve_account(sitename):
        accounts = read_data()
        if sitename not in accounts:
            return generate_response({"error": "not found"}, 404)
    
        return generate_response(accounts[sitename])
    
    #Update account
    @auth.login_required
    @app.route('/accounts/<sitename>', methods=['PUT'])
    def update_account(sitename):
        accounts = read_data()
        if sitename not in accounts:
            return generate_response({"error": "not found"}, 404)
    
        account = request.json
        print(account)
        accounts[sitename] = account
        save_data(accounts)
        return generate_response(account)
    
    #Delete account
    @auth.login_required
    @app.route('/accounts/<sitename>', methods=['DELETE'])
    def delete_account(sitename):
        accounts = read_data()
        if sitename not in accounts:
            return generate_response({"error": "not found"}, 404)
    
        del(accounts[sitename])
        save_data(accounts)
        return generate_response("", 204)
    
    if __name__ == "__main__":
        app.run(port=5000, debug=True)
    

    直接运行 python account.py 这个帐户管理的RESTful 微服务就启动了, 用 httpie 测试一下

    • list accounts
    (venv) $ http --json --auth walter:pass GET http://localhost:5000/accounts
    HTTP/1.0 200 OK
    Content-Length: 347
    Content-type: application/json
    Date: Sat, 10 Dec 2016 15:43:53 GMT
    Server: Werkzeug/0.11.11 Python/3.5.1
    
    {
        "jianshu": {
            "password": "password",
            "siteName": "jianshu",
            "siteUrl": "http://www.jianshu.com/users/e0b365801f48",
            "userName": "walterfan"
        },
        "weibo": {
            "password": "password",
            "siteName": "weibo",
            "siteUrl": "http://weibo.com/fanyamin",
            "userName": "fanyamin"
        }
    }
    
    • create account
    http --auth walter:pass --json POST http://localhost:5000/accounts userName=walter password=pass siteName=163 siteUrl=http://163.com
    HTTP/1.0 200 OK
    Content-Length: 108
    Content-type: application/json
    Date: Sat, 10 Dec 2016 15:48:59 GMT
    Server: Werkzeug/0.11.11 Python/3.5.1
    
    {
        "password": "pass",
        "siteName": "163",
        "siteUrl": "http://163.com",
        "userName": "walter"
    }
    
    • retrieve account
    http --auth walter:pass --json GET http://localhost:5000/accounts/163
    HTTP/1.0 200 OK
    Content-Length: 108
    Content-type: application/json
    Date: Sat, 10 Dec 2016 15:49:21 GMT
    Server: Werkzeug/0.11.11 Python/3.5.1
    
    {
        "password": "pass",
        "siteName": "163",
        "siteUrl": "http://163.com",
        "userName": "walter"
    }
    
    • update account
    http --auth walter:pass --json PUT http://localhost:5000/accounts/163 userName=walter password=pass123 siteName=163 siteUrl=http://163.com
    HTTP/1.0 200 OK
    Content-Length: 111
    Content-type: application/json
    Date: Sat, 10 Dec 2016 15:49:47 GMT
    Server: Werkzeug/0.11.11 Python/3.5.1
    
    {
        "password": "pass123",
        "siteName": "163",
        "siteUrl": "http://163.com",
        "userName": "walter"
    }
    
    • delete account
    http --auth walter:pass --json DELETE http://localhost:5000/accounts/163
    HTTP/1.0 204 NO CONTENT
    Content-Length: 0
    Content-type: application/json
    Date: Sat, 10 Dec 2016 15:50:18 GMT
    Server: Werkzeug/0.11.11 Python/3.5.
    

    参考文档与链接

    相关文章

      网友评论

        本文标题:微服务实战协议篇之REST

        本文链接:https://www.haomeiwen.com/subject/rwqapttx.html