这篇笔记的目标是把
RPC从“像本地调用一样调远程服务”这句抽象描述拆开,分别说明代理、服务发现、序列化、协议编码、网络传输、请求关联和服务治理在一次调用里各自承担什么职责。
RPC 的核心不在于替代
HTTP,而在于把“远程调用的工程复杂度”封装到统一框架中。只保留“代理 + 找到地址 + 封装请求”的理解还不够,还需要把注册发现、超时重试、负载均衡、连接复用和兼容性边界一并纳入。
参考资料:
官方文档:Apache Dubbo Docs 、 Dubbo Protocol Overview 、 gRPC Introduction 、 Protocol Buffers Overview 、 Apache Thrift Documentation 、 Java RMI
实现参考:知乎柳树 - 从零开始写 RPC 框架 、 知乎易哥 - 既然有 HTTP 请求,为什么还要用 RPC 调用 、 EasyRPC
[TOC]
RPC 是一种调用模型和框架设计思路,不是某个固定协议。Dubbo、gRPC、Thrift、RMI 都属于不同形态的 RPC 实现;像 OpenFeign 这样的声明式 HTTP 客户端,也常被用来承载服务间远程调用,但它并不等同于具备完整治理能力的 RPC 框架。
RPC 并不天然比 HTTP “更快”,更准确的说法是:很多 RPC 框架会使用更紧凑的二进制协议、更少的报文冗余、连接复用和更贴近方法调用的数据模型,因此在服务间内部调用场景里常常更高效。
HTTP 是一种应用层协议;如果基于 HTTP 来实现 RPC,那么底层仍然遵循 HTTP 的请求响应语义。只是 RPC 框架会在“如何描述方法、参数、返回值,以及如何生成代理和处理调用链”这几个层面继续往上封装。
什么是RPC
RPC(Remote Procedure Call,远程过程调用)要解决的核心问题,是在不同进程甚至不同机器之间调用服务时,仍然用接近本地方法调用的编程模型表达远程交互。
一次真正可用的 RPC 调用,至少要把下面这些信息跨进程传过去:
- 目标服务是谁
- 调用的是哪个方法
- 参数类型和参数值分别是什么
- 调用超时、请求 ID、认证信息等元数据是什么
- 返回值、异常或错误码如何表达
因此,RPC 不能只理解为“发一个网络请求”,它实际上同时处理两层问题:
- 调用语义层:让业务代码看起来像在调用本地接口
- 通信实现层:把接口、参数、返回值和上下文转换成网络上传输的字节序列
从这个角度看,RPC 和 HTTP 不是天然对立关系。HTTP 可以作为 RPC 的承载协议之一;RPC 关注的是远程调用模型,HTTP 关注的是应用层报文与交互语义。
先抓主线:RPC 框架到底在做什么
如果把 RPC 框架只理解成“帮我发一个请求”,会低估它做的事情。它真正要解决的是:
- 让业务代码按“调用本地方法”的方式去调用远程服务
- 在分布式环境里找到可用服务实例,而不是把 IP/端口硬编码在代码里
- 把“接口名、方法名、参数、超时、请求 ID”等信息编码成双方都能理解的协议
- 通过网络把请求发出去,并且把响应正确地对应回原来的调用线程
- 在多实例场景下提供负载均衡、超时、重试、熔断、监控等治理能力
RPC 框架的核心可以概括为:
通过代理实现,找到对应的 IP 地址,根据协议封装请求对象
这个概括已经比较接近,但还要补上下面几个关键词:
- 代理:让调用端写起来像本地方法调用
- 服务发现:不直接写死 IP,而是从注册中心或服务目录里拿到实例列表
- 路由和负载均衡:多个提供者时,要决定这次到底调哪一台
- 序列化和协议编码:把方法调用转换成可传输的字节流
- 请求关联:响应回来后,要通过请求 ID 找回原来的调用方
- 容错治理:超时、重试、降级、限流、监控,不属于“能不能调通”,但属于“框架为什么有价值”
一次 RPC 调用可以先抽象成下面这条链路:
flowchart LR
A[业务代码发起调用] --> B[动态代理 Proxy]
B --> C[服务发现]
C --> D[路由 / 负载均衡]
D --> E[组装 Invocation]
E --> F[序列化 + 协议编码]
F --> G[网络传输 TCP / HTTP2]
G --> H[服务端解码 + 反序列化]
H --> I[执行本地实现]
I --> J[结果序列化并返回]
J --> K[客户端解码并还原 Result]
从这个角度看,代理只是入口,不是 RPC 框架的全部。
RPC、HTTP、Dubbo、gRPC 到底是什么关系
这几个概念很容易混在一起,但它们并不在同一个层面:
| 名字 | 更准确的定位 | 作用 |
|---|---|---|
| RPC | 调用模型 / 编程方式 | 目标是让远程调用看起来像本地调用 |
| HTTP | 应用层协议 | 定义请求行、Header、Body、状态码等传输语义 |
| TCP | 传输层协议 | 负责可靠传输,很多 RPC 框架最终都跑在 TCP 上 |
| Dubbo | RPC 框架 | 提供代理、注册发现、集群容错、协议扩展等能力 |
| Dubbo 协议 | Dubbo 默认的私有二进制协议 | 通常基于 TCP 长连接,报文更紧凑 |
| gRPC | RPC 框架 | 通常使用 HTTP/2 + Protobuf |
| Thrift | RPC 框架 / IDL 体系 | 提供接口定义、代码生成和多语言通信 |
| RMI | Java 原生 RPC 方案 | 更偏 Java 生态内部 |
更准确的理解可以概括为:
RPC不是和HTTP对立的东西HTTP可以作为 RPC 的传输协议之一Dubbo 协议不是“RPC 的唯一协议”,它只是 Dubbo 的一种实现方式- 一个完整的 RPC 框架,通常是“代理 + 协议 + 序列化 + 传输 + 服务治理”的组合
常见组合大概是这样:
| 框架 / 方案 | 常见传输协议 | 常见序列化方式 | 特点 |
|---|---|---|---|
| 基于 RESTful 的接口调用 | HTTP/1.1 | JSON | 通用性强,可读性好,但报文相对冗长 |
| gRPC | HTTP/2 | Protobuf | 跨语言好,IDL 清晰,流式能力强 |
| Dubbo 2 经典协议 | TCP | Hessian2 等 | 内部服务调用常见,性能和治理能力都比较强 |
| Dubbo 3 Triple | HTTP/2 | Protobuf | Dubbo 对 gRPC 生态的靠拢方案 |
| Thrift | TCP / HTTP | Binary / Compact Protocol | 适合多语言服务 |
| RMI | TCP | Java 对象序列化 | 简单直接,但更偏 Java 内部使用 |
可以把它们分成两层来看:
- 传输层问题:到底走 TCP、HTTP/1.1、HTTP/2 还是别的通道
- 调用层问题:怎么描述方法调用、怎么生成代理、怎么发现服务、怎么处理重试和负载均衡
很多文章容易把这两层混在一起,这也是为什么初学 RPC 时会觉得概念很绕。
方法调用
典型的 B/S 或 C/S 模型里,客户端通过服务端接口获取数据和结果,这类调用更偏外部访问场景。
当系统演化为分布式或微服务架构后,业务会被拆成多个独立服务,调用关系开始大量发生在服务与服务之间。这时调用方更关心“如何像本地接口一样发起调用”,而不是在每一段业务代码里手动拼装 URL、Header、Body 与重试逻辑。
本地调用
假设调用函数 Multiply 来计算 lvalue * rvalue 的结果:
1
2
3
4
5
6
7
8
int Multiply(int l, int r) {
int y = l * r;
return y;
}
int lvalue = 10;
int rvalue = 20;
int l_times_r = Multiply(lvalue, rvalue);
这是标准的本地函数调用。调用方与被调用方处在同一个进程地址空间内,参数传递、栈帧切换和返回值获取都由语言运行时直接完成。
远程调用
当系统拆成多个服务后,某些公共能力会被抽到独立服务中供其他服务复用。

此时 Service A 内部并没有 Service B 的 CalculatorImpl 实现类,也不存在跨机器共享内存的前提。
一种直接做法是让 Service B 暴露 RESTful 接口,再由 Service A 通过 HTTP 请求间接调用 CalculatorImpl.add()。
这种方式可以完成远程调用,但业务代码通常仍然要显式处理 URL、请求方法、参数编码、异常处理与重试策略。
RPC 要解决的问题,是把这些远程通信细节收敛到框架内部,让调用方继续面向接口编程。
常见做法是为接口生成代理对象。以 Dubbo 为例,消费者注入的通常不是远端服务实现本身,而是一个代理引用;业务线程调用代理方法后,框架再完成方法描述、服务发现、负载均衡、协议编码和网络传输。底层承载协议可能是 Dubbo 私有协议,也可能是基于 HTTP/2 的 Triple 或其他实现,并不固定等同于某个 httpClient。
这正是很多 RPC 框架要统一处理的核心流程。
本地调用至少依赖下面三类信息:
- 明确的目标接口或函数
- 调用参数
- 返回值或异常

远程调用并不会减少这些要素,差别只在于这些信息必须先被编码成双方都能理解的消息格式,再通过网络送到远端。
Service A 需要通过网络明确告诉 Service B:目标方法是 add,参数分别是 3 和 5,结果如何返回,异常如何表达。

传输的报文里面按照约定的协议格式给出了函数名和参数,大致这样:

上述编码仅用于说明思路,不代表真实生产报文。网络上传输的最终都是字节序列,实际框架会进一步定义消息头、消息体、长度字段、序列化类型、请求 ID 等结构。
总结: RPC 至少需要解决两个基础问题:
- 解决分布式系统中,服务之间的调用问题
- 远程调用时,仍然保持接近本地方法调用的使用体验
如果把生产级框架能力也纳入考虑,还需要再补三点:
- 服务实例地址不能靠人工维护,需要服务发现和注册中心
- 多个服务实例下,需要路由、负载均衡和容错策略
- 调用结果需要和原请求一一对应,需要请求 ID、超时控制和响应还原
用 Dubbo 看一次完整流程
如果把 Dubbo 当成一个具体例子,RPC 的抽象过程会更容易理解。整个过程可以拆成两个阶段:
- 服务启动阶段:提供者暴露服务,消费者订阅服务
- 正式调用阶段:消费者拿着代理对象发起远程调用
1. 服务启动阶段
flowchart LR
A[Provider 启动] --> B[读取 ServiceConfig]
B --> C[把本地实现包装成 Invoker]
C --> D[通过 Protocol 暴露服务]
D --> E[向 Registry 注册服务地址和元数据]
F[Consumer 启动] --> G[读取 ReferenceConfig]
G --> H[向 Registry 订阅服务]
H --> I[拿到 Provider 地址列表]
I --> J[生成接口代理 Proxy]
这个阶段完成后,消费者手里虽然拿到的是一个接口代理,但它背后其实已经关联了:
- 服务接口名
- 注册中心返回的服务列表
- 集群容错策略
- 负载均衡策略
- 通信协议和序列化方式
2. 一次真实调用阶段
下面以消费者调用 userService.queryById(1) 为例:
sequenceDiagram
participant App as Consumer业务代码
participant Proxy as Dubbo代理
participant Cluster as Cluster/LoadBalance
participant Registry as 注册中心
participant Client as Dubbo Client
participant Server as Dubbo Server
participant Impl as Provider实现类
App->>Proxy: 调用 queryById(1)
Proxy->>Cluster: 组装 Invocation
Cluster->>Registry: 获取/使用本地缓存的 Provider 列表
Registry-->>Cluster: 返回可用地址
Cluster->>Cluster: 路由 + 负载均衡选择一台 Provider
Cluster->>Client: 编码请求并发起网络调用
Client->>Server: 发送二进制请求报文
Server->>Server: 解码 + 反序列化
Server->>Impl: 调用本地实现方法
Impl-->>Server: 返回结果
Server-->>Client: 编码响应报文
Client-->>Proxy: 解码并根据请求 ID 匹配响应
Proxy-->>App: 返回 queryResult
这张时序图主要体现了 4 个关键点:
- 对业务方透明:业务代码只看到接口调用,看不到底层网络细节
- 对框架方明确:框架内部其实一直在处理
Invocation -> 编码 -> 网络收发 -> 解码 -> Result - 对地址解耦:消费者不是把 IP 写死,而是通过注册中心订阅和缓存服务列表
- 对集群友好:多实例下不是“直接连某台机器”,而是先经过集群容错和负载均衡
3. Dubbo 里几个容易混淆的对象
Proxy、Invoker、Exporter 这些对象名称很容易混淆,可以先按下面的方式理解:
| 对象 | 可以先怎么理解 | 作用 |
|---|---|---|
| Proxy | 给业务方用的“假实现” | 让接口调用呈现为本地方法调用的使用方式 |
| Invocation | 一次调用的描述对象 | 里面会带方法名、参数、附件等信息 |
| Invoker | 可执行调用的统一抽象 | 不管本地、远程、集群,最终都抽象成它 |
| Exporter | 被暴露出去的服务包装 | 主要站在服务提供者一侧 |
| Directory | 服务列表目录 | 管理某个接口当前有哪些可用 Invoker |
| Cluster | 集群容错入口 | 失败重试、快速失败等都在这里决定 |
| LoadBalance | 负载均衡策略 | 决定这次请求落到哪一个 Provider |
如果只保留主线,可以记成:
接口 -> Proxy -> Invocation -> Invoker -> Protocol -> Transport -> Provider
4. Dubbo 协议在这里扮演什么角色
以经典 Dubbo 协议为例,它主要解决的是“请求报文怎么定义”这个问题。通常会包括:
- 魔法数,用来识别这是 Dubbo 报文
- 请求还是响应
- 是否双向通信、是否心跳
- 序列化方式编号
- 请求 ID
- 方法调用需要的元数据,比如接口名、方法名、参数类型、参数值
也就是说,Dubbo 协议 不是在决定“要不要调用这个服务”,它决定的是“调用已经发生时,这个请求和响应如何在网络里表达”。
协议确定之后,请求是怎么传出去的
前面提到“代理、服务发现、协议封装”,但一次 RPC 调用真正落到网络里,还需要再回答一个问题:
当协议已经选定之后,请求到底是怎么从当前进程发到远端机器上的?
这个问题不能只回答“走网络传输”,还要把它拆到网络分层里看。
1. 先分清:RPC、HTTP、Dubbo 协议分别处在哪一层
如果按常见的 TCP/IP 分层模型来看,可以先粗略理解成这样:
| 层次 | 典型内容 | 在 RPC 调用里的角色 |
|---|---|---|
| 应用层 | RPC 协议、HTTP、HTTP/2、Dubbo 协议、gRPC、序列化协议 | 定义报文格式、方法名、参数、响应体怎么表示 |
| 传输层 | TCP、UDP | 负责端到端传输、可靠性、重传、流量控制、端口 |
| 网络层 | IP | 负责从源 IP 把数据送到目标 IP |
| 数据链路层 | Ethernet、Wi-Fi | 负责同一跳链路上的帧传输 |
| 物理层 | 网线、光纤、电磁信号 | 负责把比特真正发出去 |
所以:
RPC更像调用模型,本身不是网络分层里的某一层协议Dubbo 协议、HTTP、gRPC这些,主要都落在应用层TCP是传输层IP是网络层
这也是为什么“RPC 是跑在网络上的”,但不能简单说“RPC 就是传输层协议”。准确一点的说法应该是:
RPC框架在应用层定义调用语义和报文格式,再借助底层传输通道把数据送出去;这个通道可能是TCP、UDP,也可能是基于TCP之上的HTTP/2。
2. 以基于 TCP 的 RPC 调用为例
假设现在已经确定使用:
- 传输协议:
TCP - RPC 协议:
Dubbo 协议 - 序列化方式:例如
Hessian2或Protobuf
那么一次调用在客户端侧大致会经历下面几步:
flowchart TD
A[业务代码调用代理对象] --> B[组装 Invocation]
B --> C[序列化参数]
C --> D[按 Dubbo / 自定义协议编码报文]
D --> E[写入 Socket]
E --> F[用户态切到内核态]
F --> G[TCP 负责分段/编号/校验/重传控制]
G --> H[IP 负责寻址和路由]
H --> I[网卡把二进制比特发到网络]
I --> J[服务端网卡接收数据]
J --> K[内核协议栈逐层向上交付]
K --> L[服务端进程从 Socket 读取字节流]
L --> M[协议解码 + 反序列化]
M --> N[执行本地方法并返回结果]
这个过程里最核心的分工是:
- RPC 框架负责“把调用翻译成报文”
- 操作系统协议栈负责“把报文送到对端”
也就是说,RPC 框架一般不会自己去实现 TCP/IP 协议栈,而是调用操作系统提供的 Socket API,把编码后的字节流交给内核。
3. Socket 在这里到底是什么
很多资料会说“RPC 底层就是 Socket 通信”,这个说法不算错,但容易过于简化。
Socket 可以先理解成:
- 应用程序和操作系统网络协议栈之间的一个编程接口
- 应用层进程收发网络数据时最常见的入口
客户端并不是直接操作网卡,也不是直接去“手写 TCP 包”。通常做法是:
- 创建 Socket
- 连接到目标 IP 和端口
- 把 RPC 编码后的字节写入 Socket 发送缓冲区
- 由操作系统内核负责后续的 TCP/IP 处理
服务端也是类似:
- 监听某个端口
- 接收客户端连接
- 从 Socket 读取字节流
- 按照约定好的 RPC 协议解码
- 执行本地方法后再把响应写回 Socket
因此,Socket 更像“应用程序使用网络能力的门把手”,而不是完整传输机制本身。
3.1 工程实现里常见的 NIO / Netty 在做什么
上面说的是原理分层;如果落到 Java 工程实现,很多 RPC 框架不会直接手写最底层阻塞式 Socket,而是建立在:
Java NIONetty
之类的网络通信框架之上。
它们主要解决的是“如何更高效地管理大量连接和收发事件”,比如:
- 用非阻塞 I/O 减少线程被读写操作长期卡住
- 用
Selector或事件循环监听多个连接上的可读、可写事件 - 维护连接、编解码器、心跳、线程模型
- 把业务线程和 I/O 线程分开,避免网络线程被业务逻辑拖慢
所以从工程实现角度看,很多 RPC 框架的底层路径可以再细化成:
代理对象 -> 编码器 / 序列化器 -> Netty / NIO -> Socket -> 内核 TCP/IP 协议栈 -> 网络
这也是为什么在 Dubbo、gRPC、Thrift 这类框架里,除了“协议设计”之外,I/O 模型和线程模型也会显著影响吞吐量与延迟。
4. 数据到了传输层以后,TCP 具体做什么
如果 RPC 走的是 TCP,那么 RPC 框架把字节流交给 Socket 之后,后面的很多事就交给 TCP 了。TCP 主要负责:
- 建立连接,典型就是三次握手
- 把大的字节流切成适合传输的报文段
- 为每个报文段编号,保证有序到达
- 通过 ACK、重传、超时机制保证可靠传输
- 通过滑动窗口、流量控制、拥塞控制避免把链路打爆
这也是为什么很多 RPC 框架偏爱 TCP:
- 不需要自己处理丢包重传
- 天然是面向连接的
- 更适合请求/响应式调用
- 长连接场景下可以减少频繁建连的开销
但这里也要注意一点:
TCP 只保证字节流可靠送达,不理解“这是一个 RPC 请求”这层语义。
对 TCP 来说,它看到的只是连续字节;至于哪一段字节代表:
- 请求头
- 请求体
- 方法名
- 参数列表
- 返回值
这些都属于应用层协议要解决的问题。
5. 为什么还要做“协议头”和“消息边界”
因为 TCP 是面向字节流的,不是面向消息的。它不会天然告诉接收方:
- 这一批字节是不是一个完整请求
- 一个请求从哪里开始、到哪里结束
- 当前收到的是请求还是响应
所以 RPC 协议通常都会在应用层自己定义报文结构。例如:
- 魔法数:识别这是不是本协议的包
- 消息长度:告诉接收方这一帧总共有多长
- 请求 ID:把响应匹配回对应调用
- 序列化类型:告诉接收方该怎么反序列化
- 消息类型:请求、响应、心跳、异常
这类机制本质上是在解决两个问题:
- 拆包 / 粘包之后,怎么重新切出完整消息
- 拿到完整消息之后,怎么知道该按什么规则解析
所以“RPC 是怎么把数据传过去的”这个问题,不能只回答“通过 TCP”,还必须补一句:
通过 TCP 传输的是字节流,而 RPC 协议负责给这些字节流定义结构、边界和语义。
6. 到了网络层和链路层,又发生了什么
在 TCP 之下,数据还会继续往下走:
IP负责目标地址寻址和路由选择- 链路层负责把 IP 包封装成帧,在当前链路上传输
- 网卡把帧转换成电信号、光信号或无线信号发出去
到了服务端之后,过程再反过来:
- 网卡收到比特流
- 交给内核网络协议栈
- 链路层解帧
- 网络层处理 IP
- 传输层处理 TCP
- 把字节流放到目标 Socket 对应的接收缓冲区
- 服务端进程从 Socket 读取数据
- RPC 框架完成解码、反序列化和方法调用
也就是说,网络层和链路层主要负责“送达”,应用层主要负责“看懂”。
7. 如果走 HTTP 或 HTTP/2,机制有什么不同
如果 RPC 选择的是基于 HTTP 或 HTTP/2 的方案,比如:
- 基于
HTTP/1.1 + JSON的接口调用 gRPC的HTTP/2 + ProtobufDubbo3 Triple的HTTP/2 + Protobuf
那么底层数据仍然要经过:
- Socket
- 操作系统协议栈
- TCP
- IP
差别主要不在“有没有经过传输层”,而在应用层协议长什么样:
HTTP/1.1有请求行、Header、BodyHTTP/2引入二进制帧、多路复用、Header 压缩Dubbo私有协议会更贴近方法调用模型,报文通常更紧凑
所以,HTTP RPC 和 Dubbo RPC 的根本差异,不是一个“走网络层”、一个“走传输层”,而是:
- 应用层协议格式不同
- 报文冗余程度不同
- 连接复用能力不同
- 对服务治理、流式通信、多语言生态的支持重点不同
8. 一次请求返回时,为什么能回到原来的调用方
客户端发起 RPC 调用后,通常不会只是“把数据发出去”就结束。框架还会记录:
- 请求 ID
- 超时时间
- Future / Promise 或回调对象
- 当前连接和目标节点信息
服务端响应回来后,客户端会:
- 先按协议解码出响应对象
- 读取其中的请求 ID
- 用请求 ID 找到本地等待中的 Future
- 唤醒阻塞线程,或者回调异步逻辑
所以,“方法调用像本地”只是表象,底层实际上是在做一次带请求 ID 关联的网络请求响应匹配。
9. 用一句话收束这部分
从机制上看,一次 RPC 请求的传递过程可以概括为:
业务调用先在应用层被 RPC 框架转换成协议报文,再通过 Socket 交给操作系统;随后由传输层的 TCP、网络层的 IP 和链路层共同把数据送到远端;对端收到字节流后,再由 RPC 框架按协议解码、反序列化并执行本地方法。
因此,RPC 既不是单纯的“网络层能力”,也不是单纯的“传输层能力”,而是:
- 应用层定义调用语义
- 传输层保证端到端传输
- 网络层负责寻址和路由
- 链路层和物理层负责真正把数据送出去
RPC 的工程边界与常见误区
RPC 框架会尽量把远程调用伪装成本地调用,但这种“本地感”只是一层编程抽象,底层仍然是高延迟、可失败、可抖动的网络交互。真正的工程难点,往往出现在主流程之外。
| 问题 | 根因 | 典型后果 | 常见处理方式 |
|---|---|---|---|
| 超时后自动重试 | 网络抖动、瞬时过载、调用超时配置过短 | 非幂等写操作被重复执行 | 区分读写请求、限制重试次数、引入幂等键 |
| 注册中心与本地缓存短暂不一致 | 实例上下线传播存在延迟 | 调用到已下线实例,或短时间内找不到新实例 | 本地缓存兜底、健康检查、订阅变更推送 |
| 序列化格式或模型版本不兼容 | 字段删除、类型变更、协议升级不一致 | 反序列化失败、数据错位、兼容性回退困难 | 采用可演进协议,严格管理字段兼容规则 |
| 长连接、线程池或事件循环被打满 | 慢调用积压、连接数过高、下游响应变慢 | 调用雪崩、级联超时、吞吐量急剧下降 | 连接复用、限流、隔离线程池、熔断降级 |
| 误把远程调用当成本地调用 | 忽略网络异常、超时、部分失败 | 代码只处理业务异常,不处理通信失败 | 在接口层显式设计超时、取消、降级和错误码语义 |
因此,RPC 的真正价值并不只是“把请求发出去”,而是持续处理这些跨进程、跨网络、跨版本的工程问题。
如何实现一个RPC

以上图为例,Application 是服务调用方,Client Stub 是客户端代理对象,Client Run-time Library 是负责协议编解码、连接管理和网络收发的运行时组件,最底层再由操作系统网络栈完成真实传输。
如果只做一个教学性质的最小 RPC Demo,通常至少要完成 4 件事:
- 服务契约描述
- 服务寻址
- 序列化与协议编码
- 网络传输与响应关联
1. 服务契约描述
远程调用不能像本地调用那样依赖函数指针或进程内地址空间,因此必须先回答一个最基础的问题:如何唯一描述“要调哪个方法”。
在本地调用里,函数体是由当前进程地址空间里的代码和运行时直接定位的;但在远程调用里,调用方和服务提供方通常不在同一个进程里,很多时候也不在同一台机器上,所以不能直接依赖内存地址来表达“要执行哪段逻辑”。
这时就必须用一套双方都认可的描述方式,把“目标服务、目标方法、参数类型、参数值、版本信息”等内容表达出来。
常见做法大致有两类:
- 直接使用“接口名 + 方法名 + 参数类型 + 版本”等元数据
- 通过
IDL生成统一的客户端和服务端桩代码,例如gRPC、Thrift
有些实现会进一步把这些信息压缩成内部调用标识,例如字符串签名或整数 ID,再在客户端和服务端各自维护一份映射表。这样做的目的,是让双方都能根据同一个调用标识定位到同一个方法。
因此,“Call ID 映射”可以看作一种实现手段,但它不是 RPC 的本质。RPC 真正要解决的问题是:如何在远程场景下稳定、准确地描述方法调用,并让调用双方对这份契约保持一致。
2. 服务寻址
方法描述确定之后,还要能找到实例地址。寻址方式通常有两种:
- 静态配置:直接写死目标主机和端口,适合简单场景或早期 Demo
- 注册中心:由服务提供者注册实例信息,消费者订阅并动态获取实例列表,适合多实例和弹性扩缩容场景
在 Dubbo 体系里,注册中心可以对接 ZooKeeper、Nacos 等组件;在 Java RMI 里,则有 rmiregistry 这类注册设施,用于根据名称查找远程对象引用。
以 Java RMI 为例,服务寻址通常依赖 RMI Registry。服务端启动后会把远程对象引用绑定到注册表中的某个名称上,客户端再根据名称查找对应的远程对象引用,然后发起远程方法调用。
这里需要区分两个概念:
RMI Registry是 RMI 的注册设施,负责维护“服务名 -> 远程对象引用”的映射关系JNDI是访问命名与目录服务的统一接口抽象,在某些场景下可以用来访问 RMI Registry,但它本身并不等于注册表
因此,从机制上看,RMI 的服务发现过程可以概括为:服务端完成 bind/rebind,客户端通过 lookup 查找,拿到远程对象引用后再进行调用。

3. 序列化与协议编码
本地调用时,参数传递、栈帧切换和返回值读取都发生在同一个进程地址空间内,调用方和被调用方可以直接通过内存与运行时机制完成数据交换。
但在远程过程调用里,客户端和服务端是两个不同的进程,很多时候还分布在不同机器上,参数和返回值都不能直接通过内存传递。这时就必须先把方法调用涉及的数据转换成可传输的字节流,发送到远端后,再由对方还原成自己能识别的数据结构。
这就是序列化与反序列化要解决的问题:
- 序列化:把参数、返回值、异常、元数据等对象信息转换成字节序列
- 反序列化:把接收到的字节序列还原成程序可以继续处理的对象或数据结构
从这个角度看,远程调用传输的并不是“对象本身”,而是对象经过编码后的字节表示。
但只有序列化还不够。因为网络上传输的不只是“参数内容”,还要回答下面这些问题:
- 这一段字节到底是一条完整请求,还是几条消息拼在一起
- 当前收到的是请求、响应、心跳,还是异常消息
- 这一条响应应该匹配回哪个请求
- 接收方应该按哪种序列化方式去解析这段字节
这些就属于协议编码要处理的内容。协议编码通常会在序列化后的字节流之外,再补上消息边界、消息类型、长度字段、请求 ID、压缩方式、序列化方式等协议字段。
可以把两者的分工概括为:
- 序列化:解决“对象如何变成字节,又如何从字节还原回来”
- 协议编码:解决“这些字节在网络上传输时如何分帧、识别、关联和解析”
常见序列化方案包括 Protobuf、Hessian2、JSON、Java 原生序列化等。选择时不仅要看性能,还要看跨语言能力、可演进性与安全性。
4. 网络传输与响应关联
远程调用最终还是要落到网络上传输,因此在完成方法定位、序列化和协议编码之后,还需要一个真正负责收发数据的传输层,把请求从客户端送到服务端,再把执行结果从服务端返回给客户端。
这一层最直接的职责可以概括为两件事:
- 把调用请求对应的字节流发送到远端服务
- 把远端返回的结果、异常或响应消息再传回客户端
只要能完成这两件事,都可以作为 RPC 的底层传输通道。因此,从原理上看,RPC 并不强绑定某一种网络协议;关键不在于“必须用哪种协议”,而在于是否能够稳定完成请求发送、响应返回以及后续的关联处理。
常见承载方式包括:
- 基于
TCP的私有二进制协议 - 基于
HTTP/2的gRPC/Dubbo Triple - 基于
HTTP/1.1 + JSON的声明式远程调用
很多 RPC 框架偏爱 TCP,因为它能提供可靠传输、长连接复用和更灵活的私有协议设计;但这并不意味着只能使用 TCP。有些场景也可以建立在 UDP 之上,而 gRPC、Dubbo Triple 这一类方案,则进一步选择了 HTTP/2 作为承载通道。
很多 Java RPC 框架在工程实现上会基于 NIO 或 Netty 管理连接复用、心跳、线程模型和异步收发。除了把请求发出去,还必须解决“响应回来后如何回到原来的调用方”这个问题,因此一般都会维护请求 ID 与 Future / 回调之间的对应关系。
如果只从“能跑通一次 RPC 调用”的角度看,一个最小实现通常至少需要下面三部分:
- 调用标识或方法映射:可以是“接口名 + 方法名 + 参数类型”的描述,也可以是内部维护的整数 ID / 字符串映射
- 序列化与反序列化:可以自己实现,也可以使用
Protobuf、FlatBuffers等现成方案 - 网络传输能力:可以直接基于
Socket,也可以建立在Asio、ZeroMQ、Netty等通信库之上
如果再加上前面提到的请求 ID 关联、超时控制和异常返回处理,就已经可以形成一个教学性质的简化 RPC Demo;而真正的 RPC 框架,还会继续叠加负载均衡、服务发现、重试策略、熔断限流、认证鉴权、链路追踪和监控告警等治理能力。
RPC 代码实现可参考知乎柳树
完整的 RPC 框架
生产环境里的 RPC 框架,通常是在“可完成远程调用”这个最小闭环之上,再继续叠加注册发现、负载均衡、熔断限流、监控、认证和多协议兼容等能力。

其中,RPC 协议 模块负责定义消息在网络中的表达方式,但它并不是框架的全部。

一个更完整的 RPC 框架,通常可以拆成下面几层:
| 模块 | 主要职责 |
|---|---|
| 服务契约层 | 定义接口、方法、参数、异常和版本边界 |
| 代理与调用抽象层 | 生成客户端代理,封装调用上下文 |
| 注册发现层 | 管理服务注册、实例订阅、地址变化通知 |
| 集群治理层 | 负责路由、负载均衡、超时、重试、熔断、限流 |
| 协议与序列化层 | 编码请求与响应,处理消息边界与兼容性 |
| 传输与连接层 | 管理连接、线程模型、心跳、异步收发 |
| 可观测性与安全层 | 提供日志、指标、Tracing、认证鉴权 |

- 客户端(Client):服务调用方
- 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数打包成网络消息,再通过网络传输发送给服务端
- 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理
- 服务端(Server):服务真正的提供者
- Network Service:底层传输能力,可以建立在
TCP、HTTP或其他通信通道之上
在网络消息传输上,可以基于 TCP、UDP、HTTP 等方式来实现,各自都有不同特点。
TCP:
基于 TCP 实现的 RPC 调用,通常能够更灵活地定制协议字段,减少报文冗余,在性能、吞吐量和并发能力上更有优势,但也要自行处理更多底层细节,例如消息边界、编解码和连接管理。
一种典型做法是由服务调用方和服务提供方建立 Socket 连接,再由调用方把接口名称、方法名称和参数序列化后发送给服务端,服务端完成反序列化后,再通过分发或反射调用对应的方法实现。
在实际工程里,这一层往往还会继续封装。例如 RMI 就是在 TCP 之上传递可序列化的 Java 对象,而像 Dubbo 这类框架则会继续补上协议头、请求 ID、心跳、连接池和线程模型等能力。
TCP 连接既可以是按需建立的短连接,也可以是长期复用的长连接。长连接通常会配合心跳检测机制定期确认连接是否仍然可用,从而让多个远程调用共享同一条连接,减少频繁建连的开销。
HTTP:
基于 HTTP 实现的 RPC,更接近“访问网页”或“调用 Web 接口”的方式。它的优势在于请求和响应模型成熟,工具链完善,基于 JSON、XML 之类格式进行开发和调试也比较直接。
这类方式通常由调用方向服务提供方发送 GET、POST、PUT、DELETE 等请求,请求参数可以放在 URL、Query、Header 或 Body 中,服务端再按约定的接口语义完成解析和处理,最终返回 JSON、XML 或其他格式的结果。
与基于 TCP 的私有协议相比,HTTP 作为更通用的应用层协议,往往会携带更多报文元数据,因此字节开销通常更高,但它在跨语言、跨团队协作、网关接入、调试工具支持等方面更具优势。
由于已经有大量成熟的 Web 服务器和中间件生态,例如 Tomcat、Nginx、各类网关和代理组件,基于 HTTP 的 RPC 实现通常更容易落地,也更容易与现有 Web 体系整合。
因此,从完整框架视角看,RPC 的底层传输并没有唯一标准答案。真正的差别,往往不只是“走 TCP 还是走 HTTP”,而是框架在调用抽象、消息编码、连接管理和服务治理这些层面做到什么程度。
RPC vs RESTful
这一组概念最容易被拿来直接对比,但严格来说,RPC 和 RESTful 并不是完全同一层面的东西:
RPC关注的是“像调用本地方法一样调用远程服务”RESTful关注的是“把系统能力抽象成资源,并通过统一的 HTTP 语义来操作资源”
所以这两者不是简单的“谁替代谁”,而是两种不同的服务设计思路。
1. 最核心的差别:一个偏方法调用,一个偏资源操作
如果以“下单”和“查询订单”为例:
1
2
3
4
5
6
7
8
// RPC 风格,更接近调用动作
/createOrder
/queryOrder?orderId=123
// RESTful 风格,更接近操作资源
POST /orders
GET /orders/123
DELETE /orders/123
这两种写法背后的建模方式不同:
- RPC 风格:重点是“我要调用哪个方法”,接口通常围绕动作展开,比如
createOrder()、cancelOrder()、queryOrderById() - RESTful 风格:重点是“我要操作哪个资源”,接口通常围绕资源展开,比如
orders、users、products
因此,RPC 更像“调用一个远程函数”,RESTful 更像“对一个资源做标准化操作”。
2. 对比时不要只看 URL,要看 5 个维度
| 维度 | RPC | RESTful |
|---|---|---|
| 建模方式 | 面向方法 / 面向过程 | 面向资源 |
| 协议语义 | 可以自定义,也可以基于 HTTP | 通常基于 HTTP 统一语义 |
| 接口表达 | 更像 queryOrder()、createUser() |
更像 GET /orders/123、POST /users |
| 可读性 | 对机器和 SDK 更友好 | 对人和调试工具更友好 |
| 适用场景 | 内部服务调用、追求效率和透明调用 | 对外开放接口、跨团队对接、强调通用性 |
这里最关键的一点是:
RESTful 的优势不只是 URL 好看,而是它把 HTTP 方法、资源路径、状态码、缓存语义这些能力一起纳入了接口设计。
例如:
GET表示读取资源POST表示创建资源PUT/PATCH表示更新资源DELETE表示删除资源
这种设计天然更适合开放 API、前后端分离接口和跨语言集成。
3. 为什么说 RPC 在内部调用里更常见
如果服务之间是内部调用,很多时候调用方更关心的是:
- 我要调哪个服务的方法
- 参数是什么
- 返回值是什么
- 能不能像本地接口一样直接调用
- 框架能不能帮我处理地址发现、超时、重试、负载均衡
这时 RPC 的表达通常更直接:
1
Order order = orderService.queryById(123L);
从业务代码看,它几乎就是一次本地方法调用。至于底层到底走的是:
- Dubbo 私有协议
- HTTP
- HTTP/2
- TCP 长连接
这些细节都由框架隐藏掉了。
而如果用 RESTful 风格,调用方更容易感知到“我是在发一个 HTTP 请求”,例如:
1
2
GET /orders/123 HTTP/1.1
Host: order-service.example.com
因此在“服务治理 + 接口代理 + 透明调用”这一类需求上,RPC 框架通常更占优势。
4. RESTful 的优势到底在哪里
RESTful 不是“性能不如 RPC 的次优方案”,它有自己非常明确的适用边界:
- 通用性更强:几乎任何语言、任何客户端都能直接发 HTTP 请求
- 可读性更好:路径、方法、状态码都更接近开放接口规范
- 生态更成熟:浏览器、网关、代理、缓存、调试工具都天然支持
- 边界更清晰:适合给前端、第三方平台、外部合作方暴露接口
所以 RESTful 更常出现在:
- 前后端接口
- 对外开放平台 API
- 第三方系统集成
- 需要借助 HTTP 标准能力的场景
5. RPC 和 RESTful 的关系,比较准确的结论是什么
比较准确的说法不是“RPC 比 RESTful 高级”或者“RESTful 比 RPC 规范”,而是:
- RESTful 更像一种基于 HTTP 的资源设计风格
- RPC 更像一种远程调用模型
如果只看内部微服务调用:
- 追求透明调用、性能、治理能力时,常常更偏向 RPC
- 追求通用性、可读性、开放性时,常常更偏向 RESTful
如果只看系统对外接口:
- RESTful 往往更自然
如果只看强类型服务间调用:
- RPC 往往更顺手
6. 一句话总结这部分
RPC 和 RESTful 的核心差别,不在于 URL 写法,而在于:
RPC以“远程方法调用”为中心组织接口,RESTful以“资源及其状态变化”为中心组织接口。
因此,两者并不是简单互斥关系,而是面向不同目标的两种接口设计思路。
既然有 HTTP 请求,为什么还要用 RPC 调用
HTTP 协议本身已经非常通用,以其中常见的 RESTful 风格为代表,它的优势很明显:可读性好,容易得到防火墙、网关和跨语言生态的支持,接口语义也更容易被直接观察和调试。
但 HTTP 也有明显局限,这些局限正好对应了它的优势:
- 报文里会携带较多 Header 和通用语义信息,有效业务数据的占比往往没有私有二进制协议高
- 在服务间频繁调用场景下,显式封装 URL、参数名、请求方法和响应解析会增加调用复杂度
- 如果目标是“像调用本地方法一样调用远程服务”,单纯依赖 HTTP 接口并不能天然提供这种调用抽象

图中的 HTTP 之所以出现两次,是因为它可能扮演两种不同角色:
- 作为一种直接暴露给调用方的接口风格,例如
RESTful API - 作为 RPC 框架底层承载消息的应用层协议,例如
gRPC、Dubbo Triple
因此,HTTP 和 RPC 既可能是并列比较关系,也可能是包含关系,关键取决于比较的是“跨应用调用方案”还是“RPC 通信过程中的底层承载协议”。
在分布式系统里,每个服务的边界通常都比较小,服务之间彼此调用是很常见的事情。这就会产生 Service A 调用 Service B 中某个方法的需求,也就是远程过程调用。
要让 Service A 调用 Service B 中的方法,最容易想到的做法就是直接通过 HTTP 请求来实现。例如 Service B 暴露一个 RESTful 接口,再由 Service A 去调用这个接口。
这种方式当然是可行的,而且也有非常明显的优点:
- 接口可读性好
- 更容易穿过防火墙、网关和代理体系
- 跨语言支持成熟
- 调试工具和生态更完善
但当这种调用大量发生在内部服务之间时,问题也会逐渐暴露出来:
- 每次调用都要显式封装 URL、参数名、请求方法和响应解析逻辑
- 对“像调用本地接口一样调用远程方法”这件事支持不够自然
- 在高频内部调用场景下,报文冗余和调用封装成本会变得更明显
- 负载均衡、服务发现、超时重试、熔断和监控等治理能力往往还需要额外补齐
也正是基于这种背景,RPC 才逐渐成为内部服务调用里非常常见的一类方案。它的重点并不只是“比 HTTP 更快”,而是希望把服务间调用抽象成更接近本地方法调用的工程模型。
通常,RPC 会要求调用方依赖被调用服务的接口定义或契约描述。这样一来,调用方看到的就不再是一个 URL,而是一个可以直接调用的方法入口。
为了实现“像调用内部接口一样调用远程方法”,框架一般会沿着下面这条链路工作:
- 调用方依赖接口或 IDL 生成的客户端桩代码
- 代理对象拦截方法调用并组装远程请求
- 框架完成服务发现、序列化、协议编码和网络发送
- 服务端解码后执行本地实现,再将结果返回客户端

因此,RPC 的优势主要集中在内部服务调用场景,而不是要全面替代 HTTP API。
如果只看结论,可以概括为:
- 对外开放接口、浏览器访问、第三方集成,
HTTP/RESTful往往更自然 - 内部高频服务调用、强类型契约、统一治理需求,RPC 框架通常更合适
HTTP可以是 RPC 的承载协议,因此两者不是简单替代关系
总结:
“既然有 HTTP 请求,为什么还要 RPC”这个问题,答案不在于谁绝对更先进,而在于两者优化的目标不同:HTTP/RESTful 更强调通用性、可读性和生态兼容;RPC 更强调调用抽象、契约表达、治理能力和内部服务协同效率。