你好,我是四火。
今天我要邀请两位风格迥异的主角登上舞台,一位西装革履,另一位随性洒脱。前面那位,代表着工整、严谨和细致;后面那位,代表着自由、灵活和简约。
它们来自两个不同的时代,却同时活跃于当今的互联网,并担当着重量级的角色,影响了一批新技术的诞生。今天,就让我们来认识下它们,它们的名字,分别叫做 SOAP 和 REST。
SOAP,Simple Object Access Protocol,即简单对象访问协议,定义了数据对象传输的格式,以便在网络的节点之间交换信息。你可能会问,HTTP 不就是干这事的吗?其实,它们都在 OSI 7 层模型的应用层上,但却互不冲突,SOAP 并不依赖于 HTTP 而存在,而且它们可以互相配合。
HTTP 负责信息的传输,比如传递文本数据,关心的是消息的送达,但却不关心这个数据代表着什么。这个数据可能本来是一个内存里的对象,是一篇文章,或者是一张图片。但是 SOAP 恰恰相反,它关心的就是怎样把这个数据给序列化成 XML 格式的文本,在传输到对端以后,再还原回来。
用一个形象的比喻就是,消息传输就像快递,HTTP 主要关心的是信封,而 SOAP 主要关心的是信封里的物件。今天我们讨论的 SOAP,不仅仅是协议本身,更主要的是它的风格。
REST,Representational State Transfer,即表现层状态转换,指的是一种为了信息能在互联网上顺利传递而设计的软件架构风格。对,请注意,SOAP 是协议,但 REST 是风格,而非协议或标准,至于 HTTP,它是 REST 风格的重要载体。重要,但不是唯一,因为载体并不只有 HTTP 一个,比如还有 HTML 和 XML,它们恰当地互相配合,组合在一起,来贯彻和体现 REST 的风格。
SOAP 和 REST,由于概念层次上的不同,其实原本是无法放到一起比较的,但是当我们旨在风格层面上讨论 SOAP 和 REST 的时候,这件事却变得可行而有趣了。
现在让我们用一个实际例子来进一步认识它们。这个例子很常见,假设我们要设计一个图书馆,馆中的书可以增删改查(CRUD),特别是要添加一本书的时候,我们分别来看看,应用 SOAP 该怎么做,应用 REST 又该怎么做。
这是一个最简单的给图书馆添加一本书的 XML 消息体:
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope
xmlns:soap="https://www.w3.org/2003/05/soap-envelope/"
soap:encodingStyle="https://www.w3.org/2003/05/soap-encoding">
<soap:Body xmlns:b="...">
<b:CreateBook>
<b:Name>...</m:Name>
<b:Author>...</m:Author>
...
</b:CreateBook>
</soap:Body>
</soap:Envelope>
让我来简单解释一下:
1.第一行指明了这个消息本身格式是 XML,包括它的版本号和编码格式。
2.这里的很多标签都带有“soap:”的前缀,其实,这里的“soap”就是 XML 的命名空间(其中“xmlns”就是指“xml namespace”),并且通过 XML schema 的方式预先定义好了如下两个 SOAP 消息必须要遵从的规则:
一个是代码片段第 3 行的 soap-envelope,它定义了基本的语法规则,比如标签的从属关系,比如同级某标签的个数限制等等,举例来说,你可以看到例子中有一个 Body 标签位于 Envelope 内部,这就是它定义的;
另一个是代码片段第 4 行的 soap-encoding,它定义了编码和数据类型等规则。
3.在 Body 标签内部,有一个 CreateBook 标签,这是我们的业务标签,命名空间 b 也是我们自己定义的。通过在内部封装姓名(Name)和作者(Author)等书本信息,实现了在图书馆中添加书本的需求。
上面是一个最简单的例子,实际在 Envelope 中还可以添加 Head 标签,用于存放头部信息,在 Body 中可以添加 Fault 标签,用于存放错误信息。关于这些,都在 XML schema 中做了严格的定义,通过它可以帮助验证一个 XML 是否符合格式,从而可以在最短的时间内验证并发现消息中的格式问题。
SOAP 通常是通过 HTTP POST 的方式发送到对端的,这个图书馆对书本的增删改查操作,URL 可以是同一个,这是因为 SOAP 消息的具体内容写明了具体要干什么(上述的 CreateBook 标签)。比如下面这个例子,请注意其中的 Content-Type,它是令响应具备自我描述特性的重要组成部分:
POST /books HTTP/1.1
Host: xxx
Content-Type: application/soap+xml; charset=utf-8
Content-Length: xxx
... (省略前述的 SOAP 消息体)
最后,谈谈经常和 SOAP 放在一起谈论的 WSDL,Web Service Description Language。
WSDL 用于描述一个 Web Service,说白了,就是用来说明某个 Web 服务该怎样使用,有怎样的接口方法,支持怎样的参数,会有怎样的返回。由于支持 SOAP 的服务端接口是经常使用 WSDL 来描述,因此我们才看到它们总被放在一起讨论,于是在这种情况下,WSDL 常常被形容成 SOAP 服务的使用说明书,但是请注意,本质上它们之间不存在依赖关系。
这种将服务和服务的描述解耦开设计的方式非常普遍,希望你可以去类比和联想。在软件的世界里,我们经常谈论这个“描述”的行为,以及描述者和被描述者。比如元属性描述数据,方法签名描述方法,类描述对象等等。
现在,我们再来看 REST 的做法。REST 的核心要素包括资源、表现层和状态转换这三个部分。我们把前面客户端发送请求的过程使用 REST 风格再来实现一遍,你将看到这三个要点是怎样体现出来的:
我们将使用 HTTP 协议,在加密的情况下,协议是 HTTPS,但这对我们的实现来说没有什么区别。
通常来说,这个 URL 要包括域名、版本号以及实体名称,而这个 URL 整体,代表了 REST 中的一类或一项“资源”。比如说:
https://xxx/v1/books
请注意其中的实体名称,它往往是一个单纯的名词,并且以复数形式出现。
这里提到了 URL,我想给经常混用的 URL、URI 做个简要的说明:URL 指的是 Uniform Resource Locator,URI 指的是 Uniform Resource Identifier,前者是统一资源定位符,后者是统一资源标识符。Identifier 可以有多种形式,而 locator 只是其中一种,因此 URI 更宽泛,URL 只是 URI 的其中一种形式。
当我们提到一个完整的地址,例如 https://www.google.com, 它就是 URL,因为它可以被“定位”,它当然也是 URI;但是如果我们只提到上面地址的一个片段,例如 www.google.com,那么由于缺少了具体协议,我们无法完成完整的定位,因此这样的东西只能被叫做一个标识符,故而只能算 URI,而非 URL。
HTTP 的方法反映了这个接口上将执行的行为,如果用自然语言描述,它将是一个动词。比如说,给图书馆添加一本图书,那么这个行为将是“添加”。在以 REST 风格主导的设计中,我们将使用这样的 HTTP 方法来代表增删改查(CRUD)的不同动作:
重点解释下表格的最后两列:
全栈系统的设计和优化都需要紧密围绕幂等性和安全性进行,这是两个非常重要的概念,在我们后续的学习中,你还会反复见到它们,并和它们打交道。
你看,通过这样的办法,就把 HTTP 的方法和实际对资源的操作行为绑定起来了。当然,还有一些其它方法,比较常见的有:
POST 和 PUT 请求都是有 HTTP 正文的,正文的类型和 Content-Type 的选取有关,比如 JSON 就是最典型的一种格式。请不要轻视这里的 Content-Type,从本质上说,它代表了资源的表现形式,从而体现了 REST 定义中的“表现层”这一要素。
最后,回到我们实际的图书馆添加图书的问题。SOAP 添加一本书的消息,用 REST 风格的 POST 请求实现就会变成这样:
POST /v1/books HTTP/1.1
HOST: ...
Content-Type: application/json
{
"name": "...",
"author": "...",
...
}
看到这儿,你应该已经感受到了,SOAP 和 REST 代表了两种迥异的风格。在我们取舍技术的时候,如果没有给出具体场景和限制,我们往往是很难讲出谁更“好”,而是需要进行比较,权衡利弊的。
SOAP 明显是更“成熟”的那一个。它在数据传输的协议层面做了更多的工作,藉由 XML schema,它具备更严格的检查和校验,配合 WSDL,在真正发送请求前,几乎就对所有远程接口事无巨细的问题有了答案。但是,它的复杂度令人望而生畏,也是它最受人诟病的地方。
REST 则相反,新接口的学习成本很低,只需要知道资源名称,根据我们熟知的规约,就可以创建出 CRUD 的请求来。但是直到真正发送请求去测试为止,并没有办法百分百确定远程接口的调用是否能工作,或者说,并不知道接口定义上是否有不规范、不合常规的坑在里面。
对于互联网来说,SOAP 已经是一项“古老”的技术了,晚辈 REST 似乎更切合互联网的潮流。在大多数情况下,REST 要易用和流行得多,于是很多人都不喜欢繁琐的 SOAP 协议。技术的发展总是有这样的规律,一开始无论问题还是办法都很简单,可是随着需求的进一步增加,解决的方法也缓慢演化,如 SOAP 一般强大而复杂,直到某一天突然掉到谷底,如 REST 一般返璞归真。
但是别忘了,有利必有弊。首先,正是因为 REST 只是一个缺乏限制的风格,而非一个严谨的规范,有太多不明确、不一致的实现导致的问题,这些问题轻者给接口调用者带来困惑,重者导致接口调用错误,甚至服务端数据错误。
其次,REST 通过 HTTP 方法实现本身,也受到了 HTTP 方法的局限性制约。比如最常见的 GET 请求,有时需要一个复杂的查询条件集合,因此参数需要结构化,而 GET 只支持一串键值对组合的参数传递,无法适应业务需要。对于这样的问题,有一些 workaround,比如使用 POST 消息体来传递查询条件的结构体,但那已经偏离了 REST 的最佳实践,丢失了 GET 本身的优势,也带来了实现不一致等一系列问题。
最后,REST 还存在的一个更本质的问题,资源是它的核心概念,这原本带来了抽象和简洁的优势,但如今也成为了它的桎梏。或者说,前面反复提到的增删改查是它最拿手的本事,可是互联网的需求是千变万化的,远不只有简单的增删改查。有时需要一个复杂的多步操作,有时则需要一个异步事务(需要回调或者二次查询等等方式来实现),这些都没有一个完美统一的 REST 风格的解决方案。即便实现出来了,也可谓五花八门,同时失去了以往我们所熟知的 REST 的简洁优势。
互联网总在变复杂,但矛盾的是,人们却希望互联网上的交互会不断变简单。于是这引发了 REST 的流行,可即便 REST 再流行,依旧有它不适合的场景;SOAP 虽古老,依然有它的用武之地。
对于全栈工程师或者期待修炼全栈技能的你我来说,trade-off 是永恒的话题。另外,除了 SOAP 和 REST,其实我们还有其它选择。我将在下一讲,结合实例具体介绍如何选择技术,并设计和实现远程接口。
今天我们认识并学习了 SOAP 和 REST 这样两种截然不同的风格,前者工整、严谨和细致,后者自由、灵活和简约。两道思考题如下:
http://xxx/deleteUser?userName=James
今天的内容就到这里,希望你已经享受到了技术学习的快乐,如果你还有余力,请继续学习下面的选修课堂和扩展阅读。最后,对于上面的问题,或者你对今天的学习有什么感受,欢迎在留言区和我讨论!
学习全栈怎么能不动手实践呢,现在就让我们开始吧。有一些在线工具,预置了 REST 风格的接口服务,我们可以使用命令行去指定不同的 HTTP 方法,发送一些不同的 HTTP 请求,观察返回,通过实际的练习,相信你能够更好地理解 REST。这样的工具有很多,你可以自行搜索,也可以直接选择 REQ | RES:
你可以使用网站上预置的请求,但我更推荐你自己写 curl 命令。比如发送一个 GET 请求,列出所有用户:
curl -v https://reqres.in/api/users | jq
其中的 -v 参数可以帮助输出详尽的信息,包括请求和响应的完整信息,当然也可以不用;后面的 “| jq” 是为了让返回的 JSON 数据展示更美观,当然,你需要安装 jq。如果你没有安装,不使用 jq 管道也是完全可以的。
再比如,使用 POST 请求创建一个用户:
curl -X POST -d '{"name":"xxx", "job":"yyy"}' -H "Content-Type: application/json" https://reqres.in/api/users | jq
这里使用了 -x 参数指定其为 POST 请求,之后的 Content-Type 是必不可少的,而 JSON 形式的 user 对象则通过参数 -d 传了过去。
最后得到了这样的结果:
... (省略请求统计信息)
{
"name": "xxx",
"job": "yyy",
"id": "585",
"createdAt": "2019-07-20T22:19:49.825Z"
}
评论