【自己动手实现一个简单的RPC框架】1、[v1.0]实现一个最简单的RPC框架

本文学习的代码地址: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);

    /**
     * 服务端主动注册服务
     * TODO 修改为注解然后扫描
     */
    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>
        <!--        注解简化Java代码-->
        <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注解:

  1. @Data自动生成get/set方法和toString方法
  2. @AllArgsConstructor 自动生成全参构造方法
  3. @NoArgsConstructor 自动生成无参构造方法
  4. @Builder 可以让实例化像这样循环调用赋值

@Builderhttps://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就好了

运行成功!
在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值