gitbook/趣谈网络协议/docs/12512.md
2022-09-03 22:05:03 +08:00

11 KiB
Raw Permalink Blame History

第34讲 | 基于JSON的RESTful接口协议我不关心过程请给我结果

上一节我们讲了基于XML的SOAP协议SOAP的S是啥意思来着是Simple但是好像一点儿都不简单啊

你会发现对于SOAP来讲无论XML中调用的是什么函数多是通过HTTP的POST方法发送的。但是咱们原来学HTTP的时候我们知道HTTP除了POST还有PUT、DELETE、GET等方法这些也可以代表一个个动作而且基本满足增、删、查、改的需求比如增是POST删是DELETE查是GET改是PUT。

传输协议问题

对于SOAP来讲比如我创建一个订单用POST在XML里面写明动作是CreateOrder删除一个订单还是用POST在XML里面写明了动作是DeleteOrder。其实创建订单完全可以使用POST动作然后在XML里面放一个订单的信息就可以了而删除用DELETE动作然后在XML里面放一个订单的ID就可以了。

于是上面的那个SOAP就变成下面这个简单的模样。

POST /purchaseOrder HTTP/1.1
Host: www.geektime.com
Content-Type: application/xml; charset=utf-8
Content-Length: nnn

<?xml version="1.0"?>
 <order>
     <date>2018-07-01</date>
      <className>趣谈网络协议</className>
       <Author>刘超</Author>
       <price>68</price>
  </order>

而且XML的格式也可以改成另外一种简单的文本化的对象表示格式JSON。

POST /purchaseOrder HTTP/1.1
Host: www.geektime.com
Content-Type: application/json; charset=utf-8
Content-Length: nnn

{
 "order": {
  "date": "2018-07-01",
  "className": "趣谈网络协议",
  "Author": "刘超",
  "price": "68"
 }
}

经常写Web应用的应该已经发现这就是RESTful格式的API的样子。

协议约定问题

然而RESTful可不仅仅是指API而是一种架构风格全称Representational State Transfer表述性状态转移来自一篇重要的论文《架构风格与基于网络的软件架构设计》Architectural Styles and the Design of Network-based Software Architectures

这篇文章从深层次更加抽象地论证了一个互联网应用应该有的设计要点而这些设计要点成为后来我们能看到的所有高并发应用设计都必须要考虑的问题再加上REST API比较简单直接所以后来几乎成为互联网应用的标准接口。

因此和SOAP不一样REST不是一种严格规定的标准它其实是一种设计风格。如果按这种风格进行设计RESTful接口和SOAP接口都能做到只不过后面的架构是REST倡导的而SOAP相对比较关注前面的接口。

而且由于能够通过WSDL生成客户端的Stub因而SOAP常常被用于类似传统的RPC方式也即调用远端和调用本地是一样的。

然而本地调用和远程跨网络调用毕竟不一样,这里的不一样还不仅仅是因为有网络而导致的客户端和服务端的分离,从而带来的网络性能问题。更重要的问题是,客户端和服务端谁来维护状态。所谓的状态就是对某个数据当前处理到什么程度了。

这里举几个例子,例如,我浏览到哪个目录了,我看到第几页了,我要买个东西,需要扣减一下库存,这些都是状态。本地调用其实没有人纠结这个问题,因为数据都在本地,谁处理都一样,而且一边处理了,另一边马上就能看到。

当有了RPC之后我们本来期望对上层透明就像上一节说的“远在天边尽在眼前”。于是使用RPC的时候对于状态的问题也没有太多的考虑。

就像NFS一样客户端会告诉服务端我要进入哪个目录服务端必须要为某个客户端维护一个状态就是当前这个客户端浏览到哪个目录了。例如客户端输入cd hello服务端要在某个地方记住上次浏览到/root/liuchao了因而客户的这次输入应该给它显示/root/liuchao/hello下面的文件列表。而如果有另一个客户端同样输入cd hello服务端也在某个地方记住上次浏览到/var/lib因而要给客户显示的是/var/lib/hello。

不光NFS如果浏览翻页我们经常要实现函数next()在一个列表中取下一页但是这就需要服务端记住客户端A上次浏览到2030页了那它调用next()应该显示3040页而客户端B上次浏览到100110页了调用next()应该显示110120页。

上面的例子都是在RPC场景下由服务端来维护状态很多SOAP接口设计的时候也常常按这种模式。这种模式原来没有问题是因为客户端和服务端之间的比例没有失衡。因为一般不会同时有太多的客户端同时连上来所以NFS还能把每个客户端的状态都记住。

公司内部使用的ERP系统如果使用SOAP的方式实现并且服务端为每个登录的用户维护浏览到报表那一页的状态由于一个公司内部的人也不会太多把ERP放在一个强大的物理机上也能记得过来。

但是互联网场景下,客户端和服务端就彻底失衡了。你可以想象“双十一”,多少人同时来购物,作为服务端,它能记得过来吗?当然不可能,只好多个服务端同时提供服务,大家分担一下。但是这就存在一个问题,服务端怎么把自己记住的客户端状态告诉另一个服务端呢?或者说,你让我给你分担工作,你也要把工作的前因后果给我说清楚啊!

那服务端索性就要想了,既然这么多客户端,那大家就分分工吧。服务端就只记录资源的状态,例如文件的状态,报表的状态,库存的状态,而客户端自己维护自己的状态。比如,你访问到哪个目录了啊,报表的哪一页了啊,等等。

这样对于API也有影响也就是说当客户端维护了自己的状态就不能这样调用服务端了。例如客户端说我想访问当前目录下的hello路径。服务端说我怎么知道你的当前路径。所以客户端要先看看自己当前路径是/root/liuchao然后告诉服务端说我想访问/root/liuchao/hello路径。

再比如客户端说我想访问下一页服务端说我怎么知道你当前访问到哪一页了。所以客户端要先看看自己访问到了100110页然后告诉服务器说我想访问110120页。

这就是服务端的无状态化。这样服务端就可以横向扩展了,一百个人一起服务,不用交接,每个人都能处理。

所谓的无状态其实是服务端维护资源的状态客户端维护会话的状态。对于服务端来讲只有资源的状态改变了客户端才调用POST、PUT、DELETE方法来找我如果资源的状态没变只是客户端的状态变了就不用告诉我了对于我来说都是统一的GET。

虽然这只改进了GET但是已经带来了很大的进步。因为对于互联网应用大多数是读多写少的。而且只要服务端的资源状态不变就给了我们缓存的可能。例如可以将状态缓存到接入层甚至缓存到CDN的边缘节点这都是资源状态不变的好处。

按照这种思路对于API的设计就慢慢变成了以资源为核心而非以过程为核心。也就是说客户端只要告诉服务端你想让资源状态最终变成什么样就可以了而不用告诉我过程不用告诉我动作。

还是文件目录的例子。客户端应该访问哪个绝对路径,而非一个动作,我就要进入某个路径。再如,库存的调用,应该查看当前的库存数目,然后减去购买的数量,得到结果的库存数。这个时候应该设置为目标库存数(但是当前库存数要匹配),而非告知减去多少库存。

这种API的设计需要实现幂等因为网络不稳定就会经常出错因而需要重试但是一旦重试就会存在幂等的问题也就是同一个调用多次调用的结果应该一样不能一次支付调用因为调用三次变成了支付三次。不能进入cd a做了三次就变成了cd a/a/a。也不能扣减库存调用了三次就扣减三次库存。

当然按照这种设计模式无论RESTful API还是SOAP API都可以将架构实现成无状态的面向资源的、幂等的、横向扩展的、可缓存的。

但是SOAP的XML正文中是可以放任何动作的。例如XML里面可以写< ADD >< MINUS >等。这就方便使用SOAP的人将大量的动作放在API里面。

RESTful没这么复杂也没给客户提供这么多的可能性正文里的JSON基本描述的就是资源的状态没办法描述动作而且能够出发的动作只有CRUD也即POST、GET、PUT、DELETE也就是对于状态的改变。

所以从接口角度就让你死了这条心。当然也有很多技巧的方法在使用RESTful API的情况下依然提供基于动作的有状态请求这属于反模式了。

服务发现问题

对于RESTful API来讲我们已经解决了传输协议的问题——基于HTTP协议约定问题——基于JSON最后要解决的是服务发现问题。

有个著名的基于RESTful API的跨系统调用框架叫Spring Cloud。在Spring Cloud中有一个组件叫 Eureka。传说阿基米德在洗澡时发现浮力原理高兴得来不及穿上裤子跑到街上大喊“Eureka我找到了”所以Eureka是用来实现注册中心的负责维护注册的服务列表。

服务分服务提供方它向Eureka做服务注册、续约和下线等操作注册的主要数据包括服务名、机器IP、端口号、域名等等。

另外一方是服务消费方向Eureka获取服务提供方的注册信息。为了实现负载均衡和容错服务提供方可以注册多个。

当消费方要调用服务的时候会从注册中心读出多个服务来那怎么调用呢当然是RESTful方式了。

Spring Cloud提供一个RestTemplate工具用于将请求对象转换为JSON并发起Rest调用RestTemplate的调用也是分POST、PUT、GET、 DELETE的当结果返回的时候根据返回的JSON解析成对象。

通过这样封装,调用起来也很方便。

小结

好了,这一节就到这里了,我们来总结一下。

  • SOAP过于复杂而且设计是面向动作的因而往往因为架构问题导致并发量上不去。

  • RESTful不仅仅是一个API而且是一种架构模式主要面向资源提供无状态服务有利于横向扩展应对高并发。

最后,给你留两个思考题:

  1. 在讨论RESTful模型的时候举了一个库存的例子但是这种方法有很大问题那你知道为什么要这样设计吗

  2. 基于文本的RPC虽然解决了二进制的问题但是它本身也有问题你能举出一些例子吗

我们的专栏更新到第34讲不知你掌握得如何每节课后我留的思考题你都有没有认真思考并在留言区写下答案呢我会从已发布的文章中选出一批认真留言的同学,赠送学习奖励礼券和我整理的独家网络协议知识图谱。

欢迎你留言和我讨论。趣谈网络协议,我们下期见!