既然有 HTTP 协议,为什么还要有 RPC?

作者公众号:小白 debug 原文链接


我想起了我刚工作的时候,第一次接触 RPC 协议,当时就很懵,我 HTTP 协议用的好好的,为什么还要用 RPC 协议?
于是就到网上去搜。

不少解释显得非常官方,我相信大家在各种平台上也都看到过,解释了又好像没解释,都在用一个我们不认识的概念去解释另外一个我们不认识的概念,懂的人不需要看,不懂的人看了还是不懂。

这种看了,又好像没看的感觉,云里雾里的很难受,我懂。

为了避免大家有强烈的审丑疲劳,今天我们来尝试重新换个方式讲一讲。

从 TCP 聊起#

作为一个程序员,假设我们需要在 A 电脑的进程发一段数据到 B 电脑的进程,我们一般会在代码里使用 socket 进行编程。

这时候,我们可选项一般也就 TCP 和 UDP 二选一。TCP 可靠,UDP 不可靠。 除非是马总这种神级程序员(早期 QQ 大量使用 UDP),否则,只要稍微对可靠性有些要求,普通人一般无脑选 TCP 就对了。

类似下面这样。

fd = socket(AF_INET,SOCK_STREAM,0);

其中 SOCK_STREAM ,是指使用字节流 传输数据,说白了就是 TCP 协议。

在定义了 socket 之后,我们就可以愉快的对这个 socket 进行操作,比如用 bind() 绑定 IP 端口,用 connect() 发起建连。

握手建立连接流程

在连接建立之后,我们就可以使用 send() 发送数据,recv() 接收数据。

光这样一个纯裸的 TCP 连接,就可以做到收发数据了,那是不是就够了?

不行,这么用会有问题。

使用纯裸 TCP 会有什么问题?#

八股文常背,TCP 是有三个特点,面向连接、可靠、基于字节流。

Laravel

这三个特点真的概括的非常精辟,这个八股文我们没白背。

每个特点展开都能聊一篇文章,而今天我们需要关注的是基于字节流这一点。

字节流可以理解为一个双向的通道里流淌的数据,这个数据其实就是我们常说的二进制数据,简单来说就是一大堆 01 串。纯裸 TCP 收发的这些 01 串之间是没有任何边界的,你根本不知道到哪个地方才算一条完整消息。

二进制字节流

正因为这个没有任何边界的特点,所以当我们选择使用 TCP 发送 “夏洛” 和” 特烦恼” 的时候,接收端收到的就是 “夏洛特烦恼” ,这时候接收端没发区分你是想要表达 “夏洛”+” 特烦恼” 还是 “夏洛特”+” 烦恼”

消息对比

这就是所谓的粘包问题,之前也写过一篇专门的文章聊过这个问题。

说这个的目的是为了告诉大家,纯裸 TCP 是不能直接拿来用的,你需要在这个基础上加入一些自定义的规则,用于区分消息边界

于是我们会把每条要发送的数据都包装一下,比如加入消息头,消息头里写清楚一个完整的包长度是多少,根据这个长度可以继续接收数据,截取出来后它们就是我们真正要传输的消息体

消息边界长度标志

而这里头提到的消息头,还可以放各种东西,比如消息体是否被压缩过和消息体格式之类的,只要上下游都约定好了,互相都认就可以了,这就是所谓的协议

每个使用 TCP 的项目都可能会定义一套类似这样的协议解析标准,他们可能有区别,但原理都类似

于是基于 TCP,就衍生了非常多的协议,比如 HTTP 和 RPC。

HTTP 和 RPC#

我们回过头来看网络的分层图。

四层网络协议

TCP 是传输层的协议,而基于 TCP 造出来的 HTTP 和各类 RPC 协议,它们都只是定义了不同消息格式的应用层协议而已。

HTTP 协议Hyper Text Transfer Protocol),又叫做超文本传输协议。我们用的比较多,平时上网在浏览器上敲个网址就能访问网页,这里用到的就是 HTTP 协议。

HTTP调用

RPCRemote Procedure Call),又叫做远程过程调用。它本身并不是一个具体的协议,而是一种调用方式

举个例子,我们平时调用一个本地方法就像下面这样。

res = localFunc(req)

如果现在这不是个本地方法,而是个远端服务器暴露出来的一个方法 remoteFunc ,如果我们还能像调用本地方法那样去调用它,这样就可以屏蔽掉一些网络细节,用起来更方便,岂不美哉?

res = remoteFunc(req)

RPC可以像调用本地方法那样调用远端方法

基于这个思路,大佬们造出了非常多款式的 RPC 协议,比如比较有名的 gRPCthrift

值得注意的是,虽然大部分 RPC 协议底层使用 TCP,但实际上它们不一定非得使用 TCP,改用 UDP 或者 HTTP,其实也可以做到类似的功能。

基于TCP协议的HTTP和RPC协议

到这里,我们回到文章标题的问题。

既然有 HTTP 协议,为什么还要有 RPC?

其实,TCP70 年代出来的协议,而 HTTP90 年代才开始流行的。而直接使用裸 TCP 会有问题,可想而知,这中间这么多年有多少自定义的协议,而这里面就有 80 年代出来的 RPC

所以我们该问的不是既然有 HTTP 协议为什么要有 RPC,而是为什么有 RPC 还要有 HTTP 协议。

那既然有 RPC 了,为什么还要有 HTTP 呢?#

现在电脑上装的各种 联网 软件,比如 xx 管家,xx 卫士,它们都作为客户端(client) 需要跟服务端(server) 建立连接收发消息,此时都会用到应用层协议,在这种 client/server(c/s) 架构下,它们可以使用自家造的 RPC 协议,因为它只管连自己公司的服务器就 ok 了。

但有个软件不同,浏览器(browser) ,不管是 chrome 还是 IE,它们不仅要能访问自家公司的服务器(server) ,还需要访问其他公司的网站服务器,因此它们需要有个统一的标准,不然大家没法交流。于是,HTTP 就是那个时代用于统一 browser/server (b/s) 的协议。

也就是说在多年以前,HTTP 主要用于 b/s 架构,而 RPC 更多用于 c/s 架构。但现在其实已经没分那么清了,b/s 和 c/s 在慢慢融合。 很多软件同时支持多端,比如某度云盘,既要支持网页版,还要支持手机端和 pc 端,如果通信协议都用 HTTP 的话,那服务器只用同一套就够了。而 RPC 就开始退居幕后,一般用于公司内部集群里,各个微服务之间的通讯。

那这么说的话,都用 HTTP 得了,还用什么 RPC?

仿佛又回到了文章开头的样子,那这就要从它们之间的区别开始说起。

HTTP 和 RPC 有什么区别#

我们来看看 RPC 和 HTTP 区别比较明显的几个点。

服务发现#

首先要向某个服务器发起请求,你得先建立连接,而建立连接的前提是,你得知道 IP 地址和端口。这个找到服务对应的 IP 端口的过程,其实就是服务发现

在 HTTP 中,你知道服务的域名,就可以通过 DNS 服务去解析得到它背后的 IP 地址,默认 80 端口。

RPC 的话,就有些区别,一般会有专门的中间服务去保存服务名和 IP 信息,比如 consul 或者 etcd,甚至是 redis。想要访问某个服务,就去这些中间服务去获得 IP 和端口信息。由于 dns 也是服务发现的一种,所以也有基于 dns 去做服务发现的组件,比如 CoreDNS

可以看出服务发现这一块,两者是有些区别,但不太能分高低。

底层连接形式#

以主流的 HTTP1.1 协议为例,其默认在建立底层 TCP 连接之后会一直保持这个连接(keep alive),之后的请求和响应都会复用这条连接。

而 RPC 协议,也跟 HTTP 类似,也是通过建立 TCP 长链接进行数据交互,但不同的地方在于,RPC 协议一般还会再建个连接池,在请求量大的时候,建立多条连接放在池内,要发数据的时候就从池里取一条连接出来,用完放回去,下次再复用,可以说非常环保。

connection_pool

由于连接池有利于提升网络请求性能,所以不少编程语言的网络库里都会给 HTTP 加个连接池,比如 go 就是这么干的。

可以看出这一块两者也没太大区别,所以也不是关键。

传输的内容#

基于 TCP 传输的消息,说到底,无非都是消息头 header 和消息体 body

header 是用于标记一些特殊信息,其中最重要的是消息体长度

body 则是放我们真正需要传输的内容,而这些内容只能是二进制 01 串,毕竟计算机只认识这玩意。所以 TCP 传字符串和数字都问题不大,因为字符串可以转成编码再变成 01 串,而数字本身也能直接转为二进制。但结构体呢,我们得想个办法将它也转为二进制 01 串,这样的方案现在也有很多现成的,比如 json,protobuf

这个将结构体转为二进制数组的过程就叫 序列化,反过来将二进制数组复原成结构体的过程叫反序列化

序列化和反序列化

对于主流的 HTTP1.1,虽然它现在叫超文本协议,支持音频视频,但 HTTP 设计初是用于做网页文本展示的,所以它传的内容以字符串为主。header 和 body 都是如此。在 body 这块,它使用 json 来序列化结构体数据。

我们可以随便截个图直观看下。

HTTP报文

可以看到这里面的内容非常多的冗余,显得非常啰嗦。最明显的,像 header 里的那些信息,其实如果我们约定好头部的第几位是 content-type,就不需要每次都真的把”content-type” 这个字段都传过来,类似的情况其实在 body 的 json 结构里也特别明显。

而 RPC,因为它定制化程度更高,可以采用体积更小的 protobuf 或其他序列化协议去保存结构体数据,同时也不需要像 HTTP 那样考虑各种浏览器行为,比如 302 重定向跳转啥的。因此性能也会更好一些,这也是在公司内部微服务中抛弃 HTTP,选择使用 RPC 的最主要原因。

HTTP原理

RPC原理

当然上面说的 HTTP,其实特指的是现在主流使用的 HTTP1.1HTTP2 在前者的基础上做了很多改进,所以性能可能比很多 RPC 协议还要好,甚至连 gRPC 底层都直接用的 HTTP2

那么问题又来了。

为什么既然有了 HTTP2,还要有 RPC 协议?#

这个是由于 HTTP2 是 2015 年出来的。那时候很多公司内部的 RPC 协议都已经跑了好些年了,基于历史原因,一般也没必要去换了。

总结#

  • 纯裸 TCP 是能收发数据,但它是个无边界的数据流,上层需要定义消息格式用于定义消息边界。于是就有了各种协议,HTTP 和各类 RPC 协议就是在 TCP 之上定义的应用层协议。

  • RPC 本质上不算是协议,而是一种调用方式,而像 gRPC 和 thrift 这样的具体实现,才是协议,它们是实现了 RPC 调用的协议。目的是希望程序员能像调用本地方法那样去调用远端的服务方法。同时 RPC 有很多种实现方式,不一定非得基于 TCP 协议

  • 从发展历史来说,HTTP 主要用于 b/s 架构,而 RPC 更多用于 c/s 架构。但现在其实已经没分那么清了,b/s 和 c/s 在慢慢融合。 很多软件同时支持多端,所以对外一般用 HTTP 协议,而内部集群的微服务之间则采用 RPC 协议进行通讯。

  • RPC 其实比 HTTP 出现的要早,且比目前主流的 HTTP1.1 性能要更好,所以大部分公司内部都还在使用 RPC。

  • HTTP2.0 在 HTTP1.1 的基础上做了优化,性能可能比很多 RPC 协议都要好,但由于是这几年才出来的,所以也不太可能取代掉 RPC。

最后留个问题吧,大家有没有发现,不管是 HTTP 还是 RPC,它们都有个特点,那就是消息都是客户端请求,服务端响应。客户端没问,服务端肯定就不答,这就有点僵了,但现实中肯定有需要下游主动发送消息给上游的场景,比如打个网页游戏,站在那啥也不操作,怪也会主动攻击我,这种情况该怎么办呢?

悲观者永远正确,乐观者永远前行。
本帖已被设为精华帖!
本帖由系统于 2年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 24

那以后新项目是不是直接用 HTTP2.0,不用 RPC 都可以了

2年前 评论

让我想起了 CentOS 源码编译

2年前 评论

写的不错 :+1:
最后一个问题答案是:
php 客户端也挂载一个服务进程 websocket 堵塞进行监听 或者 html 前端 new ws () 进行一个监听 就可以实现客户端没问 服务端也能进行发送消息了 这里的客户端不清楚层面指的是 php 还是 html 那个层面 ws 协议双工通信 类似于聊天系统一样
也不知道是不是这个意思

2年前 评论
cevin 2年前
pndx

还没在 php 上用过 rpc,mark

2年前 评论

写的不错,学习一下~

2年前 评论
wonbin

总结一下 因为 http1.1 的性能问题有了 rpc 的一席之地; 虽然 http2 性能优于 rpc ,但是 对于许多公司而言 rpc 已经用了许多时间了,一时半会儿也替代不了

2年前 评论
MArtian (楼主) 2年前

比我只看过的其他文章讲的更通俗易懂。我有问题请教。 不同的应用之前请求接口直接用 curl 发起网络请求就可以了。 在 SpringBoot 项目里,服务之间调用用的是 Feign。请问这种是 http 还是 rpc? 其实我的疑惑就是我知道服务之间用 http 的话使用 curl 来实现,用 rpc 的话不清楚具体的实现是什么。

2年前 评论
MArtian (楼主) 2年前

http 是传输应用层的协议,rpc 是程序的调用方式吧,两者还是有差别的,rpc 也可以通过 http 协议调用

2年前 评论
Sparkfly 2年前

文章写的不错,但是标题写错了。RPC 已经有四十多年的历史了,很古老了。

2年前 评论

不管如何,服务器想要推送数据到客户端,他们之间必须要建立连接。

既然建立连接了,那么谁发数据不是谁发都行嘛。

当然如果服务器 / 客户机并不是你编写的,那么首先分析能提供什么样的服务,如果没有提供服务器主动推送,则需要借助新的媒体,例如新的 TCP 连接,共享存储区,这样形式就很多了。

2年前 评论

通俗易懂,写的不错,好文章,好科普 👍

2年前 评论

现在就职的公司用的是 grpc,其实再向下发包的时候还是 http 的 post

2年前 评论

我站 http 协议。 http 协议里 header 也可以剔除大部分,比如 host,userAgent 都可以删除。

从完整的工程来看,链路追踪、认证等调用方之类的额外信息也要加的,所以加加减减到最后,http 传递的数据也不一定比什么 RPC 多多少,从日志角度上看检索还比 rpc 方案简单而且可以选择的方案多。

同时服务器大部分都是万兆网卡 / 万兆交换机,能在传输层吃满的场景极少,如果是流媒体场景,数据体积都远远大于协议信息了。文本传输就算真的打到带宽瓶颈,基于 http 约个 json rpc 规范,对 json 做个 zip/gzip 压缩就行了,大部分 cpu 都有专用指令做压缩 / 解压缩加速。反而各种 rpc 不同语言客户端质量不一致搞不好还给项目留下一堆技术债务。

从部署角度上看,基于 http 做灰度部署很简单,比如在 header 传递调用方信息像应用 id、版本、甚至用户 id,同时在网关层(Traefik、Kong、Nginx Unit、APISIX 等 Gateway)都有强大的条件路由,可以做到热更新规则并根据 header 的特定信息将流量指向到特定的后端节点,对项目上线前的灰度测试阶段很友好。

2年前 评论

最后一个问题的答案就是 webocket 长链接 + 事件广播消息推送啊。 不过就博主的例子来说, 怪既然能自动打你,那么客户端应该是事先准备了一套事件机制, 主动向服务器发请求了。 即并不是玩家不操作, 客户端就不会发请求。

1年前 评论
MArtian (楼主) 1年前
徵羽宫 (作者) 1年前