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 串之间是没有任何边界的,我们根本不知道到哪个地方才算一条完整消息。
正因为这个没有边界的特点,当我们选择使用 TCP 发送数据时,很可能会出现粘包问题。
举个例子,我们发送 “但丁“ 和 ”真是意大利人” 这句话时, 接收端收到的就是 “但丁真是意大利人” ,这时候接收端就无法区分去发送端是想要表达 “但丁” + “真是意大利人” 还是 “但丁真” + “是意大利人” 。
因此,纯裸 TCP 是不能直接拿来用的,需要在这个基础上加入一些自定义的规则,用于区分消息边界。
于是我们会把每条要发送的数据都包装一下,比如加入消息头,消息头里写清楚一个完整的包的长度是多少,根据这个长度可以继续接收数据,截取出来后它们就是我们真正要传输的消息体。
而这里提到的消息头,还可以放各种东西,比如消息体是否被压缩过和消息体格式之类的,只要上下游都约定好了,互相都认就可以了,这就是所谓的协议。
每个使用 TCP 的项目都可能会定义一套类似这样的协议解析标准,于是基于 TCP ,就衍生出了很多协议,比如 HTTP 和 RPC 。
HTTP 和 RPC
从上面的网络分层图可以看到,TCP 是传输层的协议,而基于 TCP 的 HTTP 和各类 RPC 协议,它们都只是定义了不同消息格式的应用层协议而已。
HTTP 协议(Hyper Text Transfer Protocol),又叫做超文本传输协议。我们用的比较多,平时上网在浏览器上敲个网址就能访问网页,这里用到的就是 HTTP 协议。
而 RPC(Remote Procedure Call),又叫做远程过程调用。它本身并不是一个具体的协议,而是一种调用方式。
举个例子,平时我们调用本地方法就像下面这样:
res = localFunc(req)
如果现在这不是个本地方法,而是个远端服务器暴露出来的一个方法 remoteFunc
,如果我们还能像调用本地方法那样去调用它,这样就可以屏蔽掉一些网络细节,使用起来更方便。
res = remoteFunc(req)
基于这个思路,大佬们造出了非常多款式的 RPC 协议,比如比较有名的 gRPC
,thrift
。
值得注意的是,虽然大部分 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 协议一般还会再建个连接池,在请求量大的时候,建立多条连接放在池内,要发数据的时候就从池里取一条连接出来,用完放回去,下次再复用。
由于连接池有利于提升网络请求性能,所以不少编程语言的网络库里都会给 HTTP 加个连接池,比如 Go 就是这么干的。
传输的内容
基于 TCP 传输的消息,说到底,无非都是消息头 Header 和消息体 Body。
Header 是用于标记一些特殊信息,其中最重要的是消息体长度。
Body 则是放我们真正需要传输的内容,而这些内容只能是二进制 01 串,所以 TCP 传字符串和数字都问题不大,因为字符串可以转成编码再变成 01 串,而数字本身也能直接转为二进制。但结构体呢,我们得想个办法将它也转为二进制 01 串,这样的方案现在也有很多现成的,比如 Json,Protobuf。
这个将结构体转为二进制数组的过程就叫序列化,反过来将二进制数组复原成结构体的过程叫反序列化。
对于主流的 HTTP/1.1,虽然它现在叫超文本协议,支持音频视频,但 HTTP 设计初是用于做网页文本展示的,所以它传的内容以字符串为主。Header 和 Body 都是如此。在 Body 这块,它使用 Json 来序列化结构体数据。
可以看到这里面的内容非常多的冗余。最明显的,像 Header
里的那些信息,如果我们约定好头部的第几位是 Content-Type,就不需要每次把 “Content-Type” 这个字段传过来,类似的情况在 body
的 Json 结构里也特别明显。
而 RPC,因为它定制化程度更高,可以采用体积更小的 Protobuf 或其他序列化协议去保存结构体数据,同时也不需要像 HTTP 那样考虑各种浏览器行为,因此性能也会更好一些,这也是在公司内部微服务中抛弃 HTTP,选择使用 RPC 的最主要原因。
当然上面说的 HTTP,其实特指的是现在主流使用的 HTTP/1.1,HTTP/2 在前者的基础上做了很多改进,所以性能可能比很多 RPC 协议还要好,甚至连 gRPC 底层都直接用的 HTTP/2。
HTTP 和 RPC 的性能对比与应用场景
- 延迟
- RPC:通常情况下,RPC 的延迟相对较低,因为它旨在实现远程过程调用,需要快速的响应时间,这使得 RPC 在一些实时性要求较高的场景中表现优越。
- HTTP: HTTP 的延迟相对较高,特别是在建立连接、发送请求和接收响应的过程中。然而,HTTP 协议的延迟也受到网络条件和服务器响应时间的影响。
- 带宽利用率
- RPC:由于RPC通常使用二进制格式进行数据传输,相对于文本格式,带宽利用率较高。这对于大量数据传输的情况下是有优势的。
- HTTP: HTTP 通常使用文本格式,如 Json 或 XML ,这可能导致较高的带宽消耗。然而,HTTP/2 和 HTTP/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