深入浅出 RPC:从"传参调用"本质到 gRPC 实战
在日常开发中,我们经常听到"微服务"、"RPC"、"HTTP 接口"这些词。很多开发者对 RPC 的认知停留在"像调用本地方法一样调用远程服务"这句口号上,却说不清它到底解决了什么痛点,为什么大厂内部偏爱 RPC,以及它和 HTTP 到底有什么区别。
今天,我们就从"传参调用"这个最底层的本质出发,彻底聊透 RPC。
一、 RPC 是什么?它的本质就是"传参调用"
RPC(Remote Procedure Call,远程过程调用),顾名思义,就是让程序能够像调用本地函数一样,去调用另一台服务器上的函数。
剥开所有框架(Dubbo、gRPC、Thrift)高大上的外衣,RPC 的底层逻辑极其简单:它本质上就是一次跨网络的"传参调用"。
它必然包含以下几个步骤:
- 序列化:客户端把要传递的参数(对象)转换成字节流。
- 网络传输:通过 TCP/HTTP 等协议把字节流发给服务端。
- 反序列化:服务端收到字节流,还原成对象。
- 执行代码:服务端执行具体的业务逻辑。
- 返回结果:把结果序列化后原路返回给客户端。
所以,RPC 并没有什么黑魔法,它就是个"打包参数 -> 发网络请求 -> 解包参数 -> 执行 -> 打包结果 -> 返回"的自动化流程。
二、 为什么需要 RPC?没有它之前我们怎么做的?
1. RPC 解决了什么问题?
随着业务发展,单体应用被拆分成微服务。服务 A 需要调用服务 B 的功能。RPC 主要解决了以下核心痛点:
- 降低沟通与开发成本:屏蔽了底层网络通信(TCP 粘包、连接池管理、序列化)的复杂性,让开发者只关注业务逻辑。
- 强类型约束(契约):通过 IDL(接口定义语言)强制约束接口,避免"接口对不上"的线上事故。
- 高性能:通常使用二进制序列化(如 Protobuf),体积小、解析快,远胜于文本格式。
- 服务治理:优秀的 RPC 框架自带负载均衡、超时重试、熔断限流等能力。
2. 没有 RPC 框架时,类似问题如何解决?
在 RPC 框架普及之前,或者在没有引入重型 RPC 框架的简单场景下,开发者通常使用以下方式:
- 原生 Socket 编程:自己建立 TCP 连接,自己处理粘包/拆包,自己写对象转字节流的逻辑。开发效率极低,极易出 Bug。
- HTTP + JSON (RESTful) :这是最常见的替代方案。服务 A 通过
HttpClient或RestTemplate发送 HTTP 请求,传递 JSON 文本。这种方式简单通用,但存在性能瓶颈(文本解析慢、HTTP/1.1 队头阻塞)且缺乏强类型约束。 - WebService (SOAP/XML):早期的跨语言方案,基于 XML 和 HTTP。极其笨重,现在已基本被淘汰。
RPC 框架的出现,就是将上述"手工作坊"式的网络调用,升级成了"标准化流水线",同时保留了类似本地调用的开发体验。
三、 RPC 与 HTTP 的终极对比
很多初学者会疑惑:"gRPC 底层不也是 HTTP/2 吗?那它和普通的 HTTP 接口有什么区别?"
我们要区分**"传输协议"和"应用层调用范式"**。普通 HTTP(RESTful)和 gRPC 本质都是传参调用,但它们的封装方式截然不同:
| 对比维度 | 传统 HTTP (RESTful + JSON) | RPC (以 gRPC 为例) |
|---|---|---|
| 底层协议 | 默认 HTTP/1.1(可升级 HTTP/2) | 强制使用 HTTP/2 |
| 数据载体 | JSON 文本(体积大,解析慢) | Protobuf 二进制(体积小,解析极快) |
| 契约模式 | 弱类型(靠 Swagger 文档约定) | 强类型 (靠 .proto 文件生成代码) |
| 调用体验 | 拼 URL、解析 JSON、处理状态码 | 像调用本地方法 (stub.getUser()) |
| 调试难度 | 极低(浏览器/Postman 直接看) | 较高(需专用工具解码二进制流) |
| 适用场景 | 对外 API、前后端交互 | 内部微服务间高频通信 |
一句话总结:HTTP 是互联网的"普通话"(通用、易懂),RPC 是公司内部的"黑话"(高效、严谨)。
四、 代码实战:gRPC 如何实现"像本地调用一样"
我们以实现一个**"根据 ID 查询用户信息"**的功能为例,对比 HTTP 和 gRPC 的实现差异。
方案一:传统 HTTP (RESTful + Spring Boot)
服务端:直接写 Controller,返回 JSON。
java
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return new User(id, "张三"); // 返回 JSON: {"id":1,"name":"张三"}
}
}
客户端 :使用 RestTemplate 发送网络请求。
java
RestTemplate restTemplate = new RestTemplate();
// 需要手动拼 URL,指定返回类型
User user = restTemplate.getForObject("http://localhost:8080/api/users/1", User.class);
方案二:gRPC (RPC)
第一步:定义契约 (Schema) 必须先写 .proto 文件,这是 gRPC 的核心。
protobuf
syntax = "proto3";
option java_package = "com.example.grpc";
message UserRequest { int64 id = 1; }
message UserResponse { int64 id = 1; string name = 2; }
service UserService {
rpc GetUser (UserRequest) returns (UserResponse);
}
编译后,自动生成 Java 的 UserRequest、UserResponse 以及 UserServiceGrpc 存根类。
服务端:实现自动生成的接口。
java
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
@Override
public void getUser(UserRequest request, StreamObserver<UserResponse> responseObserver) {
// 构建响应
UserResponse response = UserResponse.newBuilder()
.setId(request.getId())
.setName("张三 (gRPC)")
.build();
// 返回结果
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
客户端 :注意看这里的调用体验!
java
// 1. 建立连接(底层是 HTTP/2)
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 9090).usePlaintext().build();
// 2. 获取 Stub(存根)
UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(channel);
// 3. 发起调用:完全就像调用本地方法!
UserRequest request = UserRequest.newBuilder().setId(1L).build();
UserResponse response = stub.getUser(request);
System.out.println(response.getName()); // 输出: 张三 (gRPC)
五、 总结与选型建议
通过上面的代码可以看出,gRPC 最大的魔力在于代码生成器(Code Generator) 。它把序列化、网络传输全部藏在生成的 Stub 背后,让开发者产生了"调用本地代码"的错觉。
作为开发者,我们该如何选型?
- 对内(微服务间) :首选 RPC(如 gRPC、Dubbo) 。利用其强契约、高性能、多语言协作的优势。但切记,RPC 不代表不用管超时和重试,因为网络永远是不可靠的,必须做好幂等设计。
- 对外(第三方 API)或 前后端交互 :首选 HTTP + RESTful。因为浏览器原生支持,调试极其方便,且外部开发者无需安装你的 SDK 即可接入。
技术没有银弹,只有场景匹配。 理解了 RPC 是"强契约、高性能的自动化传参调用",你就能在架构设计中游刃有余地做出选择。