衔接上文[理解REST] 05 Web的需求 & 推导REST,上文根据Web的需求推导出了REST架构风格,以及REST的详细描述和解释。自从1994年以来,REST架构风格被用于指导Web架构的设计和开发工作,最重要的两点体现在设计HTTP和URI两个互联网规范协议的过程中,以及实现这些规范的libwww-perl客户端库,Apache HTTP项目(httpd)以及其他的实现中,所得到的经验以及教训。
# 1 Web标准化 {#standardizing-the-web}
本系列在一开始就提到,创造REST的目的就是为Web创建一个架构模型,用它来指导Web的架构设计以及相关的协议规范的开发。用REST来描述Web所期待的架构,识别出现的问题,对各种方案进行对比,并且保证新的协议不会违反那些**促使Web成功的核心约束**。这部分工作使IETF和W3C来负责的,它们定义了HTTP,URI,HTML这三个核心的规范。一开始这三个规范都使由IETF来负责的,后来Web之父Berners-Lee创建了W3C,使其作为Web架构的智库,并为Web编写规范以及实现相关所需的资源,随后HTML就由W3C来专职负责了(关于这部分的历史缘由就不解释了,感兴趣的朋友自行了解吧)。
得益于Fielding博士在Web开发方面的经验,IETF让他来创作URL规范,后来又和Henrik Frystyk Nielsen合作创作了HTTP/1,0,后来Fielding博士成了HTTP/1.1的主要架构师,并且最终创作了URI通用语法标准的URL规范的修订版。
REST的第一版诞生于1994年10到1995年8月之间,起初使Fielding作为编写HTTP/1.0的一种概念方法。在随后的5年中不断的迭代改进,并且用于各种Web协议标准的修行版和扩展之中。最初REST被称作 `HTTP对象模型`,这个名字很容易被误解为它是一个HTTP服务器的实现模型。**而REST(表述性状态移交)这个词使有意唤起人们对于一个设计良好的Web应用如何运转的印象 : Web应用使一个由网页组成的网络(一个虚拟状态机),用户通过选择链接(状态迁移)在应用中前进,引导系统把下一个页面(代表应用的下一个状态)的数据移交给用户,并且呈现出来,以便用户使用**。**其中承载包含应用状态的部分是由超媒体来负责的,这也是为什么REST强调HATEOAS(Hypermedia As The Engine Of Application Statue)的原因所在**。
REST并未想要捕获到Web协议规定的所有可能的使用方法,现实中仍然会存在一些于REST不匹配的Web应用存在。但是REST捕获到了Web作为一个分布式超媒体系统中最重要的方面,然后对这方面进行优化,使得Web可以满足最核心的这部分需求。
# 2 把REST应用于URI {#rest-applied-to-uri}
URI既是Web中最简单的元素,也是最重要的元素。其中URL和URN是常见的两种形式。URI的语法自从1992年以来都相对稳定。URI中也定义了**资源**的概念以及其语义,但是这个概念以及发生了很大的变化。**REST用来定义URI中"资源"这个术语,以及定义通过它们的表述操作资源的通用接口的全部语义**。
## 2.1 重新定义资源 {#redefinition-of-resource}
早期Web把URI定义为文档的标识符。创作者使用网络上一个文档的位置来定义标识符,其他人然后通过Web协议来获取这个文档。但是这个定义并不合适,首先这暗示者创作者正在标识所移交的内容,也就是意味者如果文档的内容改变了,那么这个标识符也应该改变;其次,存在很多地址对应的一个服务,而不是一个文档;最后,可能有一段时间没有这个文档。
**REST对于"资源"的定义有一个前提 : 标识符应该尽可能的少改变**。原因在于Web使用的是内嵌的标识符,而不是链接服务器。这个内嵌的标识符标识着特定的语义,允许保持对这个标识符的引用,即便是该标识符背后的资源发生了变化,但是其语义并未发生变化。也就是说REST把URI这个标识符定义为资源所要表达的语义,而不是语义背后对应的具体的值。
## 2.2 表述 {#representations}
把**资源**定义为URI标识的**一个概念**,而不是一个具体的文档,这导致了另外的一个问题 : 用户如何访问操作**一个概念**呢?**REST引入了"表述"这个中间层,即通过资源的表述来操作资源,而不是直接在资源本身上进行操作**(一个来源服务器维护者资源的标识符和其对应的表述的映射关系,因此可以通过由资源标识符定义的通用接口移交表述来操作一个资源)。
REST对于资源的定义来自于Web的核心需求 : 独立创作跨越多个可信任的组织边界的互相链接的超文本。强制要求接口的定义和接口的需求相匹配,会使得协议看起来模糊不清,但这仅仅是因为被操作的仅仅是一个接口,而不是一个实现。**所以资源和接口背后的实现细节都应该是被隐藏起来的,通过接口和表述这两个独立的概念来隔离接口和资源的这两者的具体实现,这也是REST的统一接口这个架构约束的动机。**
由HTTP和URI组成了接口,HTML作为资源的表述,使得来源服务器的对接口和资源的具体实现得以统一标准化。同时得益于客户端不再直接操作资源,使得客户端可以选择自己所能理解的表述来操作资源,比如如今的网站可以提供PC版的Web Site,同时提供基于JSON格式的API来操作同一个资源。再进一步,比如某一个网站的实现从.net升级为了net core,服务器从windows换成了centos,web服务器从iis换成了ngnix,数据库从sql server换成了mysql等等,只要其基于URI和HTTP提供的接口未发生变化,某一个API的语义未发生变化,这一切对于客户端来说,都是透明的。这就使得Web的各种组件的独立部署成为了可能。–笔者解读
## 2.3 把语义绑定到URI {#binding-semantics-to-uri}
如上面举得例子,一个资源可以由多个URL(PC网页,基于JSON的API)来操作,当访问这两个URL的时候,其语义是相同的。当然也可能有两个URL,访问的时候,服务器使用了相同的机制,但是这两个URL却是两个不同的资源。
对于通过资源的标识符和表述来操作资源的行为而言,语义是一个副产品(比如在网页上执行登录,付款这两个操作,对于Web的各部分组件来说,并不理解其中的差异,其背后的动作很可能都是post一个请求而已)。语义这部分是交由最终用户来解读的,关于语义这部分这里不细说了(其背后由协议语义,应用语义等具体的概念),后面专门写博客来解释。
## 2.4 URI中不匹配REST的情况 {#rest-mismatches-in-uri}
理想是丰满的,现实是残酷的。并非所有的已经部署的Web组件都遵循Web的设计要求,REST既可以用来定义Web的改进办法,也可以用来识别其中不匹配的部分,尽管无法避免这些不匹配,但是可以在其成为正式规范之前识别出来它们。
尽管URI的设计和REST中标识符的概念相匹配,但是仅仅依靠URI的语法规则是不足以约束不匹配的行为的。其中的一种滥用就是在URL中包含当前用户的信息,这样的办法可以用于维护服务器会话的状态,但是也会降低共享缓存的效率,也会降低服务器的可伸缩性,并且如果一个用户把这个URL发给其他的用户时,会得到不希望看到的结果。这其实时违反了REST的无状态的约束。另外一个便是把Web看作是一个分布式的文件系统的时候,因为文件系统其实是暴露了其实现细节。
# 3 把REST应用于HTTP {#rest-applied-to-http}
HTTP在Web中是一个特殊的角色,它既是Web组件之间通信的的应用级协议,也是作为移交资源的表述而设计的唯一协议(**注 : Fielding发布REST的论文是在2000年,而在2014年又诞生了一个COAP协议,所以以现在时间点来看,HTTP已经不是唯一的协议了**)。REST用来识别早期HTTP协议中的问题,并指定了一个可以和HTTP/1.0互操作的协议子集,然后分析HTTP/1.1的扩展提议,并最终诞生了HTTP/1.1。
## 3.1 可扩展性 {#extensibility}
REST的主要目标之一就是对一个已经部署的架构进行片段式的升级部署。为此为HTTP添加了版本控制,通过主版本和次版本号来区分(1.0 1.1 2.0),其版本信息代表的是消息发送者对协议的支持能力。
HTTP包含了很多的部分,比如URI模式,媒体类型,MIME等,这些部分是由单独协议来控制的。HTTP自身也管理了一些比如方法名称,响应状态码,HTTP中各种的Header信息。HTTP请求的语义由请求方法来表示,对于这个语义是在各个组件直接共享的。再比如响应状态码(1xx,2xx,3xx,4xx,5xx)分别表示一类信息,方便后续进行扩充。
HTTP/1.1也新增了Upgrade头,用来再通信双方进行协商协议版本。
## 3.2 自描述的消息 {#self-descriptive-messages}
HTTP要求组件直接的消息是自描述的,以便支持中间件对交互进行处理。但是早期的HTTP协议在一些方面并不是自描述的。
Host请求头 : 早期的HTTP请求中不会携带host头部信息,这导致了一个IP上只能部署一个服务。
分层编码 : HTTP为了描述表述的元数据,采用可MIME的语法,MIME没有定义分层的媒体类型。
传输独立性 : 早期的HTTP协议,使用了底层的传输协议来表示响应结束,比如服务器通过关闭TCP连接来表明响应消息的结束。这导致一个严重的问题,就是客户端无法无法到底是网络故障导致的断开,还是服务器主动断开的。为此HTTP/1.1加入了Content-Length,用来表示消息体的长度,并且加入了chunked这个编码格式,允许服务器在事先不知道Content-Length的情况下进行数据传输。
缓存控制 : 新增的Cache-Control,Age,Etag等更精确的缓存控制。
性能 : 早期的HTTP协议每个连接只允许发送单个请求和响应,这导致对TCP的使用非常低效。受限于已经部署的组件,HTTP/1.1把默认的持久连接作为了默认的选项,如果要关闭连接,则需要发送close的指令。
## 3.3 HTTP中不匹配REST的情况 {#rest-mismatches-in-http}
这些不匹配是由于部署的第三方扩展或者是为了保证和HTTP/1.0的兼容所导致的。
区分权威的响应 : 既无法区分一个响应是来自于源服务器还是中间的某一个组件,虽然HTTP/1.1中定义了Warning消息头,但是并未广泛使用。
cookie : cookie作为一个站点范围内的黑盒状态信息,会导致基于Cookie的交互于REST的应用状态的模型不匹配(比如上一个页面设置了一个cookie,但是下一个页面或许并不依赖cookie,然而cookie也会被发送出去)。其次其cookie并没有任何的语义信息,只是一段文本消息,这也会导致完全和隐私方便的问题,比如如今的各种的广告使用的第三方cookie,对用户的追踪,造成的隐私和安全问题。
混合元数据 : 我记得HTTP权威指南中把Header分为请求,响应,实体,通用等部分。其实这部分Header按照其用途应该在HTTP本身进行分门别类一下,比如操作的元数据、资源的、表述的、协议控制的和认证的等等用途。如果得以分层和划分,则有助于对消息的处理。
将响应和请求匹配 : 从HTTP的响应消息中,并不能知道其是由那个请求发出的,只能依赖底层的实现。比如如果每一个请求都会有一个Request-Id,然后在其响应中原样返回。
# 4 技术推广 {#technology-transfer}
尽管REST对于Web的标准规范有着最直接的影响,但是把它作为架构设计模型,则是通过各种形式的实现来验证的。比如libwww-perl库,Apache的httpd,早期的IE,网景等。REST架构风格成功的指导了Web的架构设计和部署,到目前为止(从199年的HTTP/1.1发布到如今),Web并未出现严重的问题。而CDN网络(缓存)的出现也显著的改善了用户感知的性能。
# 5 架构上的教训 {#architectural-lessons}
从Web架构和由REST识别出来的问题中,可以总结出来很多通用的架构上的教训。
## 5.1 基于网络的API的优势 {#advantages-of-a-network-based-api}
把Web和其他的中间件区分的一个标志是它使用HTTP作为一个基于网络的API。但是并不是一项如此,早期的Web利用了一个程序库(CERN的libwww)作为所有客户端和服务器软件所使用的单个协议实现库,libwww提供了一个基于库的api来构造可互操作的Web组件。
## 5.2 HTTP不是RPC {#http-is-not-rpc}
人们通常错误的把HTTP视为一种RPC机制,仅仅因为它也是由请求和响应组成的。RPC从本质上来讲,是把一个函数调用放到了跨越网络的另一端,使其使用者看来就像是在调用本地函数一样,RMI也是类似的机制和目的。**把HTTP和RPC分开的并不是其调用细节,而在于其对待网络的影响是怎样看待的,前者侧重于如何有效的利用网络,而后者则在于如何屏蔽网络带来的影响。**
HTTP是基于网络而专门设计的应用层协议 : 它的请求被定向到使用了一个标准的语义的通用接口的组件上,而这个组件可以采用几乎和最终的服务器完全相同的方式来解释这个请求以及其语义,并提供响应。这就可以基于HTTP构建起来一个支持分层转换的系统(比如缓存,代理,网关都属于其中的具体层级),这对于一个基于互联网规模的、跨域多个组织边界的、无法控制的可伸缩性的系统而言,意义是巨大的。
而RPC,则是根据编程语言的API来定义的,虽然现在众多的RPC框架可以支持很多的语言平台,但是其本质还是在有描述一个方法的调用罢了,比如SOAP干的事情本质上就是描述要调用的方法是什么名字,传什么类型的参数,返回什么类型的数据等等这些事情。
## 5.3 HTTP不是传输协议 {#http-is-not-a-transport-protocol}
HTTP并不是被设计为一种传输协议,它是一种移交协议。在HTTP中,通过对资源的表述执行各种动作,其反应出来的是Web架构的语义。使用这个非常简单的接口来实现各种的功能是可能的,前提是必须要遵循这个接口,以便HTTP的协议语义对于中间件而已是可见的,这也是为何HTTP可以穿透防火墙的原因。重点在于Web的各个组件都理解HTTP的协议语义,从而可以独自的完成HTTP的响应,而不必一定到达最终的源服务器,这也是为什么它不是传输协议的原因。
## 5.4 媒体类型的设计 {#design-of-media-types}
这是REST中最容易被忽视的一部分,也就是REST对于Web架构中的数据元素的影响程度。最频繁出现的问题在于违反应用状态和无状态交互的架构约束。比如前面提到的Cookie,以及HTML中的iframe,这导致用户代理无法管理和理解它们提供的间接应用状态。
说点题外话,最近几年火热的前端框架(react,angular,vue),通常都会遇到SEO的问题,那么这个问题产生的根源是什么呢?原因在于HTML本来是承载着应用状态的超媒体语言,浏览器可以理解它,可以通过a,form这些超媒体控件来移交应用的状态,网络爬虫也可以理解这些信息,从而构造出一个网状的状态迁移图。但是呢,前端的框架则打破了这种形态,把HTML近作为了一种UI显示语言来用,应用的状态迁移全都交给了js(js是作为REST的按需代码这一可选的架构约束的具体实现来存在的),这就导致了Web浏览器和网络爬虫得到的只有一些作为UI模板用的HTML,而不知其应用状态在何处。目前这些前端框架做出来的网站,其实是把按需代码这一约束发挥到了极致,其本质上是一个本地应用,只是它的UI语言和业务语言分别是HTML和JS,而这两者可以通过网络来动态的部署罢了。Web APP,首先它具有的一个APP的特征,其次才是Web的特征。
再比如electron,基于此开发的各种桌面应用(VS Code既是基于它),采用了HTML,CSS,JS来开发一个桌面应用,这其实已经和Web没有丝毫关系了。而是得益于可以解析HTML,CSS,JS背后的运行时可以方便的跨平台。
当然说这些并不是批判,而是感叹HTML和JS的的生名力会越来越顽强。Web在蚕食能触及到的方方面面,同时能用JS重写的地方,最终都会被JS重写。
HTML本身具有的增量处理的特性,使得浏览器可以在下载的同时进行页面的渲染,以及进来提供的预先加载的机制可以提前下载js,css,图片,甚至是另外一个HTML网页,从而提升用户感知的性能。**迄今为止,HTML可以说是最为成功的一个超媒体的例子**。
# 6 总结 {#summary}
本篇博客解释了REST在设计Web的标准协议以及实现具体的Web组件中所起到的作用,以及现实中那些并不匹配REST要求的部分和从中得到的经验以及教训。
关于REST的论文的解读,本篇是最后一篇,绝大部分的内容来源于Fielding博士的论文,毕竟REST这个术语诞生于此,论文本身比较晦涩难懂(其实这也不怪Fielding博士,这篇论文是写给IETF和W3C的专家组看的,为此Fielding博士还专门解释过这件事情 : http://roy.gbiv.com/untangled/2008/specialization。),这也从侧面说明论文中所包含的信息是非常具有价值的,比如关于架构风格和架构的解释,对于基于网络的应用的架构风格的总结和比较,以及REST本身,是非常值得花时间去读一读的。
由于笔者本身理解能力也有限,难免有些地方会理解不周或者出现偏差,欢迎大家指正。这个系列的主要意义在于以正确的角度来看待和理解REST,而不要把REST和RESTful API混在一起,后续笔者关于RESTful API的理解会单独来写。
# 7 参考资料 {#reference}
[理解REST] 00 参考资料
转自:https://linianhui.github.io/understand-rest/06-experience/