分享一位朋友在面试虾皮时遇到的关于RPC的基础问题。

从TCP协议谈起

假设我们是一名程序员,需要将一段数据从电脑A的进程发送到电脑B的进程,通常会在代码中使用socket进行编程。在这种情况下,我们的选择主要是TCP和UDP。TCP协议以其高可靠性而受到广泛使用,而UDP则是相对不太可靠的选项。除非你是像马化腾那样的高级程序员(早期的QQ大量使用UDP),否则一般情况下,大家都选择TCP来确保数据的可靠性。

例如,下面是基本的TCP socket创建代码:

fd = socket(AF_INET, SOCK_STREAM, 0);

这里的SOCK_STREAM表示采用字节流传输数据,也就是使用TCP协议

定义了socket后,我们便可以进行各种操作,例如使用bind()来绑定IP和端口,使用connect()发起连接。

图片

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

然而,单纯依靠一个原生的TCP连接是否足够呢?答案是:不够,这样做会出现一些问题。

关于纯裸TCP的潜在问题

我们经常听到八股文中提到,TCP协议有三个特点:面向连接可靠性、以及基于字节流

图片

TCP的特点

这三点确实非常准确地总结了TCP协议的本质。每个特点都可以独立展开讨论,而今天我们主要关注的是基于字节流这一特性。

字节流可以看作是在一个双向通道中流动的二进制数据,即01串。在纯裸TCP中,发送和接收的这些01串之间并没有任何边界,这意味着你无法确定何处才是完整消息的结束。

图片

01二进制字节流

正因为缺乏边界,当我们通过TCP发送**"夏洛""特烦恼"时,接收方收到的内容可能会被错误理解为"夏洛特烦恼"。此时,接收端无法分辨这是否是"夏洛"+"特烦恼"还是"夏洛特"+"烦恼"**。

图片

消息对比

这就是所谓的粘包问题。我之前也写过一篇专门讨论该问题的文章。提到这个目的,是为了告诉大家,单纯依靠TCP是无法直接使用的,我们需要在其基础上添加一些自定义规则来区分消息的边界

因此,我们会将每条要发送的数据进行包装,例如添加消息头,在消息头中明确标示出完整包的长度,以便接收方根据长度继续接收数据,并截取出我们真正想要传输的消息体

图片

消息边界长度标志

在提到的消息头中,还可以包含多种信息,例如消息体是否被压缩以及消息体的格式等。只要上下游的双方达成一致,互相认可即可,这就是所谓的协议

每个使用TCP的项目可能会定义一套类似的协议解析标准,它们可能各有不同,但原理却是相似的。

因此,基于TCP协议,衍生出了众多协议,包括HTTP和RPC等。

HTTP与RPC的关联

RPC实际上是一种调用方式

我们回到网络分层图。

图片

四层网络协议

TCP是传输层协议,而基于TCP构建的HTTP和各种RPC协议,实际上只是定义了不同消息格式的应用层协议

HTTPHyper Text Transfer Protocol)协议是我们常用的协议,通常在浏览器中输入网址来访问网页时,就是在用HTTP协议。

图片

HTTP调用

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

例如,我们在调用一个本地方法时,通常会像下面这样:

res = localFunc(req)

如果这个方法不是本地的,而是一个远程服务器暴露的remoteFunc方法,而我们仍然能够像调用本地方法一样去调用它,那么这无疑会减少我们对网络细节的关注,使得调用过程更加方便。

res = remoteFunc(req)

图片

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

基于这种思路,许多技术大咖们开发出了多种RPC协议,如知名的gRPCThrift

值得注意的是,虽然大多数RPC协议底层使用TCP,但实际上并不一定非得使用TCP,使用UDP或HTTP同样可以实现类似的功能。

回到我们开始讨论的问题。

那么,既然有了RPC,为什么仍然需要HTTP呢?

TCP协议是在70年代诞生的,而HTTP协议是在90年代才开始流行。由于直接使用裸TCP会存在问题,想必这几十年间也有很多定制协议,而RPC就是在80年代产生的。

因此,问题应当是:既然有RPC,为什么还需要HTTP协议?

如今的网络软件,如某些管理工具和安全软件,作为客户端(Client),需要与服务器(Server)建立连接以收发消息,通常会使用应用层协议。在这种Client/Server (C/S)架构下,它们可以使用自家设计的RPC协议,因为只需连接自家公司的服务器即可。

但有一种软件例外,那就是浏览器(Browser),无论是Chrome还是IE,它们不仅需要访问自家公司的服务器(Server),还需要访问其他公司的网站服务器,因此必须采用统一的标准,否则无法进行交互。HTTP便是在那个时代为**Browser/Server (B/S)**架构制定的统一协议。

简言之,早期HTTP主要用于B/S架构,而RPC则多用于C/S架构。但随着技术的发展,两者之间的界限逐渐模糊。许多软件现在支持多端,例如某度云盘,既需要支持网页版,也需要支持手机端和PC端,若通信协议统一使用HTTP,服务器便可只需维护一套协议。而RPC则逐渐被用于公司内部集群中的各个微服务之间的通信。

那么,有人可能会问:都用HTTP还要RPC干嘛?

这又回到了文章开头的问题。这就需要从两者之间的区别说起。

HTTP与RPC的区别

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

服务发现

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

HTTP中,只需知道服务的域名,就可以通过DNS服务解析出其背后的IP地址,默认使用80端口

RPC的情况有所不同,通常会有专门的中间服务来保存服务名和IP信息,比如Consul、Etcd、Nacos、ZooKeeper,甚至是Redis。要访问某个服务,需向这些中间服务请求获取IP和端口信息。由于DNS也是服务发现的一种方式,因此也有基于DNS进行服务发现的组件,比如CoreDNS

可以看出,在服务发现这一块,二者有一定区别,但并不能简单地分出高低。

底层连接形式

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

RPC协议也通过建立TCP长连接进行数据交互,但与HTTP不同的是,RPC协议还会建立一个连接池,在高请求量时,维护多条连接在池中,发数据时从池中取出一条连接,使用后再放回去,以便下次复用,可以说相当环保。

图片

连接池

由于连接池有助于提升网络请求性能,许多编程语言的网络库里也会给HTTP添加连接池。例如,Go语言就实现了这一点。

可以发现,在这一点上,两者并没有太大区别,因此这也不是关键所在。

传输内容

基于TCP传输的消息,无非是消息头Header消息体Body

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

Body则是我们真正需要传输的内容,而在计算机中,这些内容只能是二进制的01串,因为计算机只理解这种形式。因此,TCP传输字符串和数字没有问题,因为字符串可以编码为二进制,而数字本身也可以直接转换为二进制。但对于结构体数据,我们需要将其转为二进制01串,这方面现在有很多现成的解决方案,比如JSONProtocol Buffers (Protobuf)

将结构体转换为二进制数组的过程称为序列化,反过来再将二进制数组复原为结构体的过程叫反序列化

图片

序列化与反序列化

对于主流的HTTP1.1,尽管它现在被称为超文本协议,支持音频和视频内容,但HTTP的初衷是为了网页文本的展示,因此它传输的内容以字符串为主,Header和Body的内容也都是如此。在Body部分,它通常使用JSON格式来对结构体数据进行序列化

我们可以截取一段HTTP报文来直观地观察其内容。

图片

HTTP报文

从中可以看到,内容中存在较多冗余,显得相当繁琐。例如,Header中的一些信息,若我们约定好头部第几位是Content-Type,便无需每次都重复传递Content-Type这一字段,Body中的JSON结构同样存在类似的冗余。

而RPC由于定制化程度更高,可以采用体积更小的Protobuf或其他序列化协议来存储结构体数据,同时不需要考虑如HTTP那样的多种浏览器行为,比如302重定向等。这也是在公司内部微服务中,很多技术团队选择抛弃HTTP,转而使用RPC的主要原因。

图片

HTTP原理

图片

RPC原理

当然,前面提到的HTTP是指目前主流的HTTP1.1,而HTTP2在此基础上进行了许多改进,因此其性能可能甚至超过一些RPC协议,甚至连gRPC底层也直接使用HTTP2。

那么问题又来了。

为什么在有了HTTP2后,还需要RPC协议?

这一点主要是由于HTTP2是在2015年发布的,而众多公司内部的RPC协议在那之前便已经运行多年。因此,出于历史原因,通常也没有必要进行更换。

总结

  • 纯裸TCP能够收发数据,但它是一个无边界的数据流,上层需定义消息格式以确定消息的边界。因此,HTTP和各种RPC协议是在TCP之上定义的应用层协议。
  • RPC本质上并不是一个协议,而是一种调用方式,而像gRPC和Thrift这样的具体实现才算是协议,它们实现了RPC调用的协议,旨在让程序员能像调用本地方法一样调用远程服务方法。RPC存在多种实现方式,并不一定需要基于TCP协议
  • 从发展历史来看,HTTP主要用于B/S架构,而RPC更多用于C/S架构。然而,现在两者的界限正在逐渐模糊,许多软件同时支持多端,因此对外通常使用HTTP协议,而内部微服务之间则采用RPC协议进行通讯。
  • RPC实际上比HTTP出现得更早,且在大多数情况下,其性能优于当前主流的HTTP1.1,因此许多公司内部仍在使用RPC。
  • HTTP2在HTTP1.1的基础上进行了优化,其性能可能优于许多RPC协议,但由于其发布时间较晚,因此不可能完全取代RPC。