你真的知道你喜欢REST而不是RPC的原因吗?
本文讨论的内容主要是请求风格,所以本文中所说RPC侧重于HTTP请求风格,而非java中的RPC设计模式。
有关REST和RPC的讨论或争论一直活跃在各个技术角落,最近也关注了不少,看了很多人的看法之后,我意识到这个问题可以帮助我照亮自己的知识死角:为什么我喜欢REST的请求风格(资源导向)比RPC(操作导向)多一点呢? 是因为RPC的请求风格天生邪恶吗? 还是REST就是灵丹妙药?
两种请求风格长分别长什么样子
在比较这两种请求风格之前,让我们看看他们究竟长什么样子。
HTTP 请求
RPC和REST都是使用HTTP协议(如果你对rpc有其他认识,你可以忽略这句话)。http协议就是一个request/response协议。
一个基本的HTTP请求包含如下:
- 一个动词(或叫方法)
- 一个资源 (或叫endpoint)
每个HTTP动词又有如下特点:
- 表示一个意思。
- 不一定是幂等的。至于幂等是什么鬼?一般情况下如果向服务器发送该方法的多个相同请求的预期效果与对单个这样的请求的效果相同,则请求方法被认为是“幂等的”。
- 不一定是安全的。有可能是安全的,也有可能是不安全的: 如果request方法的定义语义本质上是只读的,那么就被认为是“安全的”。
- 可以被缓存或者不可以,cacheable or not。
上面这个表格中只是展示了RPC和REST API中常用的几个HTTP动词
RPC: 一个基于操作的请求风格
RPC首字母缩略词有很多含义,远程过程调用(Remote Procedure Call)也有很多的形式。
在这篇文章中,当我谈论RPC我们一般都指的是:你的GET或POST方法是一个什么操作。
使用这种类型的RPC,您可以通过HTTP作为传输协议来操作数据。
就我目前所知,对于这种风格基本上没什么明确的约定和规则。
- 端点(或叫资源)包含要调用的操作的名称。
- 这种风格的API基本上只使用两个http动词,那就是你熟悉的GET和POST。
GET /someoperation?data=anId
POST /anotheroperation
{
"data":"anId";
"anotherdata":"another value"
}
那么人们是怎么选择GET还是POST呢?
- 对于那些比较关心HTTP协议的人来说,这种类型的API往往使用GET来进行不修改任何内容的操作,而POST则用于其他情况。
- 对于那些不太在意HTTP协议的人来说,这种类型的API往往使用GET来执行不需要太多参数的操作,而POST则用于其他情况。
- 那些彻底不关心http动词或压根不知道的人来说,就会在GET和POST之间随机选择或总是使用POST。这种情况也是大多数情况,至少在国内来说。
REST: 一种基于资源(resource)的请求风格
我不会详细解释到底什么是REST,你可以阅读Roy Fielding的论文和REST cookbook的更多细节。
为了聚焦本文的主题,我们长话短说:使用REST API,你是将数据作为资源(resource),你通过HTTP协议使用正确的HTTP动词操作,注意,是正确的动词:
- 端点上包含你要操作的资源。
- 许多人喜欢使用CRUD类比来解释REST请求原则。 HTTP动词表示你要怎么操作这个资源(创建/读取/更新/删除)。
GET /someresources/anId
PUT /someresources/anId
{"anotherdata":"another value"}
上例子
下面是分别用RPC和REST两种方式来编写的API:
RPC和REST大决战
既然大家都在一个劲的争论到底谁好,或者REST就是好,RPC就是不好,等等。为了能把问题说清楚,我们试图寻找一些比较指标,然后对二者进行一个全方位的对比,看最后是谁胜出?
下面是我们列举的一些指标,来比较这两种风格:
- 颜值
- 设计性
- API 定义语言
- 可预测性和语义
- 超媒体性
- 可缓存性
- 可用性
颜值
看起来两种风格都可以设计出丑的API,也都可以设计出美的API。平手。
设计性
设计RPC API看起来更容易:
当你处理一个现有的系统时,因为它通常面向操作,所以基于RPC的API与之天然匹配。但RPC API的设计需要设计人员严格的实现一致性API,因为实际上它没有什么约束。完全依赖于设计人员的一致性执行能力。
如果你主要是处理数据,REST API可能更容易一些。
但某些情况下,设计一个REST API似乎比RPC更难一点,因为它给你定了一个框框,让你实现一致的API,让你必需依赖于资源,而不是操作。
这两个都需要去处理命名的一致性。
这两种风格都依赖于具体项目的具体情况,所以真的没法分辨哪个设计起来更容易一些。
所以,平手。
API 定义语言
你可以使用例如Swagger, RAML or blueprint等这些api 定义语言来很好地描述这两种风格。
平手。
可预测性和语义
使用RPC,语义大部分时候是依赖在端点上的,并且没有对其含义的全局共享理解,什么意思呢?就是没有一个规范和约定。 例如,删除一个项目:
- GET (or POST) /deleteItem?itemId=456
- GET (or POST) /removeIt?itemId=456
- GET (or POST) /trash?itemId=456
还有比如resign操作,你可以命名成下面任何一个似乎都没什么问题:
- POST (or GET) /resign
- POST (or GET) /goodbye
- POST (or GET) /seeya
使用RPC,您依赖于人类对端点的含义的理解来理解它的作用,因此您可以对调用此端点时发生的事情进行精细的人工可读描述。
使用REST,语义依赖主要依赖于HTTP动词。 动词的语义是全局共享的,约定好的。 删除项目的唯一方法是:
- DELETE /items/456
如果用户想注销,您可以这样做:
- DELETE /users/1234
REST比RPC更可预测,因为它依赖于HTTP动词的共享语义。 你不知道具体发生了什么,但你大概知道自己要做什么。
本局 REST 胜出。
超媒体性
这两种风格都是使用的HTTP的请求,所以都可以设计出超媒体(hypermedia)的API。
平手。
可缓存性
我们经常可以在网上看到选择REST的一个致命的理由就是http 缓存。
但看了HTTP RFC后,我不同意这个论点(也许我理解的不够深刻)。 当然,如果你的RPC API所有请求都使用POST,缓存处理起来可能有点棘手。 如果你使用GET和POST的话,你的RPC API也将能够获得与REST API相同的缓存能力。
所以,这局平手!
可用性
从开发人员的角度来看,两种样式都使用HTTP协议,因此RPC和REST请求之间基本没有区别。
平手。
总比分
指标 |
胜出方 |
---|---|
颜值 |
平手 |
设计性 |
平手 |
API 定义语言 |
平手 |
可预测性和语义 |
REST |
超媒体性 |
平手 |
可缓存性 |
平手 |
可用性 |
平手 |
REST真的就赢了吗?
REST胜出要归结于“可预测性和语义”这一项指标。
那么,是不是就可以此断言基于资源比基于操作好吗?
No.
RPC和REST各有利弊,各有价值(我真的不想说出如此没有性格的话)。 你甚至可以在单个API中混合使用这两种方法。虽然我并不想得出这样的结论,但根据上面的分析确实是这样。
具体环境,这是关键。 没有灵丹妙药的解决方案,也没必要盲目去追随时尚,你总是必须在一个具体环境中去思考,用我们程序猿的话就是“上下文”, Context,在选择解决方案时必须务实。
至少,我现在知道为什么我喜欢基于资源的这种模式(REST)了:其可预测性以及充分利用HTTP协议特性。
你呢?
最后说一句:在如今这个功能编程还是主流的时代,基于操作的请求风格也是有其现实意义的,但在微服务时代(或者说在微服务场景下)呢?
- Oracle 12c 多租户专题|隔离PDB的磁盘IO
- golang 裸写一个pool池控制协程的大小
- 2014---多校训练2(ZCC Loves Codefires)
- 完整的golang 多协程+信道 任务处理示例
- 2014---多校训练一(A Couple doubi)
- hdu----(2586)How far away ?(DFS/LCA/RMQ)
- Golang控制goroutine的启动与关闭
- spring-boot-starter-swagger迎新伙伴支持,加速更新进度(1.3.0.RELEASE)
- poj----(1470)Closest Common Ancestors(LCA)
- 测试一下golang协程资源占有率
- poj----1330Nearest Common Ancestors(简单LCA)
- fasthttp中的协程池实现
- Oracle 12c R2版本 Application Containers 特性(二)
- go sync.Mutex 设计思想与演化过程 --转
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 第十五章 并发版爬虫第二版 -- 完结
- 第十六章 分布式爬虫--准备工作
- go 搭建并行处理管道
- 新一代基于大数据的管理信息系统(MIS)报表需求开发
- 3. docker-compose实战--ghost app
- 2.1 Kubernetes--Pod
- 3. Kubernetes集群安装
- macOS VirtualBox 桥接模式 设置静态ip 且能和联网
- 重新初始化k8s master节点
- 5.k8s基本命令汇总
- 6. k8s + jenkins 实现持续集成(完)
- 7. 复制k8s Node节点 并重新初始化k8s-nodes2节点 (k8s连载)
- 8.k8s连载--重新生成k8s token(kubeadm join报错及解决)
- 3. dcoker容器的命令
- 4. 镜像的原理