[关闭]
@cnbeining 2018-02-02T16:05:07.000000Z 字数 4917 阅读 62

REST应该进垃圾桶

API


note:


摘要

作者Pakal De Bonchamp

正文

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

整个事情乱成一锅粥:抽象WSDL,不兼容的库,奇妙的bug要多少有多少。所以我们尽可能用RPC通信。

我们开发的第一台服务器很简单脆弱;但是随着迭代,我终于可以支持各种语法(包括Apache自己定义的XMLRPC语法),把Python的异常转换成错误代码,处理各种错误,记录请求日志,验证输入数据等等等等等等等等。

几句话就可以连接新的API,加几行装饰器就可以支持各种输出。连接微服务也特别简单:网管就可以自己干,对软件而言这个过程是透明的。

然后REST出现了。

REST重新定义了服务间的沟通:RPC已死,未来是RESTful的。每个资源都有独立的URL,使用HTTP协议。

然后呢?天塌地陷。

REST有什么问题?

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

创建用户(用户名, email, 密码) -> 账户id
加入订阅(账户id, 订阅类型) -> 订阅id
发送验证email(账户id) -> null
取消订阅(订阅id, 理由, 立即=True) -> null
取账户信息(账户id) -> {数据}

定义一些异常后(例如,参数不对、参数缺失、工作流异常),再抛出一些重要的错误(例如,用户名已占用),整个API就好了。

API很简单易用稳定,背后的状态机很完善,用户做不出非法操作(例如,用户不能定义创建时间)。

用RPC写这个API:几个小时。

RESTful呢?

RESTful没有标准,没有定义,只有个“RESTful哲学”,每个地方都可以拎出来讨论,各种hack。

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

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

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

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

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

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

为什么RESTful这么难用?

我们看看REST的设计哲学。

REST的动词们

REST不是CRUD,REST用户们肯定不支持混用这两个概念。然后他们就开始研究创建的POST、取的GET、更新的PUT和PATCH,加上删除的DELETE了。

REST的意思是这几个东西就足够了。好像是的:“今天我对汽车座椅UPDATE我的身体,CREATE发动机点火,油箱自己DELETE”。仿佛多多少少看起来不怎么像人话?

就算简单化是好的,REST也没做好。为什么PUT、PATCH和DELETE在web表单上从不使用?因为这几个东西百害而无一利。读就是GET,写就算POST,这就够了。或者你不想被运营商劫持,那就只用POST呗。

如果用PUT更新资源呢?可以,但是你需要和GET取到的数据格式一样,包括一大堆只读数据(创建时间、最后更新时间,服务器上的什么token)。请问您是准备不管RESTful规则了?还是老老实实组装一个请求,PUT上去了服务器报409 Conflict,因为服务器上某个值变了?然后再GET一发,重新组装再PUT?还是你觉得服务器会忽略只读的参数?(服务器有可能忽略了,也有可能直接炸了哦)要是某些值根本就不能让你GET呢,例如,信用卡密码加有效期?RESTful根本处理不了这种只读的参数。

哦,别忘了如果有多个客户端,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是stateless的。嗯,有个数据库,但是不认识客户端?也不对,因为数据库保存了session和token,但是随便吧。这东西到底怎么比其他的协议高明了?

REST可以利用HTTP缓存。好吧,至少GET可以。但是本地缓存不够吗?想想一下ISP劫持,再考虑一下你的所作所为?或者某个Varnish没配置好,所以缓存没更新?这种系统就是不安全的:缓存虽好,但是把几个点用GET就得了。

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

REST兼容性好。是吗?那为什么还搞/v2/这种URL?API的兼容性不难,只需要好好设计:REST不怎么样。

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

REST简单到可以用curl一句话读取。curl可以发任何请求。GET很简单,POST火葬场。最后您不还得老老实实用Postman嘛。

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

那怎么办?

别考虑是不是正确了。把活干了是真的。

真正的问题是:需要写一个类似RESTful的API,怎么写最快?

服务器怎么办?

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

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

慢慢写逻辑吧。

客户端怎么办?

不怎么办。

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

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

大的web服务怎么办呢?

每 个 平 台 自 己 手 撸 客 户 端。

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

一个客户端一个Github repo。一大串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/

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注