Skip to main content

Restful API Tutorial

·586 words·3 mins
WFUing
Author
WFUing
A graduate who loves coding.
Table of Contents

REST 全称是 Representational State Transfer(表现层状态转化),更具体的全称是 Resource Representational State Transfer(资源表现层状态转化),具体可以见 Roy Thomas Fielding 的博士论文 https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm 这一章。

REST 指的是一组架构约束条件和原则:

  • 为设计一个功能强、性能好、适宜通信的 web 应用
  • 如果一个架构符合 REST 的约束条件和原则,我们就称它为 RESTful 结构

Resources
#

核心概念
#

  • 资源(Resources)
  • 表现层(Representation)
  • 状态转化(State Transfer)

资源
#

网络上的一个实体,或者说是网络上的一个具体信息,任何事物,只要有被引用到的必要,它就是一个资源。

  • 一段文本,一张图片,一首歌曲
  • 数据库中的一行数据
  • 一个手机号码,某用户的个人信息
  • 一种服务

资源标识
#

要让一个资源可以被识别,需要有个唯一标识,在Web中这个唯一标识就是URI(Uniform Resource Identifier)。例如:

URI 设计原则

统一资源接口

  • RESTful 架构应该遵循统一接口原则,统一接口包含了一组受限的预定义的操作,不论什么样的资源,都是通过使用相同的接口进行资源的访问。接口应该使用标准的 HTTP 方法如 GET,PUT 和 POST,并遵循这些方法的语义
  • 如果按照HTTP方法的语义来暴露资源,那么接口将会拥有安全性和幂等性的特性
    • GET和HEAD请求是安全的,无论请求多少次,都不改变服务器状态
    • GET、HEAD、PUT和DELETE请求是幂等的,无论对资源操作多少次,结果总是一样的,后面的请求并不会产生比第一次更多的影响

GET
#

获取表示,变更时获取表示(缓存)。安全且幂等。

  • 200: OK,表示已在响应中发出
  • 204: 无内容,资源有空表示
  • 301: Moved Permanently,资源的URI已被更新
  • 303: See Other,其他(如,负载均衡)
  • 304: not modified,资源未更改(缓存)
  • 400: bad request,指代坏请求(如,参数错误)
  • 404: not found,资源不存在
  • 406: not acceptable,服务端不支持所需表示
  • 500: internal server error,通用错误响应
  • 503: Service Unavailable,服务端当前无法处理请求

POST
#

使用服务端管理的(自动产生)的实例号创建资源,或创建子资源,部分更新资源,如果没有被修改,则不过更新资源(乐观锁)。不安全且不幂等。

  • 406: not acceptable,服务端不支持所需表示
  • 409: conflict,通用冲突
  • 412: Precondition Failed,前置条件失败(如执行条件更新时的冲突)
  • 415: unsupported media type,接受到的表示不受支持
  • 500: internal server error,通用错误响应
  • 503: Service Unavailable,服务当前无法处理请求

PUT
#

用客户端管理的实例号创建一个资源,通过替换的方式更新资源,如果未被修改,则更新资源(乐观锁)。不安全但幂等。

  • 200: OK,如果已存在资源被更改
  • 201: created,如果新资源被创建
  • 301: Moved Permanently,资源的URI已更改
  • 303: See Other,其他(如,负载均衡)
  • 400: bad request,指代坏请求
  • 404: not found,资源不存在

DELETE
#

删除资源。不安全但幂等。

  • 200: OK,资源已被删除
  • 301: Moved Permanently,资源的URI已更改
  • 303: See Other,其他,如负载均衡
  • 400: bad request,指代坏请求
  • 404: not found,资源不存在
  • 409: conflict,通用冲突
  • 500: internal server error,通用错误响应
  • 503: Service Unavailable,服务端当前无法处理请求

指导意义
#

统一资源接口要求使用标准的HTTP方法对资源进行操作,所以URI只应该来表示资源的名称,而不应该包括资源的操作。通俗来说,URI不应该使用动作来描述。例如:

  • POST /getUser?id=1 $\rightarrow$ GET /Uset/1
  • GET /newUser $\rightarrow$ POST /User
  • GET /updateUser $\rightarrow$ PUT /User/1
  • GET /deleteUser?id=2 $\rightarrow$ DELETE /User/2

表现 (Representation)
#

“资源"是一种信息实体,它可以有多种外在表现形式。我们把"资源"具体呈现出来的形式,叫做它的"表现层”(Representation)

  • 文本可以用txt格式表现,也可以用HTML格式、XMIL格式、JSON格式表现,甚至可以采用二进制格式
  • 图片可以用JPG格式表现,也可以用PNG格式表示

资源表述
#

URI只代表资源的实体,不代表它的形式。严格地说,有些网址最后的 .html 后缀名是不必要的,因为这个后缀名表示格式,属于 “表现层” 范畴,而URI应该只代表 “资源” 的位置。

资源的表述包括数据和描述数据的元数据,例如,HTTP头 “Content-Type” 就是这样一个元数据属性

客户端可以通过 Accept 头请求一种特定格式的表述,服务端则通过 Content-Type 告诉客户端资源的表述形式

支持的表达

 ~ » http get  https://api.github.com/orgs/github 'Accept: application/json'
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
    "archived_at": null,
    "avatar_url": "https://avatars.githubusercontent.com/u/9919?v=4",
    "blog": "https://github.com/about",
    "company": null,
    "created_at": "2008-05-11T04:37:31Z",
    "description": "How people build software.",
    "email": null,
    "events_url": "https://api.github.com/orgs/github/events",
    "followers": 29993,
    "following": 0,
    "has_organization_projects": true,
    "has_repository_projects": true,
    "hooks_url": "https://api.github.com/orgs/github/hooks",
    "html_url": "https://github.com/github",
    "id": 9919,
    "is_verified": true,
    "issues_url": "https://api.github.com/orgs/github/issues",
    "location": "San Francisco, CA",
    "login": "github",
    "members_url": "https://api.github.com/orgs/github/members{/member}",
    "name": "GitHub",
    "node_id": "MDEyOk9yZ2FuaXphdGlvbjk5MTk=",
    "public_gists": 0,
    "public_members_url": "https://api.github.com/orgs/github/public_members{/member}",
    "public_repos": 477,
    "repos_url": "https://api.github.com/orgs/github/repos",
    "twitter_username": null,
    "type": "Organization",
    "updated_at": "2022-11-29T19:44:55Z",
    "url": "https://api.github.com/orgs/github"
}

不支持的表达

 ~ » http get  https://api.github.com/orgs/github 'Accept: text/xml'
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json; charset=utf-8

{
    "documentation_url": "https://docs.github.com/v3/media",
    "message": "Unsupported 'Accept' header: 'text/xml'. Must accept 'application/json'."
}

资源链接
#

当你浏览Web网页时,从一个连接跳到一个页面,再从另一个连接跳到另外一冬页面,就是利用了超媒体的概念:把一个个把资源链接起来。

同样,我们在表述格式里边加入链接来引导客户端:

  • 在Link头告诉客户端怎么访问下一页和最后一页的记录;
  • 在响应体里用url来链接项目所有者和项目地址
 ~ » http -h  get https://api.github.com/orgs/github/repos
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Link: <https://api.github.com/organizations/9919/repos?page=2>; rel="next", <https://api.github.com/organizations/9919/repos?page=16>; rel="last"

[
  {
    "id": 3222,
    "node_id": "MDEwOlJlcG9zaXRvcnkzMjIy",
    "name": "media",
    "full_name": "github/media",
    "private": false,
    "owner": {
      "login": "github",
      "id": 9919,
      "node_id": "MDEyOk9yZ2FuaXphdGlvbjk5MTk=",
      "avatar_url": "https://avatars.githubusercontent.com/u/9919?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/github",
      "html_url": "https://github.com/github",
      "followers_url": "https://api.github.com/users/github/followers",
      "following_url": "https://api.github.com/users/github/following{/other_user}",
      "gists_url": "https://api.github.com/users/github/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/github/subscriptions",
      "organizations_url": "https://api.github.com/users/github/orgs",
      "repos_url": "https://api.github.com/users/github/repos",
      "events_url": "https://api.github.com/users/github/events{/privacy}",
      "received_events_url": "https://api.github.com/users/github/received_events",
      "type": "Organization",
      "site_admin": false
    },
    "html_url": "https://github.com/github/media",
    "description": "Media files for use in your GitHub integration projects",
    "fork": false,
    "url": "https://api.github.com/repos/github/media",
    "forks_url": "https://api.github.com/repos/github/media/forks",
    "keys_url": "https://api.github.com/repos/github/media/keys{/key_id}",
    "collaborators_url": "https://api.github.com/repos/github/media/collaborators{/collaborator}",
    "teams_url": "https://api.github.com/repos/github/media/teams",
    "hooks_url": "https://api.github.com/repos/github/media/hooks",
    "issue_events_url": "https://api.github.com/repos/github/media/issues/events{/number}",
    "events_url": "https://api.github.com/repos/github/media/events",
    "assignees_url": "https://api.github.com/repos/github/media/assignees{/user}",
    "branches_url": "https://api.github.com/repos/github/media/branches{/branch}",
    "tags_url": "https://api.github.com/repos/github/media/tags",
    "blobs_url": "https://api.github.com/repos/github/media/git/blobs{/sha}",
    "git_tags_url": "https://api.github.com/repos/github/media/git/tags{/sha}",
    "git_refs_url": "https://api.github.com/repos/github/media/git/refs{/sha}",
    "trees_url": "https://api.github.com/repos/github/media/git/trees{/sha}",
    "statuses_url": "https://api.github.com/repos/github/media/statuses/{sha}",
    "languages_url": "https://api.github.com/repos/github/media/languages",
    "stargazers_url": "https://api.github.com/repos/github/media/stargazers",
    "contributors_url": "https://api.github.com/repos/github/media/contributors",
    "subscribers_url": "https://api.github.com/repos/github/media/subscribers",
    "subscription_url": "https://api.github.com/repos/github/media/subscription",
    "commits_url": "https://api.github.com/repos/github/media/commits{/sha}",
    "git_commits_url": "https://api.github.com/repos/github/media/git/commits{/sha}",
    "comments_url": "https://api.github.com/repos/github/media/comments{/number}",
    "issue_comment_url": "https://api.github.com/repos/github/media/issues/comments{/number}",
    "contents_url": "https://api.github.com/repos/github/media/contents/{+path}",
    "compare_url": "https://api.github.com/repos/github/media/compare/{base}...{head}",
    "merges_url": "https://api.github.com/repos/github/media/merges",
    "archive_url": "https://api.github.com/repos/github/media/{archive_format}{/ref}",
    "downloads_url": "https://api.github.com/repos/github/media/downloads",
    "issues_url": "https://api.github.com/repos/github/media/issues{/number}",
    "pulls_url": "https://api.github.com/repos/github/media/pulls{/number}",
    "milestones_url": "https://api.github.com/repos/github/media/milestones{/number}",
    "notifications_url": "https://api.github.com/repos/github/media/notifications{?since,all,participating}",
    "labels_url": "https://api.github.com/repos/github/media/labels{/name}",
    "releases_url": "https://api.github.com/repos/github/media/releases{/id}",
    "deployments_url": "https://api.github.com/repos/github/media/deployments",
    "created_at": "2008-03-09T22:43:49Z",
    "updated_at": "2023-09-23T01:50:37Z",
    "pushed_at": "2015-02-27T17:31:20Z",
    "git_url": "git://github.com/github/media.git",
    "ssh_url": "git@github.com:github/media.git",
    "clone_url": "https://github.com/github/media.git",
    "svn_url": "https://github.com/github/media",
    "homepage": "https://github.com/logos",
    "size": 4484,
    "stargazers_count": 293,
    "watchers_count": 293,
    "language": null,
    "has_issues": false,
    "has_projects": true,
    "has_downloads": true,
    "has_wiki": false,
    "has_pages": false,
    "has_discussions": false,
    "forks_count": 69,
    "mirror_url": null,
    "archived": true,
    "disabled": false,
    "open_issues_count": 0,
    "license": null,
    "allow_forking": true,
    "is_template": false,
    "web_commit_signoff_required": false,
    "topics": [],
    "visibility": "public",
    "forks": 69,
    "open_issues": 0,
    "watchers": 293,
    "default_branch": "master",
    "permissions": {
      "admin": false,
      "maintain": false,
      "push": false,
      "triage": false,
      "pull": true
    }
  },
  ...
]

状态转移(State Transfer)
#

状态应该区分应用状态和资源状态,

  • 客户端负责维护应用状态,
  • 而服务端维护资源状态。

客户端与服务端的交互必须是无状态的,并在每一次请求中包含处理该请求所需的一切信息。服务端不需要在请求间保留应用状态,只有在接受到实际请求的时候,服务端才会关注应用状态。这种无状态通信原则,使得服务端和中介能够理解独立的请求和响应。在多次请求中,同一客户端也不再需要依赖于同一服务器,方便实现高可扩展和高可用性的服务端。

客户端应用状态在服务端提供的超媒体的指引下发生变迁。服务端通过超媒体告诉客户端当前状态有哪些后续状态可以进入。