每天一点源代码:rpc/dubbo
1、什么是RPC、什么 是远程调用
简单的说本机上内部的方法调用都可以称为本地过程调用,而远程过程调用实际上就指的是你本地调用了远程机子上的某个方法,这就是远程过程调用。
所以说 RPC 对标的是本地过程调用,至于 RPC 要如何调用远程的方法可以走 HTTP、也可以是基于 TCP 自定义协议。所以说你讨论 RPC 和 HTTP 就不是一个层级的东西。而 RPC 框架就是要实现像那小助手一样的东西,目的就是让我们使用远程调用像本地调用一样简单方便,并且解决一些远程调用会发生的一些问题,使用户用的无感知、舒心、放心、顺心,它好我也好,快乐没烦恼。
2、领域划分为消费者和生产者,涉及到本地和远程方
2.1、消费者需要知道哪些接口可以调用
我们先从消费者方(也就是调用方)来看需要些什么,首先消费者面向接口编程,所以需要得知有哪些接口可以调用,可以通过公用 jar 包的方式来维护接口。todo//
2.2、领域划分
-
代理角色(框架)
代理框架其他功能
当然还需要有容错机制,毕竟这是远程调用,网络是不可靠的,所以可能需要重试什么的。还要和服务提供方约定一个协议,例如我们就用 HTTP 来通信就好啦,也就是大家要讲一样的话,不然可能听不懂了。当然序列化必不可少,毕竟我们本地的结构是“立体”的,需要序列化之后才能传输,因此还需要约定序列化格式。并且这过程中间可能还需要掺入一些 Filter,来作一波统一的处理,例如调用计数啊等等。这些都是框架需要做的,让消费者像在调用本地方法一样
-
注册中心角色
这样调用方从注册中心可以知晓可以调用哪些服务提供方,一般而言提供方不止一个,毕竟只有一个挂了那不就没了。大家在上面暴露自己的服务,也在上面得知自己能调用哪些服务。当然还能做配置中心,将配置集中化处理,动态变更通知订阅者。
-
服务提供者
反序列化请求到服务
然后有消费者请求过来需要处理,提供者需要用和消费者协商好的协议来处理这个请求,然后做反序列化。
序列化完的请求应该扔到线程池里面做处理,某个线程接受到这个请求之后找到对应的实现调用,然后再将结果原路返回。
3、手写Demo
3.1RPC框架远程调用领域划分
-
接受消费端调用信息,反序列化、反射调用消费端
socket监听接收消息
1行
public static void export(Object service, int port) throws Exception {
ServerSocket server = new ServerSocket(port);
while(true) {
Socket socket = server.accept();
new Thread(new Runnable() {
//反序列化
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
String methodName = input.read(); //读取方法名
Class<?>[] parameterTypes = (Class<?>[]) input.readObject(); //参数类型
Object[] arguments = (Object[]) input.readObject(); //参数
Method method = service.getClass().getMethod(methodName, parameterTypes); //找到方法
Object result = method.invoke(service, arguments); //调用方法
// 返回结果
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
output.writeObject(result);
}).start();
}
}
2. 注册消费者位置,和调用参数 socket客户端发送消息和参数过去
1行
public static <T> T refer (Class<T> interfaceClass, String host, int port) throws Exception {
return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class<?>[] {interfaceClass},
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
Socket socket = new Socket(host, port); //指定 provider 的 ip 和端口
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
output.write(method.getName()); //传方法名
output.writeObject(method.getParameterTypes()); //传参数类型
output.writeObject(arguments); //传参数值
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
Object result = input.readObject(); //读取结果
return result;
}
});
}
4Dubbo
4.1 dubbo架构图
类似手写Demo的过程
-
服务提供者 Provider 启动然后向注册中心注册自己所能提供的服务。 -
服务消费者 Consumer 启动向注册中心订阅自己所需的服务。然后注册中心将提供者元信息通知给 Consumer, 之后 Consumer 因为已经从注册中心获取提供者的地址,因此可以通过负载均衡选择一个 Provider 直接调用 。 -
服务提供方元数据变更的话注册中心会把变更推送给服务消费者。 -
服务提供者和消费者都会在内存中记录着调用的次数和时间,然后定时的发送统计数据到监控中心。
5、注册中心领域问题
5.1、监控中心和注册中心必须吗?注册中心宕机了有影响吗
首先注册中心和监控中心是可选的,你可以不要监控,也不要注册中心,直接在配置文件里面写然后提供方和消费方直连。然后注册中心、提供方和消费方之间都是长连接,和监控方不是长连接,并且消费方是直接调用提供方,不经过注册中心。就算注册中心和监控中心宕机了也不会影响到已经正常运行的提供者和消费者,因为消费者有本地缓存提供者的信息。
6、Dubbo领域划分
大的三层分别为 Business(业务层)、RPC 层、Remoting,并且还分为 API 层和 SPI 层。
6.1、SPI层的好处有哪些
我再稍微提一下 SPI(Service Provider Interface),是 JDK 内置的一个服务发现机制,它使得接口和具体实现完全解耦。我们只声明接口,具体的实现类在配置中选择。具体的就是你定义了一个接口,然后在META-INF/services目录下放置一个与接口同名的文本文件,文件的内容为接口的实现类,多个实现类用换行符分隔。这样就通过配置来决定具体用哪个实现!
6.2、服务提供者参照层次划分的工作流程
包含执行体包装的过程和暴露给注册中心包装的过程
1、首先 Provider 启动,通过 Proxy 组件根据具体的协议Protocol 将需要暴露出去的接口封装成 Invoker,Invoker 是 Dubbo 一个很核心的组件,代表一个可执行体。
2、然后再通过 Exporter 包装一下,这是为了在注册中心暴露自己套的一层,然后将 Exporter 通过 Registry 注册到注册中心。这就是整体服务暴露过程。
6.3、消费过程
-
消费者启动会向注册中心拉取服务提供者的元信息,然后调用流程也是从 Proxy 开始,毕竟都需要代理才能无感知。 -
Proxy 持有一个 Invoker 对象,调用 invoke 之后需要通过 Cluster 先从 Directory 获取所有可调用的远程服务的 Invoker 列表,如果配置了某些路由规则,比如某个接口只能调用某个节点的那就再过滤一遍 Invoker 列表。 -
剩下的 Invoker 再通过 LoadBalance 做负载均衡选取一个。然后再经过 Filter 做一些统计什么的,再通过 Client 做数据传输,比如用 Netty 来传输。 -
传输需要经过 Codec 接口做协议构造,再序列化。最终发往对应的服务提供者。 -
服务提供者接收到之后也会进行 Codec 协议处理,然后反序列化后将请求扔到线程池处理。某个线程会根据请求找到对应的 Exporter ,而找到 Exporter 其实就是找到了 Invoker,但是还会有一层层 Filter,经过一层层过滤链之后最终调用实现类然后原路返回结果。
ref:https://juejin.im/post/6870276943448080392