首先需要有处理网络连接通讯的模块,负责连接建立、管理和消息的传输。其次需要有编解码的模块,因为网络通讯都是传输的字节码,需要将我们使用的对象序列化和反序列化。剩下的就是客户端和服务器端的部分,服务器端暴露要开放的服务接口,客户调用服务接口的一个代理实现,这个代理实现负责收集数据、编码并传输给服务器然后等待结果返回。

这段描述勾勒出 RPC 的四大件:网络通讯、编解码、服务端暴露、客户端代理。本文把它们串成一次完整调用,并补上生产环境绕不开的两块:服务发现与负载均衡。

RPC 解决什么问题

微服务拆开后,订单服务要调用户服务,两者在不同进程甚至不同机器。直接写 Socket 代码意味着每个调用点都要处理连接、字节拼装、超时重试——重复且易错。RPC 的目标是:让远程调用看起来像本地方法调用,把网络细节全部藏进框架层。

一次调用的完整链路

客户端侧:

  1. 动态代理:调用方持有服务接口的代理对象,方法调用被拦截
  2. 封装请求:接口名、方法名、参数类型、参数值打包成 Request 对象
  3. 序列化:Request → 字节流(Hessian/Protobuf/Kryo,选型权衡可读性与体积性能)
  4. 网络传输:通过长连接(Netty)发送,记录 requestId 用于匹配响应

服务端侧:

  1. 反序列化还原 Request,按接口名+方法名在本地服务注册表找到实现类
  2. 反射调用本地实现,得到结果
  3. 结果序列化回传,客户端按 requestId 从 Future 表中唤醒等待线程,拿到返回值

整条链路对调用方只表现为一次普通的方法调用+返回,异常则被包装成远程异常抛出。

服务发现与负载均衡

生产环境服务端是多实例的,客户端怎么知道调谁?

  • 注册中心:服务端启动时把 接口名 → ip:port 注册到 Zookeeper/Nacos;客户端订阅并缓存列表,实例上下线实时推送
  • 负载均衡:客户端从列表中按策略选一个(轮询、随机、加权、一致性哈希),配合熔断降级(失败快速切换实例或返回兜底)

这两块是 RPC 框架与”玩具 demo”的分水岭:Dubbo 的目录-路由-负载均衡三层、gRPC 的 naming + LB policy,本质都在解决”列表从哪来、选哪个、挂了怎么办”。

手写最小 RPC 骨架

用 JDK 代理 + Socket 几十行即可跑通核心:

// 客户端:代理把方法调用变成网络请求
Object proxy = Proxy.newProxyInstance(cl, new Class[]{UserService.class},
(p, method, args) -> {
Request req = new Request(UserService.class.getName(),
method.getName(), method.getParameterTypes(), args);
return send(req); // 序列化 → socket 写 → 读响应 → 反序列化
});

// 服务端:反射分发
ServerSocket ss = new ServerSocket(8080);
while (true) socketPool.submit(() -> {
Request req = read(socket);
Object svc = registry.get(req.getInterfaceName());
Object ret = svc.getClass().getMethod(req.getMethod(), req.getParamTypes())
.invoke(svc, req.getArgs());
write(socket, ret);
});

跑通这个骨架后,再看 Dubbo 的协议头设计、Netty 线程模型、心跳与重连,都是在它之上补”生产级”三个字。

RPC 与 HTTP REST 的取舍

  • RPC:二进制协议+长连接,性能高、接口契约强(IDL/接口),适合内部服务间高频调用
  • REST:文本协议+无状态,通用性好、跨语言调试方便,适合对外开放 API

两者不是替代关系:内部走 RPC、边界走网关转 REST,是常见组合。

RPC 原理并不神秘:代理隐藏调用、序列化隐藏数据格式、网络层隐藏传输、注册中心隐藏地址。四层抽象叠起来,”远程”二字才对业务透明。