Skip to content
Go back

HTTP vs RPC:两种服务间通信协议的全面对比

一句话结论(30s)

HTTP 和 RPC 不是二选一的替代品,而是同一架构里不同层的最优解:对外 API 用 HTTP/REST(通用、可读、生态兼容),内部微服务间用 RPC/gRPC(二进制高性能、强类型、低延迟)。因为本质上这是在「通用性 vs 性能」「人类友好 vs 机器友好」之间做权衡——网关面向五花八门的外部调用方需要通用性,内部调用方受控、调用链深需要性能。

核心原理(2min)

底层深入(5-10min)

两种设计范式

HTTP 和 RPC 代表了服务间通信的两种设计范式:

思考:HTTP 和 RPC 的本质区别到底在哪?表面看是「文本 JSON vs 二进制 protobuf」,但更根本的是语义模型不同——HTTP 用「资源」(URL 指向的资源 + Method 表达动作)描述世界,RPC 用「方法」(像调本地函数那样调用一个具名过程)描述世界。所以选型前先问一句:你的调用方更关心「操作了什么资源」(对外、通用),还是更关心「调用了什么能力」(对内、性能)?答案不同,选型就不同。

HTTP:万维网的通用语言

RESTful 哲学

HTTP 最初设计用于传输超文本文档,但在 RESTful 架构的推动下,它成为了一套通用的服务间通信协议:

GET    /users/123        → 获取用户信息
POST   /users            → 创建用户
PUT    /users/123        → 完整更新用户
PATCH  /users/123        → 部分更新用户
DELETE /users/123        → 删除用户

HTTP 的资源导向设计使其拥有极强的自描述性——仅通过 URL + Method 就能大致理解接口意图。这对大型组织中的跨团队协作和 API 公开至关重要。

HTTP/1.1 的性能局限

队头阻塞:HTTP/1.1 的持久连接(Keep-Alive)允许在单个 TCP 连接上发送多个请求,但响应必须按发送顺序返回(Pipelining)。如果第一个请求处理慢(数据库查询耗时),后续所有响应都被阻塞。浏览器通过 6-8 个并发连接缓解此问题,但增加了连接开销和服务端资源消耗。

文本协议开销:HTTP/1.1 的请求和响应头都是文本格式,每次请求都携带大量重复的元数据(User-AgentAccept-EncodingCookie 等),这些几百字节的头部在大量请求时是显著的开销。

无强类型:HTTP 的请求体和响应体通常是 JSON,缺乏编译时的类型检查。服务端和客户端需要各自手动序列化/反序列化,容易出现字段名拼写错误、类型不匹配等运行时问题。

思考:为什么 HTTP 会有「队头阻塞 + 文本开销」这两个先天短板?因为它设计之初是为传输「超文本文档」服务的,请求互相独立、以可读性为先,从没想过要被高并发的服务间调用反复打。理解了它的出身,就能理解为什么后来会出现 HTTP/2 和 RPC 来「打补丁」——一个在传输层做优化,一个干脆换一种语义模型。

RPC:让远程调用像本地方法

核心思想

RPC(Remote Procedure Call)的核心思想是透明化网络通信。从开发者的视角看,调用远程服务与调用本地函数完全一样:

// 调用本地方法
User user = userService.getUserById(123);

// 调用远程 RPC 服务(看起来一样)
User user = remoteUserService.getUserById(123);

框架在底层自动处理序列化、网络传输、反序列化、异常转换。这个”看起来一样”的承诺非常强大——它极大降低了分布式系统开发的心智负担。

思考:RPC 凭什么能让远程调用「看起来像本地方法」?靠的是契约 + 代码生成:先用 IDL(如 .proto)把接口和方法签名固定下来,框架再据此生成 client/server 两端的 stub,把序列化、网络收发、异常转换全部藏进 stub 里。但「透明」也是一种「伪透明」——网络延迟、超时、失败这些分布式问题并不会因为看起来像本地调用就消失,反而更容易被忽略。

gRPC:基于 Protocol Buffers 的现代 RPC

gRPC 使用 Protocol Buffers(protobuf)作为接口定义语言(IDL)和序列化格式:

// user.proto
service UserService {
  rpc GetUserById (GetUserRequest) returns (GetUserResponse);
}

message GetUserRequest {
  int64 user_id = 1;
}

message GetUserResponse {
  int64 id = 1;
  string name = 2;
  string email = 3;
}

protobuf 编译后生成强类型的客户端和服务端 stub。调用方拿到的是编译时类型安全的接口,不会有字段名错误或类型不匹配的问题。

protobuf 的二进制编码相比 JSON 具有显著优势:

思考:为什么 protobuf 比 JSON 小 3-10 倍?因为 JSON 每次都要把字段名(“name”、“email” 这样的字符串)原样传输,而 protobuf 用字段编号(1、2、3)替代字段名,整数还用变长编码(Varint)。字段名再短也是字节,字段编号却可以短到 1 个字节——这就是「机器友好」和「人类友好」在序列化上的具体差别。

Thrift 与 Dubbo

Apache Thrift(Facebook 开源)是另一个流行的 RPC 框架,支持多种传输协议(TCP、HTTP)和序列化格式(Binary、Compact、JSON)。其设计更注重可扩展性——协议栈的每一层(传输、协议、处理器、服务端/客户端)都可以独立替换。

Dubbo(阿里巴巴开源)是 Java 生态中最广泛使用的 RPC 框架之一,为服务治理而设计:自动服务注册与发现(通过 ZooKeeper/Nacos)、负载均衡(随机/轮询/一致性哈希)、失败重试、熔断降级——这些不是 RPC 协议本身的功能,而是 RPC 框架提供的分布式系统基础设施。

HTTP/2.0:缩小差距

HTTP/2.0 引入了多项改进,使其性能接近传统 RPC:

二进制帧层:HTTP/2 将 HTTP 消息分解为二进制帧(HEADERS frame、DATA frame),而非 HTTP/1.1 的文本流。所有帧在单个 TCP 连接上多路复用。

流多路复用:多个请求-响应可以在同一个 TCP 连接上交错传输,每个流有独立的流 ID。接收端按流 ID 将帧组装回完整消息。这解决了 HTTP/1.1 的队头阻塞问题(但 TCP 层面的队头阻塞仍然存在)。

头部压缩(HPACK):HTTP/2 通过 HPACK 算法压缩请求/响应头。静态表预定义了 61 个常见头字段,动态表在连接期间学习重复出现的自定义头。后续请求可以只发送索引引用而非完整头字符串,极大减少了冗余传输。

服务端推送:服务端可以主动向客户端发送资源(如 CSS、JS),无需等待客户端请求。虽然实际使用不如预期广泛,但在受控环境(CDN 边缘节点)中有独特价值。

gRPC + HTTP/2

gRPC 正是构建在 HTTP/2 之上的——它利用了 HTTP/2 的流多路复用、二进制帧和头部压缩。但 gRPC 不仅是一个传输协议,更是一套完整的 RPC 体系(IDL + 代码生成 + 拦截器 + 超时/重试/熔断 + 负载均衡)。HTTP/2 提供了传输层的效率,gRPC 在此之上提供了开发体验层的便利。

微服务架构中的分层策略

现代微服务架构中的典型分层:

外部流量(浏览器、移动 App、第三方)


API Gateway(HTTP/REST — 通用、人类可读、生态兼容)


内部微服务(gRPC/Thrift — 高性能、强类型、低延迟)

网关层对外使用 HTTP/REST,因为:

内部服务间使用 RPC,因为:

对比总结

维度HTTP/RESTgRPC (RPC)
接口定义OpenAPI/Swagger(文档驱动)Protocol Buffers(IDL 驱动,代码生成)
序列化JSON(文本,人类可读)Protocol Buffers(二进制,机器最优)
传输协议HTTP/1.1 或 HTTP/2HTTP/2(原生多路复用)
类型安全运行时检查编译时(代码生成)
浏览器兼容原生支持需 gRPC-Web 代理
调试友好极高(curl、浏览器 DevTools)中等(需 grpcurl 等专用工具)
性能(大负载)中等高(二进制序列化 + 多路复用)
生态成熟度极成熟成熟(但不如 HTTP 广泛)
适用场景对外 API、跨组织通信、BFF内部微服务、高性能场景、多语言环境

思考:回到最初的问题——HTTP 和 RPC 是替代品吗?不是。表格里的每一条差异(类型安全、调试友好、浏览器兼容……)都指向同一个结论:它们在不同的「信任半径」里最优。对外边界要通用、要能被陌生客户端理解,用 HTTP;对内受控环境要性能、要契约,用 RPC。选型 = 判断你站在信任半径的哪一侧。

选择 HTTP 还是 RPC,本质上是在通用性性能人类友好机器友好之间做权衡。在一个成熟的微服务架构中,两者不是互斥的——它们是同一架构中不同层的最优解。

章末提问

Q1:HTTP 和 RPC 的本质区别是什么?

结论先行:本质区别不是「文本 vs 二进制」,而是语义模型不同——HTTP 是资源导向(URL + Method),RPC 是方法导向(具名函数调用)。因为 HTTP 为 B/S 架构设计、面向陌生调用方,强调通用性和可读性;RPC 为 C/S 架构设计、面向受控调用方,强调性能和类型安全。二进制序列化、代码生成只是这一本质在实现层的投影。

Q2:为什么内部微服务用 RPC 而不用 HTTP?

结论先行:因为内部调用方受控、调用链深、性能与契约要求高,RPC 的强类型 IDL + 二进制序列化 + 服务治理正好匹配。因为内部没有「生态兼容」负担(不必讨好陌生客户端),却有「开销逐级放大」的压力(深调用链下每次序列化/解析开销都会被逐级放大);protobuf 的 IDL 还能强制接口契约、减少跨团队沟通歧义。

Q3:HTTP/2 解决了什么?没解决什么?

结论先行:HTTP/2 用二进制帧 + 流多路复用 + HPACK 解决了 HTTP/1.1 的应用层队头阻塞和头部冗余,但没解决 TCP 层的队头阻塞。因为多路复用是在单个 TCP 连接上按流 ID 交错传输帧,一旦 TCP 丢包重传,整条连接上的所有流都要等待;彻底解决要靠 HTTP/3 换用 UDP(QUIC)。

Q4:gRPC 和 HTTP/2 是什么关系?

结论先行:gRPC 是构建在 HTTP/2 之上的一套完整 RPC 体系,HTTP/2 只提供传输层效率。因为 gRPC 复用了 HTTP/2 的流多路复用、二进制帧、头部压缩,又叠加了 IDL + 代码生成 + 拦截器 + 超时/重试/熔断等 RPC 层能力;所以「用 HTTP/2」不等于「有 RPC 体系」。

Q5:选型时怎么在 HTTP 和 RPC 之间做决策?

结论先行:看信任半径——对外边界用 HTTP/REST,内部受控环境用 RPC。因为对外要通用、可读、生态兼容(CDN/网关/防火墙全支持),对内要性能、类型安全、服务治理;成熟架构里两者不互斥,网关层对外 REST、内部服务间 gRPC,各取所长。


Share this post on:

Previous Post
HTTP/2 vs HTTP/3——从TCP队头阻塞到QUIC的彻底解决
Next Post
DNS域名解析——从输入URL到返回IP的完整链路