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)。例如:
- https://www.ex.com/software/releases/latest.tar.gz
- https://www.ex.com/map/roads/USA/CA/17_mile_drive
- https://www.ex.com/search/cs578
URI 设计原则
- 易读:
- 表达资源的层级关系:
- 表示资源的同级关系:
- /git/block-sha1/sha1.h/compare/e3af72cda056f87e;bd63e61bdf38eb264
- 表达资源的过滤:
统一资源接口
- 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) #
状态应该区分应用状态和资源状态,
- 客户端负责维护应用状态,
- 而服务端维护资源状态。
客户端与服务端的交互必须是无状态的,并在每一次请求中包含处理该请求所需的一切信息。服务端不需要在请求间保留应用状态,只有在接受到实际请求的时候,服务端才会关注应用状态。这种无状态通信原则,使得服务端和中介能够理解独立的请求和响应。在多次请求中,同一客户端也不再需要依赖于同一服务器,方便实现高可扩展和高可用性的服务端。
客户端应用状态在服务端提供的超媒体的指引下发生变迁。服务端通过超媒体告诉客户端当前状态有哪些后续状态可以进入。