REST 是新 SOAP?

阅读数:1728 2018 年 2 月 6 日

话题:REST语言 & 开发架构

REST 只是新时期的 SOAP?来看看 Pakal De Bonchamp 是怎么说的,以及 Phil Sturgeon 的反击。以下内容经过编译,点击文末的链接可查看原文。

背景

几年前我为一个大型通信公司开发了一套系统,需要在各种 Web 服务间通信,从老掉牙的系统或者商业伙伴的系统中获取数据。

整个事情乱成一锅粥:难懂的 WSDL、不兼容的库、奇妙的 bug……所以我们尽可能用 RPC 通信:XMLRPC 或 JSONRPC。

我们开发的第一个应用很简单,不过随着迭代的进行,终于可以支持各种 dialect(包括 Apache 对 XMLRPC 的扩展),把 Python 的异常转换成错误代码,处理各种错误,记录请求日志,验证输入数据,等等。

几行代码就可以调用这些 API,稍微封装一下就可以生成新的功能。应用间的通信也特别简单:系统管理员自己就可以完成这些工作。

然后 REST 出现了

REST 重新定义了服务间的通信:RPC 已死,未来是 RESTful 的。每个资源都有自己的 URL,并通过 HTTP 协议进行操作。

然后呢?天塌地陷。

REST 有什么问题?

给个例子吧。以下是一个 API,去掉了数据:

复制代码
createAccount(username, contact_email, password) -> account_id
addSubscription(account_id, subscription_type) -> subscription_id
sendActivationReminderEmail(account_id) -> null
cancelSubscription(subscription_id, reason, immediate=True) -> null
getAccountDetails(account_id) -> {full data tree}

再定义一些异常(例如,参数不对、参数缺失、工作流异常),抛出一些错误(例如,用户名已被使用),整个 API 就算完成了。

这个 API 简单易用、稳定,而且背后的状态机也很完善,用户无法进行非法操作(例如,用户不能修改账号的创建时间)。

用 RPC 写这个 API 需要几个小时时间

RESTful 呢?

RESTful 没有太多的标准和规范,只有“RESTful 哲学”,每个点都可以拎出来讨论一番。

上面的功能怎么能直接映射到简单的 CRUD 操作?发送验证邮件是把“必须发送验证邮件”更新一下?还是创建一个“验证邮件”?在宽限期内取消订阅这个命令能不能用 DELETE,过后能不能回滚?获取账户信息如何写成 RESTful 的?

资源怎么定义?是不难,但也得定义啊。

怎么用几个 HTTP 代码表示错误信息?

输入输出的格式是怎样的?怎么序列化?

HTTP 动词、URL、请求、请求头和状态码的分界线在哪?

整件事情就是在重复造轮子,而且造的还不是好轮子。造轮子还需要一大堆文档,而且肯定违反 RESTful 原则。

为什么 RESTful 这么难用?

先让我们来看看 REST 的设计哲学。

REST 的动词们

REST 并不是 CRUD,所以 REST 的拥护者们当然会想办法让用户不会将这二者混在一起。他们发现 HTTP 已经提供了一些语义用于 CRUD,例如用于创建的 POST、用于获取的 GET、用于更新的 PUT 和 PATCH,以及用于删除的 DELETE。

REST 的意思是,有这几个方法就足够了。看起来好像是这么回事:“今天我 UPDATE 了 CarDriverSeat,CREATE 了 EngineIgnition,DELETE 了 FuelTank”。这些听起来是不是有点挺别扭?

如果说简单化是件好事,起码要把它做好了。为什么从来不在 Web 表单上使用 PUT、PATCH 和 DELETE?因为这几个方法百害而无一利。读取的时候使用 GET,写入的时候使用 POST,这就够了。或者如果你不想被运营商劫持,那就只用 POST。

如果用 PUT 更新资源呢?可以,但是你需要和 GET 读取到的数据格式一样,包括一大堆只读数据(创建时间、最后更新时间、服务器生成的令牌……)。请问你是准备不管 RESTful 规则了吗?还是老老实实组装一个请求,PUT 到服务器上,结果报“HTTP 409 Conflict”错误,因为服务器上某个值变了。然后再 GET 一下,重新组装请求后再 PUT?还是你觉得服务器会忽略只读的参数?(服务器有可能忽略了,也有可能直接炸了哦)要是某些值根本就不能让你 GET 呢,例如,密码或信用卡号码?

哦,别忘了,如果有多个客户端,PUT 有可能造成竞态条件,哪怕每个客户端要更新的参数不一样。

行,那就用 PATCH。那么该如何使用 PATCH?只发送需要改变的参数,指望服务器能够理解你的操作意图?然而你又违反了 RESTful 的原则:PATCH 不是发一堆参数让服务器猜,而是需要给服务器一些指示。

DELETE 呢?你不能只 DELETE 一部分内容,例如 PDF 的一页,因为 DELETE 不能包含请求体。当然大家已经不管这套了,因为没人这么写。连 RFC 2616 都撒手不管了。

所以根本没人能写出完全 RESTful 的 API。很多人用 PUT 指向 URL 创建资源,但是 RESTful 要求你对上级 URL 发一个 POST,在 Location 头部信息(和 301 不是一个意思)里面加上地址。

手写 URL 挺有意思,但是你用好 urlencode 了吗?如果没有,那就等着接受 SSRF/CSRF 攻击吧。

REST 的错误处理

实现功能容易,但是好的程序员会做错误处理。

HTTP 有很多错误码,让我们来瞧瞧。

HTTP 404 表示某个资源不存在,是不是很直观?但如果你没配好 Nginx,你的 API 用户有可能把账号给删了,因为在他们看来,这些账号不存在啊。

HTTP 401 表示用户没权限,这看起来是不是很棒?然而,如果你在浏览器里这么玩,你的用户有可能看见浏览器蹦出一个输入框让他们输入用户名和密码。

HTTP 比 RESTful 的历史长得多,有很多约定俗成的东西。用 HTTP 状态码表示错误就像用牛奶瓶装剧毒废物:总有一天你会药死谁。

有些 HTTP 状态码是 WebDAV 专用的,有些是微软专用的,有些不知所云。最后大家开始瞎用状态码,什么 HTTP 418(查查呗)什么的。或者,所有的错误全用 HTTP 400,里面再套一层状态码。或者全用 200,里面再写加上详细信息。

REST 的概念

REST 搞了一大堆概念,我从官网摘录了一些。

REST 是一种客户端到服务器端的架构,客户端和服务器端的关注点不同

REST 为组件提供了统一的接口

REST 是一种分层的架构,每个组件只能看到与自己有直接交互关系的层

REST 是无状态的。嗯,有个数据库,但是记不住客户端的状态?也不对,因为数据库保存了 session 和 token 了啊。但是随便吧,这东西到底怎么比其他的协议高明了?

REST 可以利用 HTTP 缓存。好吧,至少 GET 是可以的。但是本地缓存(比如 Memcached)不够用吗?想想一下 ISP 劫持,再考虑一下你的所作所为?或者某个 Varnish 没配置好,所以缓存没更新?这种系统就是不安全的:缓存虽好,但只给读操作频繁的那部分 GET 端点用上缓存就可以了。

REST 性能高。是吗?本地的 API 最好功能强大,开发方便;远程的 API 最好粗放,减少网络压力。RESTful 永远有 N+1 请求问题:要获取数据,必须每个参数都发一个请求,而且不能并行化,因为请求互相依赖。这简直是在自找麻烦。

REST 兼容性好。是吗?那为什么还需要“/v2/”或“/v3/”这样的 URL?实现 API 的兼容性并不难,但需要好好设计。

REST 简单易用,因为大家都知道 HTTP。我还知道鹅卵石呢,但是我照样用钢筋水泥盖房子。所以 XML 是文本语言,HTTP 是文本协议。想开发功能需要定义很多东西,然后就是重写 RPC。

REST 简单到可以使用 curl 来读取,因为curl 可以发送任何请求。GET 很简单,但 POST 不是,所以最后你还得老老实实用 Postman。

客户端不需要预先知道服务器的信息。我发现这个东西和 HATEOAS 经常一起出现。但说实话,客户端也是人写的啊。客户端不会瞎请求 API,然后一点点猜服务器支持的操作。一般不都是客户端要求服务器开放某个端点嘛,否则怎么开发?

那怎么办?

别考虑是不是正确了,把活干了才是硬道理。

真正的问题是:如果需要开发一个类似 RESTful 的 API,怎么做是最快的?

服务器怎么办?

任何框架都可以设置 URL 端点。利用这个吧。

Django 可以自动创建这种 API,就是在 SQL 或 noSQL 加一层。如果就是 HTTP 的 CRUD,这样一般就够了。但是如果想做事情,那么什么东西都不大好用。

慢慢写逻辑吧。

客户端怎么办?

老老实实看文档,看看怎么发送请求,怎么处理错误。

自己写 URL,慢慢连接吧。多试试。

每个平台自己开发客户端。

我之前用过一个订阅系统,有官方的 PHP、Ruby、Python、.NET、iOS、Android、Java 客户端,还有社区的 Go 和 NodeJS 客户端。

一个客户端就有一个 GitHub 仓库。一大串 commit、issue 和 PR。自己的用例,架构差不多是 ActiveRecord 和 RPC 代理那样。我们在开发各种连接器上浪费了多少时间?

结论

过去,各种语言的工作流差不多一样:向某个地方发送输入数据,获取输出数据。一直没什么问题。

REST 呢?鸡同鸭讲。前脚赞扬 HTTP 标准,后脚就开始瞎写。

微服务开始流行了:但为什么用网络把各个库连起来的事情这么难?

肯定有人开始喷我,给我丢点代码,告诉我 REST 可以在任意的数据上进行操作,就像当年的超链接那样。或者告诉我人丑得多读书,我没搞明白 REST。

我不管这些:没用的技术就是垃圾。我几个小时能用 RPC 写好的东西现在几周都写不完,到处是破绽。开发不是灵机一动的事情。

RPC 可以完成 99% 的工作,各种旧方法虽然不完善但能奏效。在 HTTP 上包一层 REST 纯属浪费时间。

REST 嘴上说简单,其实复杂;

REST 嘴上说稳定,其实脆弱;

REST 嘴上说互用,其实破碎。

REST 就是新时代的 SOAP。

结语

未来还是可以展望的:还有很多其他的协议,无论是二进制还是文本、有无 schema、利用 HTTP2,等等。我们不能被困在 Web 的石器时代。

针对上述内容的讨论

上述内容一出立刻引发了讨论狂潮。

Phil Sturgeon 称,RPC 和 REST 并不冲突。

RPC 有自己的用处,而 REST 也可以利用 JSON-API 或 OData 格式。在必要时,也可以使用传统的 API 定义方法——能奏效就是好的。

Phil 认为,HTTP 错误代码不能代替错误处理——使用 API 的双方必须规范文档并逐个处理错误。RPC 也是一样。

Phil 说,REST API 更直观,比 RPC 需要的文档量更少,因为有很多约定俗成的东西。

对于更新冲突问题,Phil 认为 PUT 不解决这个问题:服务器的状态是 PUT 的最后一个指示。而 PATCH 有自己的 RFC 6902,和其他标准没有关系。

关于 DELETE 的内容问题,Phil 指出,RFC 7231 中没有对这个问题进行定义:服务器可以选择接受一个有内容的 DELETE 请求。

至于误用缓存问题,Phil 认为这不是 REST 的问题,而且缓存是不可避免的。正确使用 HTTP 201 可以更好的表达“请求已经成功,请稍候”这个概念。

关于“滥用 400 错误”,Phil 觉得这是约定俗成的:不存在一种办法让客户端不需要读文档就可以处理错误。

原作者认为提出 C-S 架构是吹嘘:Phil 反驳说,RPC 很多时候分不清这种问题。

提到“通用性”问题,Phil 表达了不同的观点。Phil 认为,这个通用性是指使用者不应该在 REST 内部再搞一个 RPC 或 GraphQL。

至于 Session,Phil 觉得这种技术决策是绝对错误的:一个 token 会方便的多,而且可以扩展。Session 在单机还可以,开负载均衡就是灾难。

关于缓存问题,Phil 强调,memcache 这种缓存和 HTTP 缓存完全不同。所以需要缓存的数据必须正确设置 HTTP 头,方便中途缓存;如果一定不需要缓存,那么也可以强行刷新缓存。

作者认为 REST 有 N+1 请求问题,Phil 说写上需要请求的具体内容即可解决。而且现在流行 HTTP/2,多发请求也不会造成很大的压力。

至于 curl 难写,Phil 说,难道现在还有人不用 Postman 吗?

至于客户不需要预先知道服务器信息,Phil 强调,REST 的目的从来不是这个。

自己手写客户端?Phil 表示使用 OpenAPI 可以自动生成。

最后,Phil 建议读者研究 JSON Hyper-Schema——这个协议可以解决上文中的大部分问题。

查看原文

https://medium.freecodecamp.org/rest-is-the-new-soap-97ff6c09896d

https://philsturgeon.uk/api/2017/12/18/rest-confusion-explained/

感谢薛命灯对本文的审校。