YUKIPEDIA's blog

一个普通的XMUER

《Summer Pockets》久島鴎推し


RPC vs HTTP

目录

HTTP 协议RPC 协议是两种广泛使用的通信协议,它们在分布式系统、微服务架构和跨语言应用中扮演着重要角色。这两种协议有一个共同的特点:它们都是由 TCP 协议 衍生而来的。

从 TCP 聊起

在日常开发中,常常会遇到网络编程,比如我们需要在 A 电脑的进程发一段数据给 B 电脑的进程,我们一般都会在代码里使用 Socket 进行编程。

大多数情况下,可选项就两个:TCP 和 UDP 。这两个协议的区别简单来讲:TCP 可靠,UDP 不可靠。所以只要我们的程序对可靠性有些要求,无脑选 TCP 就对了。

类似下面这样:

fd = socket(AF_INET,SOCK_STREAM,0);

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

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

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

这样,我们就可以使用这样一个纯裸的 TCP 连接收发数据了。那这样是不是就够了呢?肯定不是,这么用还会出现一些问题。

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

首先,TCP 有三个特点:面向连接可靠、基于字节流。这里出现的问题主要是第三点:基于字节流。

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

image.png

正因为这个没有边界的特点,当我们选择使用 TCP 发送数据时,很可能会出现粘包问题

举个例子,我们发送 “但丁“ 和 ”真是意大利人” 这句话时, 接收端收到的就是 “但丁真是意大利人” ,这时候接收端就无法区分去发送端是想要表达 “但丁” + “真是意大利人” 还是 “但丁真” + “是意大利人” 。

因此,纯裸 TCP 是不能直接拿来用的,需要在这个基础上加入一些自定义的规则,用于区分消息边界

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

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

每个使用 TCP 的项目都可能会定义一套类似这样的协议解析标准,于是基于 TCP ,就衍生出了很多协议,比如 HTTP 和 RPC 。

HTTP 和 RPC

image.png

从上面的网络分层图可以看到,TCP 是传输层的协议,而基于 TCP 的 HTTP 和各类 RPC 协议,它们都只是定义了不同消息格式的应用层协议而已。

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

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

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

res = localFunc(req)

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

res = remoteFunc(req)

image.png

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

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

HTTP 和 RPC 有什么区别?

服务发现

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

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

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

底层连接形式

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

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

image.png

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

传输的内容

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

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

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

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

image.png

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

image.png

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

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

image.png

image.png

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

HTTP 和 RPC 的性能对比与应用场景

  1. 延迟
    • RPC:通常情况下,RPC 的延迟相对较低,因为它旨在实现远程过程调用,需要快速的响应时间,这使得 RPC 在一些实时性要求较高的场景中表现优越。
    • HTTP: HTTP 的延迟相对较高,特别是在建立连接、发送请求和接收响应的过程中。然而,HTTP 协议的延迟也受到网络条件和服务器响应时间的影响。
  2. 带宽利用率
    • RPC:由于RPC通常使用二进制格式进行数据传输,相对于文本格式,带宽利用率较高。这对于大量数据传输的情况下是有优势的。
    • HTTP: HTTP 通常使用文本格式,如 Json 或 XML ,这可能导致较高的带宽消耗。然而,HTTP/2 和 HTTP/3 引入了一些特性,如多路复用和头部压缩,以提高带宽利用率。
  3. 并发性能
    • RPC:一些 RPC 框架支持高度的并发性能,使得多个请求可以同时处理。这对于大规模分布式系统中的并发需求是关键的。
    • HTTP:HTTP/1.1 在一个连接上一次只能处理一个请求,但 HTTP/2 和 HTTP/3 引入了多路复用,允许多个请求同时在一个连接上进行,提高了并发性能。

从上面三点性能对比来看,HTTP 和 RPC 有各自不同的应用场景,简单总结来讲:

  • HTTP协议适用于简单的请求-响应通信、Web 应用、RESTful API、文件传输等,具有广泛的支持和易用性。
  • RPC协议适用于需要高效、低延迟和跨语言支持的分布式系统、微服务架构和高性能计算场景,特别是在多语言系统之间通信时,RPC 协议的灵活性和效率具有明显优势。

参考:

  • https://xiaolincoding.com/network/2_http/http_rpc.html#%E4%BB%8E-tcp-%E8%81%8A%E8%B5%B7
  • https://developer.aliyun.com/article/1436143