一个粗糙的RPC框架设计思路
如果让你写一个实现 1+1=? 的需求,你会怎么做?
第一步,定义一个接口。
public interface Calculator {
Long add(Long a,Long b);
}
第二步,实现接口
public class CalculatorImpl implements Calculator {
@Override
public Long add(Long a,Long b){
return a + b;
}
}
第三步,使用
Calculator calculator = new CalculatorImpl();
//2
System.out.println(calculator.add(1L,1L));
而RPC简单来讲就是远程调用,那么调用方(客户端)和被调用方(服务端)就要分开部署了。
为了保证服务高可用,我们在增加几个服务实例。在服务数量少的时候,每台服务器进行多实例部署也不是什么大问题,客户端进行相应的配置修改即可。
而当服务实例、服务类型越来越多的时候就可以引入注册中心去管理服务了。因为客户端并不知道服务实例有多少,而且,即便手动配置上服务地址,那么在服务实例增加或者宕机下线的时候在客户端也是难以维护的。所以通过注册中心统一做服务管理,客户端只需要知道注册中心的地址即可。
服务端启动流程:
- 服务启动
- 注册服务实例
- 维持心跳
客户端启动流程
- 客户端启动
- 注册客户端实例
- 拉取服务列表
- 维持心跳
到这里一个简单的RPC框架的骨架就初步成型了。
服务实例可以通过哪些方式对外提供服务呢?
- 提供HTTP接口,使用HTTP协议。使用简单,市面流行
- 自定义协议,灵活性较高,性能较HTTP更好一些
使用HTTP协议交互就不用说了,像SpringCloud提供了一整套完整的框架,包括服务发现,注册,服务降级,负载均衡,网关等这里不再赘述
而自定义协议会更灵活一些,这里就涉及到了一些网路底层的知识了,因为要自己处理编解码和粘包拆包的工作。比如设计一个简单的协议格式如下:
这里只是简单地做了一个设计,比如一个RPC请求包可以转化成这样:
XXXX 0001 (魔法值 版本号)
00000000 00000000 00000000 00000000 (固定头,每一位可以设计指定不同的值代表不同的含义,比如存放一些请求ID,请求类型等数据)
00000000 00000000 00000000 00000000 (body长度 64位)
00000000 00000000 00000000 00000010 (body长度 64位,比如此值为2个字节)
01010001 01010101 (两个字节的body)
1 (固定1位的结束字符)
所以在服务实例启动的时候,不仅要注册服务,还要对外开放一个TCP端口,此端口用于接收和处理客户端的请求。另外,客户端和服务器还要有相应的编解码处理器。
网络通信这一块可以选用Netty等网络框架来实现。
这里在客户端还有一个细节要处理,就是我们发出去的每个请求都要有对应的请求序号,响应请求体中也会带着请求序号,这样在多个请求并发执行的时候,客户端可以使用一个Map<String,Request>结构来存储正在进行中的请求,当获得响应之后,就可以将该请求移除然后做后续的操作。另外还可以开启线程扫描该Map,做超时检查之类的操作.
到最后,我们还可以在加一个监控中心对服务端和客户端进行监控或者对服务调用情况做一个统计,例如调用频次计数,调用延迟,错误率等等.
到此为止,一个粗糙的RPC框架就成型啦!拜了个拜。