本文目录导读:

手把手教你实现一个简单的RPC调用框架:从零搭建分布式通信核心
目录导读
- RPC基础概念与原理——理解远程过程调用的本质
- 技术选型与架构设计——选择最适合的通信与序列化方案
- 核心代码实现步骤——动态代理、网络传输、协议封装
- 完整示例与测试——服务端与客户端的联调验证
- 常见问题与优化方向——性能、可靠性与扩展性
先问先答:为什么需要自己实现RPC框架?
因为理解底层原理后才能灵活处理生产环境中的超时、重试、负载均衡等复杂问题,直接使用成熟的框架(如gRPC)虽快,但遇到定制化需求时容易陷入“黑盒焦虑”。
RPC基础概念与原理
RPC(Remote Procedure Call) 的核心是“像调用本地方法一样调用远程服务”,它必须解决两个关键问题:
- 网络通信:客户端与服务端如何传输数据?
- 序列化/反序列化:内存中的对象如何变成字节流?
1 一次RPC调用的完整流程
客户端(Client) 服务端(Server)
│ │
├─● 动态代理生成Stub对象 │
├─● 序列化方法名+参数为字节流 │
├─● 通过网络发送请求(如TCP) │
│ ├─● 接收请求并反序列化
│ ├─● 根据方法名调用本地实现
│ ├─● 序列化返回结果
├─● 接收响应并反序列化 │
├─● 返回结果给调用者 │
└─● 完成调用 └─● 完成处理
问答环节:RPC与RESTful API有何本质区别?
答:RPC更关注方法调用和性能(二进制协议),RESTful更关注资源语义(HTTP文本协议),RPC常用于内部服务间通信,RESTful更适合对外开放接口。
技术选型与架构设计
在实现简单框架时,我们需要选择最小依赖且易于理解的组合:
| 组件 | 推荐选择 | 原因 |
|---|---|---|
| 传输协议 | 原生Java Socket(TCP) | 无额外依赖,便于理解底层 |
| 序列化方案 | JDK原生序列化或JSON | JSON调试方便,JDK实现最简单 |
| 代理生成 | JDK动态代理 | 只支持接口代理,满足基本需求 |
| 服务注册与发现 | 手动写死IP+端口(简化) | 重点在于核心通信流程 |
1 模块划分
simple-rpc-framework/
├─ api/ # 公共接口定义(服务契约)
├─ server/ # 服务端:暴露服务、处理请求
└─ client/ # 客户端:动态代理、发送请求
问答环节:为什么不选择Netty作为传输层?
答:Netty虽性能卓越,但会增加学习曲线,本文聚焦“最小化原理演示”,原生Socket足以说明核心逻辑,实际生产环境可替换为Netty。
核心代码实现步骤
1 定义公共API接口
public interface HelloService {
String sayHello(String name);
}
2 服务端实现
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String name) {
return "Hello, " + name + "!";
}
}
3 实现RPC服务器(暴露服务)
核心逻辑:
- 监听指定端口(如9090)
- 接收客户端传来的方法名+参数
- 通过反射调用本地实现类
- 序列化返回结果
// 简化版服务端代码片段
try (ServerSocket server = new ServerSocket(9090)) {
while (true) {
try (Socket socket = server.accept()) {
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
String methodName = input.readUTF(); // 读取方法名
Class<?>[] paramTypes = (Class<?>[]) input.readObject();
Object[] args = (Object[]) input.readObject();
// 假设service持有HelloServiceImpl实例
Method method = service.getClass().getMethod(methodName, paramTypes);
Object result = method.invoke(service, args);
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
output.writeObject(result);
}
}
}
4 客户端动态代理实现
使用JDK动态代理拦截对接口的调用,转换为网络请求:
public class RpcClientProxy {
public static <T> T create(Class<T> clazz, String host, int port) {
return (T) Proxy.newProxyInstance(
clazz.getClassLoader(),
new Class[]{clazz},
(proxy, method, args) -> {
try (Socket socket = new Socket(host, port)) {
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
output.writeUTF(method.getName());
output.writeObject(method.getParameterTypes());
output.writeObject(args);
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
return input.readObject();
}
}
);
}
}
关键点解析:
- 代理对象会拦截
proxy.sayHello("World")调用,自动跳入invoke方法- 每次调用都会建立一次TCP连接(生产环境需改用连接池)
完整示例与测试
1 启动服务端
public class ServerBoot {
public static void main(String[] args) {
RpcServer server = new RpcServer();
server.register(HelloService.class, new HelloServiceImpl());
server.start(9090); // 循环监听
}
}
2 客户端调用
public class ClientBoot {
public static void main(String[] args) {
HelloService proxy = RpcClientProxy.create(HelloService.class, "127.0.0.1", 9090);
String result = proxy.sayHello("Architect"); // 实际触发网络通信
System.out.println(result); // 输出: Hello, Architect!
}
}
3 验证流程
- 先运行
ServerBoot,终端进入等待连接状态 - 运行
ClientBoot,观察到控制台输出Hello, Architect! - 服务端可打印接收到的请求参数,验证数据正确传输
问答环节:如果服务端挂了,客户端会怎样?
答:客户端会抛出ConnectException,实际生产框架需要实现重试机制、超时控制和服务发现故障剔除。
常见问题与优化方向
1 当前框架的局限
| 问题 | 简单实现表现 | 优化方案 |
|---|---|---|
| 性能 | 每次调用创建TCP连接 | 改用长连接复用(如连接池) |
| 传输效率 | JDK序列化体积大、速度慢 | 使用Protobuf或Kryo |
| 服务发现 | IP/Port硬编码 | 引入ZooKeeper或Consul |
| 负载均衡 | 不支持 | 客户端侧轮询/随机策略 |
| 异常处理 | 网络异常直接抛出 | 熔断、降级、异步回调 |
2 生产级框架演进思路
阶段1:当前实现(教学演示)
↓
阶段2:引入Netty+NIO(高性能网络层)
↓
阶段3:集成ZooKeeper实现自动服务发现
↓
阶段4:添加Hystrix实现熔断与限流
↓
阶段5:扩展为全异步、支持流式调用
最后思考:你会在生产中使用纯手写的RPC框架吗?
答:不会,但理解手写框架能让你在阅读gRPC或Dubbo源码时,一眼看透他们的设计骨架——动态代理、序列化、传输协议,万变不离其宗。
本文通过不到200行核心代码,实现了RPC框架最基础的功能,关键在于理解“动态代理包装网络通信”这个核心设计模式,建议读者在复现代码后,尝试替换序列化为JSON、增加异常重试等小功能,以加深对分布式系统设计的理解。