COMP9321 – Data Services Engineering- Week 5

Week 5 RESTful Client 这周的内容跟上周有很多是一样的,进阶讲了REST 的设计和REST的客户端。怎么把 API 设计对”到“怎么把 API 用对”的完整思维框架。Assignment2这周发布也是,主要是关于RESTful API的作业

6大约束RESTful

Week4 有提到,这里在做一些额外补充, Client–Server、Uniform Interface、Stateless、Caching、Layered System、Code on demand(可选)。如果前 5 条你都满足,基本可以称自己“RESTful”。这不是教条,而是为了解耦规模化(扩展性、缓存、演进)而制定的一套约束。

Uniform Interface

核心是:资源有统一、可预测的表现形式,并且是“带链接的表现(Hypermedia)”——除了数据,还给出“下一步能干什么”(HATEOAS)。这样 API 能“解释”,开发者不必翻厚文档才能走通流程。

无状态(Stateless)不是“没有会话”,而是区分两类“状态”

  • 资源状态(Resource state):在服务器端统一管理(比如 GTFS 中的 routes.txttrips.txt 数据)。
  • 应用状态(Application state):属于客户端会话流程(比如“已选定 agency、当前分页位置、搜索关键字”),应由客户端随请求一起带上,而不是让服务器替你“记住”。因此每个请求都自描述(包含必要上下文:路径、查询参数、首部、体)。

工程要点:

  • 认证/授权可用 token(放在 Authorization 头里),这不违背 Stateless,因为 token 只是让服务器校验你是谁,不是服务器替你保存应用状态。
  • 不要用服务器端 Session 来记住“当前 agency”,而是让客户端每次调用明确 agency 或者把“当前 agency”写入你自己的用户表后由后端查询,但请求必须可重放并自包含。

Ass2注意的点 :你的“当前使用的 bus agency”可以通过显式切换端点设置(例如 PUT /settings/agency),查询端点在服务端根据用户配置返回对应数据;也可以让所有查询都带 ?agency=GSBC_01。两种都 OK,但都遵守“请求自描述”。

缓存(Caching)设计要“能判定是否该工作”

  • Why:REST 强调“无状态”,请求会“更啰嗦”;为避免过度聊天,必须协同缓存——客户端在请求里带“护栏条款”,让服务端能快速判断“是否有必要干活”。典型首部:If-Modified-Since / If-None-Match(ETag)。
  • 服务端要明确缓存策略Cache-Controlmax-ageno-storemust-revalidate…)、ETag/Last-Modified 的生成与对比逻辑。

Ass2 注意点:

  • 抓取 GTFS 压缩包可以本地缓存:记录上次 ETag/时间戳,再次拉取时用 If-None-Match/If-Modified-Since,收到 304 Not Modified 就不重下、也不重导。
  • 对你自家 API:例如“路线列表”是你数据库里的静态快照(直到下次导入),可以设置 ETag 为一版号(如 agency+导入时间),列表查询返回 Cache-Control: public, max-age=300

Richardson Maturity Model

成熟度模型(RMM)指的是“你有多 REST”

Level 0 使用HTTP作为传输方式

  • 只是将HTTP用作远程交互手段,而不是引入任何web机制,相当于把HTTP应用层协议降级为传输层协议,参数放在body中,以POST方式提交到某个URI上的公开服务端点
  • 例子: 在一个医院挂号系统中,医院会通过某个URI来暴露出该挂号服务端点(Service Endpoint)。然后患者会向该URI发送一个文档作为请求,文档中包含了请求的所有细节

请求 某天医生的出诊信息

HTTP
POST /appointmentService HTTP/1.1
[header Information]
<openSlotRequest date = "2010-01-04" doctor = "mjones" />

响应 传回一个包含了所有信息文档

HTTP
HTTP/1.1 200 OK
[header Information]
<openSlotList>
<slot start = "1400" end = "1450">
<doctor id = "mjones" />
</slot>

有了这些信息,下一步就可以创建一个预约,同样可以通过这样向某一个断点发送一个文档来完成

Leve 1 引入了资源

  • 每一个资源都有对应的标识符和表达,不是将所有请求发送到单个服务器端点,而是和单独的资源进行交互。
  • 例子: 在一个医院的挂号系统中,对指定的医生会有一个对应的资源

请求:某天医生(resources)的出诊信息

HTTP
POST /document/mjones HTTP/1.1
[header information]
<openSlotRequest date = "2010-01-04" />

响应: 包含了各个时间段的就诊信息,这些就诊时间可以作为资源单独处理

HTTP
HTTP/1.1 200 OK
[Header information]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450" />
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650" />
</openSlotList>

有了这些资源后,创建一个预约就是向某个特定的就诊时间发送请求

请求:创建某一时段(Resources)的预约

HTTP
POST /slots/1234 HTTP/1.1
[header information]
<appointmentRequest>
<patient id = "jsmith" />
</appointmentRequest>

响应:成功预约

HTTP
HTTP/1.1 200 OK
[header information]
<appointmentRequest>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450" />
<patient id = "jsmith"/>
</appointment>

Level 2 HTTP动词 HTTP响应码

  • HTTP谓词和资源结合起来就可以准确的表示“对XX(资源)做XX(操作)”,Server端通过HTTP状态码告诉客户端请求执行的结果,减少了服务器和客户端的耦合。
  • 例子:在医院挂号系统中,获取医生的就诊时间信息需要使用GET(安全且幂等操作。

请求: GET医生的就诊时间信息

HTTP
GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1
Host: royalhope.nhs.uk

响应: 同level1

HTTP
HTTP/1.1 200 OK
[header information]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450" />
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650" />
</popenSlotList>

请求: POST创建一个预约同level1

HTTP
POST /slots/1234 HTTP/1.1
[header information]
<appointmentRequest>
<patient id = "jsmith" />
</appointmentRequest>

响应: 返回201 预约成功

HTTP
HTTP/1.1 201 Created
Location: slots/1234/appointment
[header information]
<appointmentRequest>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450" />
<patient id = "jsmith"/>
</appointment>

Level 3 Hypermedia control 超媒体控制

  • HATEOAS是Hypertext As The Engine Of Application State的缩写,是指在资源的表达中包含了链接信息,客户端可以根据链接来发现可以执行的动作

请求: GET医生的就诊时间信息 (同level2)

HTTP
GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1
Host: royalhope.nhs.uk

响应: 每个就诊时间信息现在都包含一个URI, 用来告诉我们如何创建一个预约

Hypermedia control的关键就是在:它告诉我们下一步能够做什么,以及对应相关资源的URI

HTTP
HTTP/1.1 200 OK
[header information]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450" />
<link rel = "/linkrels/slot/book" uri = "/slots/1234" />
</slot>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650" />
<link rel = "/linkrels/slot/book" uri = "/slots/5678" />
</slot>
</openSlotList>

请求: POST创建一个预约(同level1)

HTTP
POST /slots/1234 HTTP/1.1
[header information]
<appointmentRequest>
<patient id = "jsmith" />
</appointmentRequest>

响应:包含了一系列的超媒体控制,用来告我们后面可以进行什么操作。

HTTP
HTTP/1.1 201 Created
Location: slots/1234/appointment
[header information]
<appointmentRequest>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450" />
<patient id = "jsmith"/>
<link rel = "/linkrels/appointment/cancel" uri = "/slots/1234/appointment"/>
<link rel = "/linkrels/appointment/addTest" uri = "/slots/1234/tests"/>
<link rel = "self" uri = "/slots/1234/appointment"/>
<link rel = "/linkrels/appointment/changeTime" uri = "/doctors/mjones/slots?date=20100104@status=open"/>
<link rel = "/linkrels/appointment/updateContactInfo" uri = "/patients/jsmith/contactInfo"/>
<link rel = "/linkrels/help" uri = "/help/appointment"/>
</appointment>

Ass2注意点 Level 0:一个大入口;Level 1:资源 URI;Level 2:正确用 HTTP 动词与状态码语义化资源+动词+状态码);Level 3:HATEOAS(响应里有“下一步”链接)。目标是至少到 Level 2,力所能及往 Level 3 靠。

RESTful 应用场景

Resource(资源):一切对外暴露的东西都用 URI 标识,比如 /orders/1234/payments/1234

Representation(表现层):资源用 JSON/XML 表示(图里用 XML),请求/响应里来回传。

Stateless(无状态):服务器记你是谁、对话进行到哪一步;每个请求都自带完成所需的信息(token、body、Accept/Content-Type 等)。

Uniform Interface(统一接口):少量 HTTP 动词完成大部分事:GET/POST/PUT/PATCH/DELETE/OPTIONS/HEAD

HATEOAS(Hypermedia as the Engine of Application State):响应里给下一步可做的链接<link rel="payment" href="...">),客户端“跟着链接走”。

  • 顾客工作流Place Order → Paid → Drink Received,中途可 Update(比如加一份 Espresso)。
  • 咖啡师工作流Lookup Next Order → Drink Made → Take Payment → Drink Released
  • 每个状态变迁 = 对某个 URI 用某个 HTTP 方法

下单

  • 动作POST /orders
  • 为什么用 POST:创建新的订单资源(服务器分配 ID)。
  • 期望响应201 Created + Location: /order/1234 告诉新资源的地址(URI)。 + 表现体里带 links(响应体里给出后续可操作的关系链接HATEOAS),付款的链接。)。

我还能改单吗?

  • 动作OPTIONS /order/1234
  • 意义:询问服务器该资源当前允许的动词Allow: GET, PUTOPTIONSAllow header
    • 如果还可以 PUT,说明订单仍处于“可修改”状态(尚未制作/出杯/结束)。

更新订单

动作PUT /order/1234(把订单更新为“加一份 shot”)

  • PUT = Idempotent(幂等):同样的 PUT 重试 10 次,最终状态仍然一致,不会“越更越多”。
  • 与 PATCH 的区别:PUT 通常是整体替换(或显式“设为”某状态),PATCH 是部分修改

更新成功返回 200 OK(或无体时 204 No Content),并可在体内继续返回 links(例如付款)。

并发冲突

咖啡师在修改订单之前把饮品做出来了,会有两种情况

  • 场景:你在 PUT 之前,咖啡师可能已经把饮品做了,资源状态先一步改变。
  • 结果:服务器返回 409 Conflict,提示你当前变更与资源现状冲突。
  • 另一种结果:去option OPTIONS /order/1234 看现在还能干啥(只允许 GET),然后 GET 拉取最新状态,跟着链接走(可能进入付款或取餐)。

使用 ETag / If-Match(Optimistic Concurrency, 乐观锁)

  1. GET /order/1234 → 响应头 ETag: "abc123"
  2. PUT /order/1234 + 头 If-Match: "abc123"
  3. 若期间资源变了,返回 412 Precondition Failed,避免“丢失更新”。

订单更新成功

  • 动作GET /order/1234
  • 作用:拿到最新表示(representation),里面含有 rel="payment" 的链接。
  • 结果Follow the links(沿着超媒体链接推进状态),而不是在客户端硬编码流程。

付款

  • 动作PUT /payment/order/1234
  • 为什么用PUT幂等——网络抖动可能会导致客户重新付款,所以用PUT不会多扣款(在真实系统里再配合幂等键 idempotency key)。
  • 期望响应201 Created(或 200/204 取决于你把付款建模为“创建一个 payment 子资源”还是“设置支付状态已完成”)。
  • 很多系统会把付款单独建模成 /orders/1234/payment/payments/{paymentId}
  • 无论路径如何,关键是让重试安全idempotent)。

确认支付状态

  • 动作GET /payment/order/1234
  • 期望响应200 OK + 支付详情(cardNo、name、amount ……)。
  • 下一步:HATEOAS 可再给出“可取餐”的链接或当前队列位置。

REST 客户端

一个合格的客户端开发流程是:读文档→识别对象→拼地址→处理动作(过滤、编辑、删除)

URI Design

  • 用名词复数/routes/stops/{id};关系用层级或查询表达:/routes/{id}/trips vs /trips?routeId=...,以“预期返回的资源”来选方案。
    • • Walmart /items /items/{id}
    • • LInkedIn /people /people/{id}
    • • BBC /programmes /programmes/{id}
  • 查询字符串用于过滤/选择/routes?short_name=333&direction=outbound
    • • /orders?date=2015-04-15
    • • /customers?state=NSW&status=gold
    • • Twitter /friendships/lookup
    • • Walmart /search?query
  • 资源关系的表达
    • • Facebook /me/photos
    • • Walmart /items/{id}/reviews
    • • /customers/123/orders vs. /orders?customer=123
  • 状态码要一致GET 200POST 201(+Location)PUT 200/204/201DELETE 200/204,不要乱用 400 一把梭;404403401 各有场景。
  • 响应格式:单个对象返回对象,多个返回集合容器(建议统一 { items: [...], total, _links }),并支持内容协商(至少 JSON)

具体的到实例可以长下面这样子

  • GET /coffeeOrders/123
  • POST /coffeeOrders
  • PUT /coffeeOrders/123
  • PATCH /coffeeOrders/123
  • DELETE /coffeeOrders/123

Consuming a REST API

The client in this situation is pre-written software program 客户端是预先编写好的程序

两类客户端形态:

  • 传统 Web 应用(浏览器拿 HTML):整页渲染为主;
  • REST 客户端(程序拿 JSON/XML):通过 HTTP 动词直接与 API 交互,可嵌入到应用中作为“数据层”。

应用里常常内置一个 REST client去调用外部服务;交互是纯 HTTP、stateless的。

SPA(Single Page Application)场景:前端只刷新局部 UI、与 API 频繁交换 JSON,提高交互性;而“人机交互”主要发生在 UI,不在 API。

How to write Web REST API Client

首先就是阅读API 文档 / OpenAPI(=Swagger) 了解

  • 对象模型(返回 JSON 里有哪些字段/链接)
  • 地址构造(资源的 URI 规则、查询参数)
  • 可做的动作(过滤、创建、更新、删除)

核心任务就是

  • Recognising the Objects in HTTP responses
  • Constructing Addresses (URLs) for interacting with the service
  • Handling Actions such as filtering, editing or deleting data

Client-API interaction pattern

循环模式(very basic client pattern)

发送 HTTP 请求 → 2) 把响应(JSON)暂存 → 3) 解析出当前上下文(要显示什么、下一步能做什么) → 4) 渲染 UI(含动作按钮);然后根据用户操作或链接再走下一轮。

Python requests

Requests will allow you to send HTTP/1.1 requests using Python. It also allows you to access the response data of Python in the same way.

json= 参数会自动 json.dumps() 并设置 Content-Type: application/json

适合自动化脚本/服务对服务(machine-to-machine)

Python
import requests

resp = requests.get('https://todolist.example.com/tasks/')
if resp.status_code != 200:
  # This means something went wrong
  raise ApiError('GET /task/ {}'.format(resp.status_code))
for todo_item in resp.json():
  print('{}{}'.format(todo_item['id'], todo_item['summary']))
  
"""
the json argument to post. If we use that, requests will do the following for
us:
Convert that into a JSON representation string, json.dumps()
Set the requests’ content type to "application/json" (by adding an HTTP
header).
"""
task = {"summary": "Take out trash", "description": ""}
resp = request.post('https://todlist.example.com/task/', json=task)
if resp.status_code != 200:
  raise ApiError('POST /tasks/ {}'.format(resp.status_code))
print('Created task. ID: {}'.format(resp.json()['id']))

正确校验状态码(Status Code Check)

  • GET 成功通常 200
  • POST(创建) 期望 201 Created(并常带 Location);
  • PUT/PATCH/DELETE 成功常用 200/204
  • 失败分 4xx(客户端问题)5xx(服务端问题),响应体应包含可读错误→ 客户端要按“操作语义”检查返回码,否则容易把错误当成功用

Caching API Requests

通过导入import requests_cache上,减少重复请求、加快体验;也支持 dict/redis/mongodb。适合读多写少、且服务端响应变化没那么频繁的资源。

By default the cache is saved in a sqlite database. 默认情况下缓存是存在SQlite数据库中的

Python
import requests_cache

requests_cache.install_cache(cache_name=‘mystuff_cache', backend='sqlite', expire_after=180)

封装为“自己的 API 库”

把“单次调用”升级为“稳定可复用的库”,既方便团队,也让外部调用者容易使用你的 API

  • 当项目复杂时,不要在业务代码到处散落 requests.get/post;请封装成专用 SDK/库,统一:
    • base URL / 认证 / 超时 / 重试 / 错误映射 / 日志 / 追踪 id
    • 公共能力(分页、批量、幂等键、重放保护)
Python
import requests, time

class Api:
    def __init__(self, base, token=None, timeout=10):
        self.base, self.timeout = base.rstrip('/'), timeout
        self.session = requests.Session()
        self.session.headers.update({'Accept':'application/json'})
        if token: self.session.headers['Authorization'] = f'Bearer {token}'

    def _url(self, path): return f'{self.base}{path}'

    def _req(self, method, path, **kw):
        for attempt in range(3):                      # 轻量重试(可加指数退避)
            r = self.session.request(method, self._url(path), timeout=self.timeout, **kw)
            if r.status_code >= 500 and attempt < 2:
                time.sleep(0.5 * (2**attempt)); continue
            return r
        return r

    def list_items(self, page=1):
        r = self._req('GET', f'/items?page={page}')
        r.raise_for_status(); return r.json()

    def create_item(self, title, idempotency_key=None):
        headers = {}
        if idempotency_key: headers['Idempotency-Key'] = idempotency_key
        r = self._req('POST', '/items', json={'title':title}, headers=headers)
        if r.status_code != 201: raise RuntimeError(r.text)
        return r.json(), r.headers.get('Location')

MUST IGNORE 原则

MUST IGNORE:客户端必须忽略响应中不认识的字段,而不是报错或崩溃。这是 HTTP/HTML 演进的通用策略,也适用于自己的客户端。

V1 响应含 links/data/actions;V2 新增 extensions。旧客户端应该跳过新字段继续处理,以实现向前兼容;如果要用新能力,升级客户端即可。

Back to top arrow

评论

发表回复

目录