本文共 5571 字,大约阅读时间需要 18 分钟。
REST只是新时期的SOAP?来看看Pakal De Bonchamp是怎么说的,以及Phil Sturgeon的反击。以下内容经过编译,点击文末的链接可查看原文。
\\几年前我为一个大型通信公司开发了一套系统,需要在各种Web服务间通信,从老掉牙的系统或者商业伙伴的系统中获取数据。
\\整个事情乱成一锅粥:难懂的WSDL、不兼容的库、奇妙的bug……所以我们尽可能用RPC通信:XMLRPC或JSONRPC。
\\我们开发的第一个应用很简单,不过随着迭代的进行,终于可以支持各种dialect(包括Apache对XMLRPC的扩展),把Python的异常转换成错误代码,处理各种错误,记录请求日志,验证输入数据,等等。
\\几行代码就可以调用这些API,稍微封装一下就可以生成新的功能。应用间的通信也特别简单:系统管理员自己就可以完成这些工作。
\\然后REST出现了
\\REST重新定义了服务间的通信:RPC已死,未来是RESTful的。每个资源都有自己的URL,并通过HTTP协议进行操作。
\\然后呢?天塌地陷。
\\给个例子吧。以下是一个API,去掉了数据:
\\\createAccount(username, contact_email, password) -\u0026gt; account_id\addSubscription(account_id, subscription_type) -\u0026gt; subscription_id\sendActivationReminderEmail(account_id) -\u0026gt; null\cancelSubscription(subscription_id, reason, immediate=True) -\u0026gt; null\getAccountDetails(account_id) -\u0026gt; {full data tree}\\
再定义一些异常(例如,参数不对、参数缺失、工作流异常),抛出一些错误(例如,用户名已被使用),整个API就算完成了。
\\这个API简单易用、稳定,而且背后的状态机也很完善,用户无法进行非法操作(例如,用户不能修改账号的创建时间)。
\\用RPC写这个API需要几个小时时间
\\RESTful呢?
\\RESTful没有太多的标准和规范,只有“RESTful哲学”,每个点都可以拎出来讨论一番。
\\上面的功能怎么能直接映射到简单的CRUD操作?发送验证邮件是把“必须发送验证邮件”更新一下?还是创建一个“验证邮件”?在宽限期内取消订阅这个命令能不能用DELETE,过后能不能回滚?获取账户信息如何写成RESTful的?
\\资源怎么定义?是不难,但也得定义啊。
\\怎么用几个HTTP代码表示错误信息?
\\输入输出的格式是怎样的?怎么序列化?
\\HTTP动词、URL、请求、请求头和状态码的分界线在哪?
\\整件事情就是在重复造轮子,而且造的还不是好轮子。造轮子还需要一大堆文档,而且肯定违反RESTful原则。
\\为什么RESTful这么难用?
\\先让我们来看看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攻击吧。
\\实现功能容易,但是好的程序员会做错误处理。
\\HTTP有很多错误码,让我们来瞧瞧。
\\HTTP 404表示某个资源不存在,是不是很直观?但如果你没配好Nginx,你的API用户有可能把账号给删了,因为在他们看来,这些账号不存在啊。
\\HTTP 401表示用户没权限,这看起来是不是很棒?然而,如果你在浏览器里这么玩,你的用户有可能看见浏览器蹦出一个输入框让他们输入用户名和密码。
\\HTTP比RESTful的历史长得多,有很多约定俗成的东西。用HTTP状态码表示错误就像用牛奶瓶装剧毒废物:总有一天你会药死谁。
\\有些HTTP状态码是WebDAV专用的,有些是微软专用的,有些不知所云。最后大家开始瞎用状态码,什么HTTP 418(查查呗)什么的。或者,所有的错误全用HTTP 400,里面再套一层状态码。或者全用200,里面再写加上详细信息。
\\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——这个协议可以解决上文中的大部分问题。
\\查看原文:
\\ \\ \\感谢对本文的审校。
转载地址:http://uhjbx.baihongyu.com/