本文学习的代码地址:https://github.com/Snailclimb/guide-rpc-framework 本文代码地址:https://gitee.com/uamaa/msc-rpc-framework
这篇文章打算先看懂原作者guide的第一版代码(实现一个最简单的RPC框架),再自己动手实现一遍
1 看懂原作者的代码
先下载第一版代码,运行起来。运行方式:先运行服务端,再运行客户端
如何下载历史版本代码见:【git】在GitHub上下载历史版本
鉴于只有服务端和客户端可以运行,先从这两个函数着手看
https://blog.csdn.net/qq_38939822/article/details/123301409根据这篇文章的前半段,设计RPC最主要是考虑注册中心、序列化、网络传输。那么我们在看代码的时候重点关注一下这三个问题
1.1 客户端代码
现在看的是example-client 文件夹下的RpcFrameworkSimpleMain
1.1.1 第一句
实例化了一个代理,此处我心里有两个疑问:1.代理在RPC里有什么用?2.实例化代理为啥要传地址和端口号进去呢? 带着这两个疑问,点开RpcClientProxy 类
RpcClientProxy类
属性:地址和端口号 方法0:全参构造 方法1:getProxy 看不懂先放着 方法2:invoke调用
invoke方法
代理类都要实现InvocationHandler接口,重写invoke方法 从官方文档https://docs.oracle.com/javase/8/docs/api/中找到invoke方法 输入参数: proxy - 调用该方法的代理实例 method - 对应于代理实例上调用的接口方法的 Method 实例。(说人话就是要调用的方法) args - 要调用method需要传入的参数 返回值: 从代理实例上的方法调用返回的值。(从代理实例上做方法调用这个下面会讲) 这个方法实现了什么功能,等我们看完里面有什么语句就知道了 第一句:实例化了一个RpcRequest,点进去看看
RpcRequest类
四个属性:类名、方法名、返回值类型、参数类型。这四个属性可以唯一确定一个方法。 所以这个类的功能是描述一个方法(或者说服务)
RpcRequest rpcRequest = RpcRequest.builder().methodName(method.getName())
.parameters(args)
.interfaceName(method.getDeclaringClass().getName())
.paramTypes(method.getParameterTypes())
.build();
这个实例化语句有点特别,查了一下,RpcRequest类两个注解都是lombok的,第一个注解@Data 是生成get/set方法,第二个注解@Builder 就可以让实例化像这样循环调用实现,很简单
@Builder:https://cloud.tencent.com/developer/article/1477728
RpcRequest类还实现了Serializable,这个后面会讲
第二句:实例化RpcClient,点进去看看
RpcClient类
里面只有一个方法就是发送RPC请求 输入参数:希望调用的服务、IP地址、端口号
仔细看看客户端发送请求是怎么做的? 1.用IP地址和端口号实例化一个Socket,这里应该是网络传输的内容,可能看不太懂。 https://blog.csdn.net/alrdy/article/details/7718174 看了一下是:Socket要建立连接,客户端要先实例化输出流,再实例化输入流 【这里明确了该RPC框架的网络传输协议是TCP里的socket】 2.客户端将希望调用的服务RpcRequest作为输出流传给服务端(RpcRequest务实现了Serializable,可以序列化成二进制,所以可以作为输出流在网络上传输)【这里明确了该RPC框架的序列化方式是Java自带的Serializable】 3.实例化输入流,从输入流里的内容读取对象,并将对象作为返回值返回
第三句:将客户端发送请求的返回值作为返回值返回 因为这里客户端发送请求需要传入地址和端口号,所以代理类需要传入这两个属性,解决了第一个疑问
1.1.2 第二句
后半句调用了之前略过的getProxy方法,现在点进去看看
RpcClientProxy类的getProxy方法
直接return的Proxy.newProxyInstance方法,我们在文档里查这个方法是干嘛的https://docs.oracle.com/javase/8/docs/api/
newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
输入参数: loader - 定义代理类的类加载器 interfaces - 代理类要实现的接口列表 h - 将方法调用分派到的调用处理程序 输出: 具有代理类的指定调用处理程序的代理实例,该代理类由指定的类加载器定义并实现指定的接口
就是说:生成了一个代理类的代理实例,这个代理类呢,是由指定的类加载器定义(第一个输入参数),并且实现了指定接口(第二个输入参数)。然后我们生成了这个代理类的调用指定处理程序(第三个输入参数)的代理实例
类加载器是干啥的:在内存中要有这个类的类加载器,才能将二进制转化为该类对应的实例对象(这里不是很懂)
所以我们就是通过Proxy.newProxyInstance实例化了一个代理对象,代理类实例化了HelloService接口,然后代理对象将这一坨数据丢给RpcClientProxy处理(就是调用)
回到第二句2,HelloService helloService 就好像实例化了一个接口一样,当我们想调用这个接口的方法的时候直接调用就好了,这就是代理的神奇之处:
以下引用自:浅谈动态代理在 RPC 中的应用 在项目中,当我们要使用 RPC 的时候,一般的做法是 先找服务提供方要接口,通过 Maven 等工具把接口依赖到我们项目中。如果要调用提供方的接口,就只需要通过依赖注入的方式把接口注入到项目中就行了,然后在代码里面直接调用接口的方法。
但是接口里并不包含真实的业务逻辑,业务逻辑都在服务提供方应用里面,但我们通过调用接口方法,拿到了我们想要的结果,那在 RPC 中这是怎么完成的呢,答案就是动态代理。
动态代理把调用过程封装起来,于是客户端可以像调用接口一样调用远程服务。 这解决了我们的第一个疑问:代理在RPC里有什么用。
1.1.3 第三句
实例化了一个Hello对象,将其作为参数传入helloService.hello方法 这里可以看出代理的优越性:通过代理,客户端可以像调用接口一样调用远程服务
以下不是很懂:所以helloService是一个HelloService类的代理实例?代理实例可以是接口类?所以这就是传入类加载器的原因,好把代理实例化成对应类的对象?
1.2 服务端代码
看完客户端,我们再来看看服务器
1.2.1 第一句
实例化了服务接口类
1.2.2 第二句
实例化了RpcServer类,点进去看看是啥
RpcServer类
这里我先把线程相关的注释掉了
public class RpcServer {
private static final Logger logger = LoggerFactory.getLogger(RpcServer.class);
public void register(Object service, int port) {
try (ServerSocket server = new ServerSocket(port);) {
logger.info("server starts...");
Socket socket;
while ((socket = server.accept()) != null) {
logger.info("client connected");
try (ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {
RpcRequest rpcRequest = (RpcRequest) objectInputStream.readObject();
Method method = service.getClass().getMethod(rpcRequest.getMethodName(), rpcRequest.getParamTypes());
Object result = method.invoke(service, rpcRequest.getParameters());
objectOutputStream.writeObject(result);
objectOutputStream.flush();
} catch (IOException | ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
logger.error("occur exception:", e);
}
}
} catch (IOException e) {
logger.error("occur IOException:", e);
}
}
}
首先根据port实例化了一个ServerSocket。客户端实现的是Socket 两者区别:http://c.biancheng.net/view/1199.html 当建立好连接之后,从字节输入流中取到RpcRequest对象 调用方法得到返回结果,将返回结果写入输出流,传给客户端 【虽然这个方法名为注册,但是我搞不懂它跟注册有什么关系,也许后面版本会优化这个地方】
1.2.3 第三句
调用rpcServer.register方法,将服务/端口号传进去
至此,除了线程的所有代码都看完了
1.3 线程部分
int corePoolSize = 10;
int maximumPoolSizeSize = 100;
long keepAliveTime = 1;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
this.threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSizeSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory);
线程池有什么用呢?为什么要用线程池?
参考线程池的好处,详解,单例(绝对好记)
好处1:线程池的存在可以大大降低线程创建和销毁的开销,从而提升线程执行速度 好处2:管理线程池里的线程
1.4 目录结构
然后我们看一下目录文件是怎么分配的
1.5 日志
为什么要打日志? 单个的小程序可以用system.out.println()来判断程序走到了哪一步,大项目就需要打日志了。可以用@Slf4j 注解,方便快速打日志 日志目的是记录关键操作的轨迹。记录具体时间具体数据具体操作。万一生产出错方便排查。
1.6 总结
到目前为止,我们把第一个版本的代码看完了 RPC的简易流程如图,这一版本的RPC,注册服务和获取服务信息功能完成得不好,但是完成了最基本的调用服务。
序列化:使用Java自带的Serializable; 网络传输:使用Socket实现TCP;
知道了动态代理在RPC中的作用,就是可以使客户端像调用接口一样调用远程服务; 知道的线程池的好处:加快程序执行速度、便于管理线程
2 自己动手实现一个最简单的RPC框架
我打算目录不按照他那么写 服务端与客户端示例放一个模块,核心框架放一个模块,工具类放一个模块。(其实核心框架我想多份几个模块,但是现在对RPC还不熟练没有能力quq)
拿到自己要写代码,一下子不知道从哪里写起了。。。。
2.0 创建工程
我咋没有pom文件呢?大的小的模块都没有 原来是创建工程的时候要选Maven作为依赖管理的工具,这样才会有pom文件 导入依赖
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<lombok.version>1.18.8</lombok.version>
<junit.version>4.12</junit.version>
<slf4j.version>1.7.26</slf4j.version>
<logback.version>1.2.3</logback.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
</dependencies>
1.单元测试快捷键:在被测试的类中任意空白处按:ctrl+shift+t,自动生成测试类 2.lombok注解:
@Data 自动生成get/set方法和toString方法@AllArgsConstructor 自动生成全参构造方法@NoArgsConstructor 自动生成无参构造方法@Builder 可以让实例化像这样循环调用赋值
@Builder :https://cloud.tencent.com/developer/article/1477728
3.日志使用注解@Slf4j ,不用每次都写一大串private static final Logger logger = LoggerFactory.getLogger(XXX.class);
2.1 思路
首先要有一个客户端类,还要有一个服务端类,还要有一个代理,还要有一个服务描述
客户端内执行: 创建Socket套接字,与服务器端建立连接,发送请求数据
在try中声明的变量,相当于一个局部变量,其作用域范围,仅限于try中。 在try里面声明变量我记得有个什么好处来着的,现在只搜到在try里声明流,可以不用关闭,JDK会自动关闭try里声明的流。“try-with-resources” catch里可以记日志
服务端内执行: 注册方法
代理执行: 实例化代理类; 继承InvocationHandler类重写invoke方法;
都写好之后写example 1.写配置类
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>rpc-framework</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
明明我后面都用了这两个属性的,但是这两个属性还是没有亮起来,原来是我没有设置get/set方法,加上注解@Data就好了
运行成功!
|