You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

214 lines
16 KiB
Markdown

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 04 | 工整与自由的风格之争SOAP和REST
你好,我是四火。
今天我要邀请两位风格迥异的主角登上舞台,一位西装革履,另一位随性洒脱。前面那位,代表着工整、严谨和细致;后面那位,代表着自由、灵活和简约。
它们来自两个不同的时代,却同时活跃于当今的互联网,并担当着重量级的角色,影响了一批新技术的诞生。今天,就让我们来认识下它们,它们的名字,分别叫做 SOAP 和 REST。
## 概念
SOAPSimple Object Access Protocol即简单对象访问协议定义了数据对象传输的格式以便在网络的节点之间交换信息。你可能会问HTTP 不就是干这事的吗?其实,它们都在 OSI 7 层模型的应用层上但却互不冲突SOAP 并不依赖于 HTTP 而存在,而且它们可以互相配合。
HTTP 负责信息的传输,比如传递文本数据,关心的是消息的送达,但却不关心这个数据代表着什么。这个数据可能本来是一个内存里的对象,是一篇文章,或者是一张图片。但是 SOAP 恰恰相反,它关心的就是怎样把这个数据给序列化成 XML 格式的文本,在传输到对端以后,再还原回来。
用一个形象的比喻就是,**消息传输就像快递HTTP 主要关心的是信封,而 SOAP 主要关心的是信封里的物件。**今天我们讨论的 SOAP不仅仅是协议本身更主要的是它的风格。
RESTRepresentational State Transfer即表现层状态转换指的是一种为了信息能在互联网上顺利传递而设计的软件架构风格。对请注意**SOAP 是协议,但 REST 是风格,而非协议或标准**,至于 HTTP它是 REST 风格的重要载体。重要,但不是唯一,因为载体并不只有 HTTP 一个,比如还有 HTML 和 XML它们恰当地互相配合组合在一起来贯彻和体现 REST 的风格。
SOAP 和 REST由于概念层次上的不同其实原本是无法放到一起比较的但是当我们旨在风格层面上讨论 SOAP 和 REST 的时候,这件事却变得可行而有趣了。
现在让我们用一个实际例子来进一步认识它们。这个例子很常见假设我们要设计一个图书馆馆中的书可以增删改查CRUD特别是要添加一本书的时候我们分别来看看应用 SOAP 该怎么做,应用 REST 又该怎么做。
## SOAP
这是一个最简单的给图书馆添加一本书的 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](https://www.w3.org/2003/05/soap-envelope/),它定义了基本的语法规则,比如标签的从属关系,比如同级某标签的个数限制等等,举例来说,你可以看到例子中有一个 Body 标签位于 Envelope 内部,这就是它定义的;
* 另一个是代码片段第 4 行的 [soap-encoding](https://www.w3.org/2003/05/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 放在一起谈论的 WSDLWeb Service Description Language。
WSDL 用于描述一个 Web Service说白了就是用来说明某个 Web 服务该怎样使用,有怎样的接口方法,支持怎样的参数,会有怎样的返回。由于支持 SOAP 的服务端接口是经常使用 WSDL 来描述,因此我们才看到它们总被放在一起讨论,于是在这种情况下,**WSDL 常常被形容成 SOAP 服务的使用说明书**,但是请注意,本质上它们之间不存在依赖关系。
**这种将服务和服务的描述解耦开设计的方式非常普遍**,希望你可以去类比和联想。在软件的世界里,我们经常谈论这个“描述”的行为,以及描述者和被描述者。比如元属性描述数据,方法签名描述方法,类描述对象等等。
## REST
现在,我们再来看 REST 的做法。**REST 的核心要素包括资源、表现层和状态转换这三个部分。**我们把前面客户端发送请求的过程使用 REST 风格再来实现一遍,你将看到这三个要点是怎样体现出来的:
### 1\. 协议
我们将使用 HTTP 协议,在加密的情况下,协议是 HTTPS但这对我们的实现来说没有什么区别。
### 2\. URL
通常来说,这个 URL 要包括域名、版本号以及实体名称,而这个 URL 整体,代表了 REST 中的一类或一项“资源”。比如说:
```
https://xxx/v1/books
```
请注意其中的实体名称,它往往是一个单纯的名词,并且以复数形式出现。
这里提到了 URL我想给经常混用的 URL、URI 做个简要的说明URL 指的是 Uniform Resource LocatorURI 指的是 Uniform Resource Identifier前者是统一资源定位符后者是统一资源标识符。**Identifier 可以有多种形式,而 locator 只是其中一种,因此 URI 更宽泛URL 只是 URI 的其中一种形式**。
当我们提到一个完整的地址,例如 [https://www.google.com](https://www.google.com) 它就是 URL因为它可以被“定位”它当然也是 URI但是如果我们只提到上面地址的一个片段例如 www.google.com那么由于缺少了具体协议我们无法完成完整的定位因此这样的东西只能被叫做一个标识符故而只能算 URI而非 URL。
### 3\. 方法
HTTP 的方法反映了这个接口上将执行的行为,如果用自然语言描述,它将是一个动词。比如说,给图书馆添加一本图书,那么这个行为将是“添加”。在以 REST 风格主导的设计中,我们将使用这样的 HTTP 方法来代表增删改查CRUD的不同动作
![](https://static001.geekbang.org/resource/image/2f/6c/2f80b22c23fb5382e359ece6082c2f6c.jpg)
重点解释下表格的最后两列:
* **幂等性指的是对服务端的数据状态,执行多次操作是否和执行一次产生的结果一样。**从表格中你可以看到,创建资源单位不是幂等的,执行多次就意味着创建了多个资源单位,而其它操作都是幂等的。通常来说,**幂等操作是可以被重试而不产生副作用的。**
* **安全性指的是该操作是否对服务端的数据状态产生了影响。**从表格中可以看到,获取资源的操作是安全的,不会对资源数据产生影响,但是其它操作都是不安全的。一定条件下,**安全操作是可以被缓存的**,而不安全的操作,必定对服务端的状态产生了影响,这体现了 REST 的“状态转换”这一要素。
全栈系统的设计和优化都需要紧密围绕幂等性和安全性进行,这是两个非常重要的概念,在我们后续的学习中,你还会反复见到它们,并和它们打交道。
你看,通过这样的办法,就把 HTTP 的方法和实际对资源的操作行为绑定起来了。当然,还有一些其它方法,比较常见的有:
* PATCH和 PUT 类似,也用于资源更新,但支持的是资源单位的部分更新,并且在资源不存在的时候,能够自动创建资源,这个方法实际使用比较少。
* HEAD这个方法只返回资源的头部避免了资源本身获取和传输的开销。这种方法很有用经常用来检查资源是否存在以及有无修改。
* OPTIONS这个方法常用来返回服务器对于指定资源所支持的方法列表。
### 4\. 正文
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 这样两种截然不同的风格,前者工整、严谨和细致,后者自由、灵活和简约。两道思考题如下:
* 在做技术比较的时候,文中已经简单介绍了 REST 和 SOAP 的优劣,你觉得,它们各自适合怎样的业务场景呢?
* 有位程序员朋友在应用 RESTful 风格设计用户管理系统的接口时,“删除单个用户”功能的 URL 举例如下,你觉得有哪些问题?
```
http://xxx/deleteUser?userName=James
```
今天的内容就到这里,希望你已经享受到了技术学习的快乐,如果你还有余力,请继续学习下面的选修课堂和扩展阅读。最后,对于上面的问题,或者你对今天的学习有什么感受,欢迎在留言区和我讨论!
## 选修课堂:动手调用 RESTful API
学习全栈怎么能不动手实践呢,现在就让我们开始吧。有一些在线工具,预置了 REST 风格的接口服务,我们可以使用命令行去指定不同的 HTTP 方法,发送一些不同的 HTTP 请求,观察返回,通过实际的练习,相信你能够更好地理解 REST。这样的工具有很多你可以自行搜索也可以直接选择 [REQ|RES](https://reqres.in/)
![](https://static001.geekbang.org/resource/image/2d/b8/2d216d1910348056c1631d46f0f099b8.png)
你可以使用网站上预置的请求,但我更推荐你自己写 curl 命令。比如发送一个 GET 请求,列出所有用户:
```
curl -v https://reqres.in/api/users | jq
```
其中的 -v 参数可以帮助输出详尽的信息,包括请求和响应的完整信息,当然也可以不用;后面的 “| jq” 是为了让返回的 JSON 数据展示更美观,当然,你需要安装 [jq](https://stedolan.github.io/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"
}
```
## 扩展阅读
* W3Cschool 上的 [SOAP 教程](https://www.w3cschool.cn/soap/?),如果你对 SOAP 不够熟悉,那么你可以参考这个简明扼要的教程。
* 【基础】[REST API Tutorial](https://www.restapitutorial.com/)REST 的教程很多,这是我觉得非常简洁和清晰的一个。
* ProgrammableWeb 上的 [Web API 列表](https://www.programmableweb.com/apis/directory),排名最靠前的 10 个 API其中有 9 个的架构风格都是 REST这也从侧面应证了 REST 在互联网的趋势。
* [REST 和 SOAP谁更好或者都好](https://www.infoq.cn/article/rest-soap-when-to-use-each)这是一篇内容精悍的译文,分别介绍了适合 REST 和 SOAP 的场景。