面试官让我手写一个RPC框架

如今,分布式系统大行其道,RPC 有着举足轻重的地位。Dubbo、Thrift、gRpc 等框架各领风骚,学习RPC是新手也是老鸟的必修课。本文带你手撸一个rpc-spring-starter,深入学习和理解rpc相关技术,包括但不限于 RPC 原理、动态代理、Javassist 字节码增强、服务注册与发现、Netty 网络通讯、传输协议、序列化、包压缩、TCP 粘包、拆包、长连接复用、心跳检测、SpringBoot 自动装载、服务分组、接口版本、客户端连接池、负载均衡、异步调用等知识, 值得收藏。

RPC定义

远程服务调用(Remote procedure call)的概念历史已久,1981年就已经被提出,最初的目的就是为了 调用远程方法像调用本地方法一样简单 ,经历了四十多年的更新与迭代,RPC 的大体思路已经趋于稳定,如今百家争鸣的 RPC 协议和框架,诸如 Dubbo (阿里)、Thrift(FaceBook)、gRpc(Google)、brpc (百度)等都在不同侧重点去解决最初的目的,有的想极致完美,有的追求极致性能,有的偏向极致简单。

RPC基本原理

让我们回到 RPC 最初的目的,要想实现 调用远程方法像调用本地方法一样简单 ,至少要解决如下问题:

  1. 如何获取可用的远程服务器
  2. 如何表示数据
  3. 如何传递数据
  4. 服务端如何确定并调用目标方法

上述四点问题,都能与现在分布式系统火热的术语一一对应,如何获取可用的远程服务器(服务注册与发现)、如何表示数据(序列化与反序列化)、如何传递数据(网络通讯)、服务端如何确定并调用目标方法(调用方法映射)。笔者将通过一个简单 RPC 项目来解决这些问题。

首先来看 RPC 的整体系统架构图:

图中服务端启动时将自己的服务节点信息注册到注册中心,客户端调用远程方法时会订阅注册中心中的可用服务节点信息,拿到可用服务节点之后远程调用方法,当注册中心中的可用服务节点发生变化时会通知客户端,避免客户端继续调用已经失效的节点。那客户端是如何调用远程方法的呢,来看一下远程调用示意图:

  1. 客户端模块代理所有远程方法的调用
  2. 将目标服务、目标方法、调用目标方法的参数等必要信息序列化
  3. 序列化之后的数据包进一步压缩,压缩后的数据包通过网络通信传输到目标服务节点
  4. 服务节点将接受到的数据包进行解压
  5. 解压后的数据包反序列化成目标服务、目标方法、目标方法的调用参数
  6. 通过服务端代理调用目标方法获取结果,结果同样需要序列化、压缩然后回传给客户端

通过以上描述,相信读者应该大体上了解了 RPC 是如何工作的,接下来看如何使用代码具体实现上述的流程。鉴于篇幅笔者会选择重要或者网络上介绍相对较少的模块来讲述。

RPC实现细节

1\. 服务注册与发现

作为一个入门项目,我们的系统选用 Zookeeper 作为注册中心, ZooKeeper 将数据保存在内存中,性能很高。在读多写少的场景中尤其适用,因为写操作会导致所有的服务器间同步状态。服务注册与发现是典型的读多写少的协调服务场景。Zookeeper 是一个典型的CP系统,在服务选举或者集群半数机器宕机时是不可用状态,相对于服务发现中主流的AP系统来说,可用性稍低,但是用于理解RPC的实现,也是绰绰有余。

ZooKeeper节点介绍

  • 持久节点( PERSISENT ):一旦创建,除非主动调用删除操作,否则一直持久化存储。
  • 临时节点( EPHEMERAL ):与客户端会话绑定,客户端会话失效,这个客户端所创建的所有临时节点都会被删除除。
  • 节点顺序( SEQUENTIAL ):创建子节点时,如果设置SEQUENTIAL属性,则会自动在节点名后追加一个整形数字,上限是整形的最大值;同一目录下共享顺序,例如(/a0000000001,/b0000000002,/c0000000003,/test0000000004)。

ZooKeeper服务注册

在 ZooKeeper 根节点下根据服务名创建持久节点 /rpc/{serviceName}/service ,将该服务的所有服务节点使用临时节点创建在 /rpc/{serviceName}/service 目录下,代码如下(为方便展示,后续展示代码都做了删减):

public void exportService(Service serviceResource) {
  String name = serviceResource.getName();
  String uri = GSON.toJson(serviceResource);
  String servicePath = "rpc/" + name + "/service";
  zkClient.createPersistent(servicePath, true);
  String uriPath = servicePath + "/" + uri;
  //创建一个新的临时节点,当该节点宕机会话失效时,该临时节点会被清理
  zkClient.createEphemeral(uriPath);
}

注册效果如图,本地启动两个服务则 service 下有两个服务节点信息:

存储的节点信息包括服务名,服务 IP:PORT ,序列化协议,压缩协议等。

ZooKeeper服务发现

客户端启动后,不会立即从注册中心获取可用服务节点,而是在调用远程方法时获取节点信息(懒加载),并放入本地缓存 MAP 中,供后续调用,当注册中心通知目录变化时清空服务所有节点缓存,代码如下:

public List<Service> getServices(String name) {
  Map<String, List<Service>> SERVER_MAP = new ConcurrentHashMap<>();
  String servicePath = "rpc/" + name + "/service";
  List<String> children = zkClient.getChildren(servicePath);
  List<Service> serviceList = Optional.ofNullable(children).orElse(new ArrayList<>()).stream().map(str -> {
    String deCh = URLDecoder.decode(str, StandardCharsets.UTF_8.toString());
    return gson.fromJson(deCh, Service.class);
  }).collect(Collectors.toList());
  SERVER_MAP.put(name, serviceList);
  return serviceList;
}
public class ZkChildListenerImpl implements IZkChildListener {
    //监听子节点的删除和新增事件
    @Override
    public void handleChildChange(String parentPath, List<String> childList) throws Exception {
        //有变动就清空服务所有节点缓存
        String[] arr = 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值