【手写一个RPC框架】simpleRPC-02

2023-10-30

在这里插入图片描述

本项目所有代码可见:https://github.com/weiyu-zeng/SimpleRPC

前言

在simpleRPC-01中,我们实现了仅仅能用的RPC,但是调用只能调用server中某一个确定的方法,如果有很多方法需要调用怎么办?

因此需要把调用请求抽象出来,记为RPCRequest。

同样的,调用的返回对象也需要解耦,我们不需要知道对象是User还是其他。我们把调用返回抽象出来,记为RPCResponse。

我们不希望每次调用都要重新写host,port和调用方法,因此也需要抽象。

实现

项目创建

创建名为simpleRPC-02的module

在这里插入图片描述

老样子,把名为com.rpc的package创建好,然后创建client,common,server,service四个package:

在这里插入图片描述

依赖配置

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>SimpleRPC</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>simpleRPC-02</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

</project>

common

User.java

package com.rpc.common;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;


/**
 * @author zwy
 *
 * 定义简单User信息。
 */

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
    // 客户端和服务端共有的
    private Integer id;
    private String userName;
    private Boolean sex;
}


我们定义抽象的RPC调用请求

RPCRequest.java

package com.rpc.common;


import lombok.Builder;
import lombok.Data;

import java.io.Serializable;


/**
 * 客户端请求的抽象RPCRequest(接口名,方法名,参数,参数类型)
 *
 * 在上个例子中,我们的Request仅仅只发送了一个id参数过去,这显然是不合理的,
 * 因为服务端不会只有一个服务一个方法,因此只传递参数服务端不会知道调用那个方法
 *
 * 因此一个RPC请求中,client发送应该是需要调用的Service接口名,方法名,参数,参数类型
 * 这样服务端就能根据这些信息根据反射调用相应的方法
 * 使用java自带的序列化方式(实现接口)
 */
@Data
@Builder
public class RPCRequest implements Serializable {
    // 服务类(接口)名,客户端只知道接口名,在服务端中用接口名指向实现类
    private String interfaceName;
    // 方法名
    private String methodName;
    // 参数列表
    private Object[] params;
    // 参数类型
    private Class<?>[] paramsTypes;
}

RPCResponse.java

package com.rpc.common;

import lombok.Builder;
import lombok.Data;

import java.io.Serializable;

/**
 * 定义了服务器端给客户端回应的抽象 RPCResponse,包含两个部分:
 * 1.状态信息:状态码int code,状态信息String message
 * 2.具体数据:Object data
 * 此外还包含success方法:将RPCResponse对象的code状态码初始化为200,data初始化为传入的data,后返回RPCResponse
 * 还包含fail方法,将RPCResponse对象的code初始化为500,将状态信息message初始化为"服务器发生错误",后返回RPCResponse对象
 *
 * 上个例子中response传输的是User对象,显然在一个应用中我们不可能只传输一种类型的数据
 * 由此我们将传输对象抽象成为Object
 * RPC需要经过网络传输,有可能失败,类似于http,引入状态码和状态信息表示服务调用成功还是失败
 */
@Data
@Builder
public class RPCResponse implements Serializable {

    // 状态信息
    private int code;
    
    private String message;
    // 具体数据
    private Object data;

    public static RPCResponse success(Object data) {
        return RPCResponse.builder().code(200).data(data).build();
    }

    public static RPCResponse fail() {
        return RPCResponse.builder().code(500).message("服务器发生错误").build();
    }
}


service

创建服务接口 UserService.java

package com.rpc.service;

import com.rpc.common.User;

/**
 * @author zwy
 *
 * 服务器端提供服务的方法的接口
 */
public interface UserService {
    
    // 客户端通过这个接口调用服务端的实现类
    User getUserByUserId(Integer id);
    
    // 给这个服务增加一个功能
    Integer insertUserId(User user);
}


服务实现类:UserServiceImpl.java

package com.rpc.service;


import com.rpc.common.User;

import java.util.Random;
import java.util.UUID;


/**
 * @author zwy
 *
 * 服务器端提供服务的方法
 * 1.getUserByUserId方法:接收一个id,返回一个User对象,提供属于这个ID(Integer)的User,
 * User中包含他的ID(Integer),名字Name(String)和性别sex(Boolean)。
 * 2.insertUserId:打印成功插入数据的信息(模拟数据库插入数据的情况)
 */
public class UserServiceImpl implements UserService {

    @Override
    public User getUserByUserId(Integer id) {
        System.out.println("客户端查询了"+id+"的用户");

        // 模拟从数据库中取用户的行为
        Random random = new Random();
        User user = User.builder()
                .userName(UUID.randomUUID().toString())
                .id(id)
                .sex(random.nextBoolean()).build();
        return user;
    }

    @Override
    public Integer insertUserId(User user) {
        System.out.println("插入数据成功: " + user);
        return 1;
    }
}

到此为止我们把需要的实例,request和response定义好了。之后对于server和client的设计我们将用到动态代理和反射。

client

我们先定义底层Client逻辑:IOClient.java

package com.rpc.client;


import com.rpc.common.RPCRequest;
import com.rpc.common.RPCResponse;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;


/**
 * @author zwy
 *
 * IO Client:底层的通信
 * 通过Socket和输出流把 RPCRequest 传给服务器端,接收到服务器端传来的 RPCResponse,返回这个 RPCResponse
 *
 * 这里负责底层与服务器端的通信,发送的Request,接受的是Response对象
 * 客户端发起一次请求调用,Socket建立连接,发起请求Request,得到相应Response
 * 这里的request是封装好的(上层进行封装),不同的service需要进行不同的封装,客户端只知道Service接口,
 * 需要一层动态代理根据反射封装不同的Service
 */
public class IOClient {

    public static RPCResponse sendRequest(String host, int port, RPCRequest request) throws IOException, ClassNotFoundException {

        // 老样子,创建Socket对象,定义host和port
        Socket socket = new Socket(host, port);

        // 定义输入输出流对象
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
        ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
        System.out.println("request: " + request);

        // 输出流写入request对对象,刷新输出流
        objectOutputStream.writeObject(request);
        objectOutputStream.flush();
        
        // 通过输入流的readObject方法,得到服务器端传来的RPCResponse,并返回RPCResponse对象
        RPCResponse response = (RPCResponse) objectInputStream.readObject();

        return response;

    }
}

然后我们编写代理类:ClientProxy.java

它实现了InvocationHandler接口

package com.rpc.client;

import com.rpc.common.RPCRequest;
import com.rpc.common.RPCResponse;
import lombok.AllArgsConstructor;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @author zwy
 *
 * 客户端代理:把动态代理封装request对象
 *
 * '@AllArgsConstructor':它是lombok中的注解。使用后添加一个构造函数,该构造函数含有所有已声明字段属性参数
 *                        (这也就是为什么ClientProxy明明没定义构造函数,但RPCClient还可以再创建ClientProxy时,
 *                        通过构造函数传参给 host 和 port。)
 * java动态代理机制中有两个重要的类和接口InvocationHandler(接口)和Proxy(类):也是实现动态代理的核心
 * InvocationHandler接口:是proxy代理实例的调用处理程序实现的一个接口,每一个proxy代理实例都有一个关联的调用处理程序,
 *                        在代理实例调用方法时,方法调用被编码分派到调用处理程序的invoke方法。
 *                        每一个动态代理类的调用处理程序都必须实现InvocationHandler接口,并且每个代理类的实例都关联到了
 *                        实现该接口的动态代理类调用处理程序中,当我们通过动态代理对象调用一个方法时候,这个方法的调用
 *                        就会被转发到实现InvocationHandler接口类的invoke方法来调用
 * Proxy:该类用于动态生成代理类,只需传入目标接口、目标接口的类加载器以及InvocationHandler便可为目标接口生成代理类及代理对象
 * Proxy.newProxyInstance:该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例
 */
@AllArgsConstructor
public class ClientProxy implements InvocationHandler {
    
    private String host;
    
    private int port;

    /**
     * 动态代理,每一次代理对象调用方法,会经过此方法增强(反射获取request对象,socket发送至客户端)
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        // 构建RPCRequest对象,初始化其中的四个重要参数,使用了lombok中的builder。
        // 初始化interfaceName。初始化methodName,初始化params,,初始化paramsTypes
        RPCRequest request = RPCRequest.builder()
                                       .interfaceName(method.getDeclaringClass().getName())
                                       .methodName(method.getName())
                                       .params(args)
                                       .paramsTypes(method.getParameterTypes())
                                       .build();

        // 调用IOClient,通过输入输出流进行request的数据传输,并返回服务器端传来的response
        RPCResponse response = IOClient.sendRequest(host, port, request);
        System.out.println("response: " + response);
        
        return response.getData();  // 获取RPCResponse中的目标数据(因为RPCResponse中除了目标数据,还有状态码和状态信息这些非目标数据)
    }

    /**
     * 传入Client需要的服务的class反射对象
     */
    <T> T getProxy(Class<T> clazz) {
        // 传入目标接口的类加载器,目标接口,和InvocationHandler(的实现类,也就是本类,this),生成动态代理类实例
        Object o = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, this);
        return (T)o;
    }
}


最后我们编写RPC客户端,也就是用户操作的界面:RPCClient.java

package com.rpc.client;


import com.rpc.common.User;
import com.rpc.service.UserService;

/**
 * @author zwy
 *
 * RPC客户端:调用服务器端的方法
 */
public class RPCClient {

    public static void main(String[] args) {
        // 初始化主机名ip和端口号port
        ClientProxy clientProxy = new ClientProxy("127.0.0.1", 8899);
        UserService proxy = clientProxy.getProxy(UserService.class);  // 反射获得代理

        // 服务的方法1:通过id获取User
        User userByUserId = proxy.getUserByUserId(10);
        System.out.println("从服务器端得到的user为:" + userByUserId);
        System.out.println();

        // 服务的方法2:(假装)插入一个User数据
        User user = User.builder().userName("张三").id(100).sex(true).build();
        Integer integer = proxy.insertUserId(user);
        System.out.println("向服务器端插入数据" + integer);
    }
}


server

现在写服务端server:RPCServer.java

package com.rpc.server;


import com.rpc.common.RPCRequest;
import com.rpc.common.RPCResponse;
import com.rpc.service.UserServiceImpl;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;


/**
 * @author zwy
 *
 * RPC server:接受/解析request,封装,发送response
 *
 * getClass方法:返回Object的运行时类
 * Class.getMethod(String name, Class<?>... parameterTypes):返回Method对象,方法的作用是获得对象所声明的公开方法
 *                 该方法的第一个参数name是要获得方法的名字,第二个参数parameterTypes是按声明顺序标识该方法形参类型。
 *  java.lang.reflect.Method.invoke(Object receiver, Object... args):返回Object对象,方法来反射调用一个方法,
 *                  当然一般只用于正常情况下无法直接访问的方法(比如:private 的方法,或者无法或者该类的对象)。
 *                  方法第一个参数是方法属于的对象(如果是静态方法,则可以直接传 null),第二个可变参数是该方法的参数
 */
public class RPCServer {

    public static void main(String[] args) throws IOException {

        // 初始化(客户端Client)需要的服务:UserServiceImpl
        UserServiceImpl userService = new UserServiceImpl();
        
        // 创建ServerSocket对象,端口号要和Client一致
        ServerSocket serverSocket = new ServerSocket(8899);
        System.out.println("服务器启动!");

        // BIO的方式监听Socket,监听到之后返回Socket对象
        while (true) {
            Socket socket = serverSocket.accept();

            // 监听到连接之后,开启一个线程来处理
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // socket对象的获取输入输出流作为targat,初始化输入输出流
                        ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                        ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());

                        // 读取客户端传过来的request
                        RPCRequest request = (RPCRequest) ois.readObject();

                        // 反射调用方法
                        Method method = userService.getClass().getMethod(request.getMethodName(), request.getParamsTypes());
                        Object invoke = method.invoke(userService, request.getParams());

                        // 把得到的invoke对象写入response的success方法中,写入输出流(传给客户端),刷新输出流
                        oos.writeObject(RPCResponse.success(invoke));
                        oos.flush();
                    } catch (IOException | ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

到此RPC改造完成

文件结构

文件结构如下:

在这里插入图片描述

运行

先运行RPCServer.java

在这里插入图片描述

然后运行RPCClient.java

在这里插入图片描述
我们可以看到,Client调用的两个service,都成功打印了信息,说明RPC功能运行成功。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【手写一个RPC框架】simpleRPC-02 的相关文章

随机推荐

  • Targan 算法[有向图强连通分量]

    有向图强连通分量 在有向图G中 如果两个顶点间至少存在一条路径 称两个顶点强连通 stronglyconnected 如果有向图G的每两个顶点都强连通 称G是一个强连通图 非强连通图有向图的极大强连通子图 称为强连通分量 strongly
  • 标题hadoop源码编译报错

    标题hadoop源码编译报错 最近在学习hadoop的过程中 遇到了许多问题在这里记录下 没想到在源码编译的时候就卡了好久 现在简单说下我的问题 这是我编译hadoop所安装的jar包 网上的教程一搜一大堆 我也是按照文档一步步进行的 没想
  • 机器学习之决策树(实战)

    决策树 什么是决策树 信息增益 熵 基尼指数 熵 基尼指数 CART 算法模型实战 分类树 树的可视化 回归树 总结 什么是决策树 决策树是一种树形结构 其中每个内部节点表示一个属性上的判断 每个分支代表一个判断结果的输出 最后每个叶节点代
  • jpa limit查询_spring data jpa 分页查询

    法一 本地sql查询 注意表名啥的都用数据库中的名称 适用于特定数据库的查询 public interface UserRepository extends JpaRepository Query value SELECT FROM USE
  • 带圈圈的数字1~50,求50以上,不要word的

    带圈圈的数字1 50 求50以上 不要word的 posted on 2018 04 25 16 07 jony413 阅读 评论 编辑 收藏 转载于 https www cnblogs com jony413 p 8945365 html
  • 微信小程序悬浮框实现

    最近在公司负责微信小程序 小程序相比html JavaScript更加简单 很多接口直接就给了 所以我们直接利用就好了 下面说正题 微信小程序悬浮框实现 效果图如下 做了一个随时拨打客服电话的悬浮框 1 第一种 目录结构如下 index j
  • 关于golang性能调试及pprof可视化

    golang支持使用pprof进行可视化性能检测 打开powershell 在需要性能可视化的文件夹下输入 go test bench cpuprofile cpu out 得到一个 out文件 接着输入 go tool pprof cpu
  • 计算机网络知识点汇总(考研用)——第一章:计算机网络体系结构

    计算机网络知识点汇总 考研用 第一章 计算机网络体系结构 本文参考于 2021年计算机网络考研复习指导 王道考研 计算机网络 思维导图 文章目录 计算机网络知识点汇总 考研用 第一章 计算机网络体系结构 1 计算机网络体系结构 1 1 计算
  • 145句经典诗句

    1 知我者 谓我心忧 不知我者 谓我何求 诗经 王风 黍离 释义 了解我心情的人 认为我心中惆怅 不了解我心情的 还以为我呆在这儿有什么要求呢 2 人而无仪 不死何为 诗经风相鼠 释义 人活着如果不重视礼仪 那么就如同死人 3 言者无罪 闻
  • 关于工牌(必须5-10个字)

    今天蹲坑 低头看了下工牌觉得挺有意思 我从啥时候起也不排斥将工牌挂在脖子上了 工牌 一个标识 不仅标识了你 也标识了你所在的群体 如果你认可这个群体 佩戴它那是一种荣誉 荣耀 如果你不认可这个群体 佩戴它就是耻辱 羞辱 尤其挂到脖子上 那不
  • 使用机器学习算法预测航班价格

    一 前言 机票价格的预测一直是航空业和旅行者关注的重要问题之一 随着航空业的快速发展和市场竞争的加剧 正确预测机票价格对于航空公司的利润最大化和旅行者的预算规划至关重要 在过去 人们通常依靠经验和市场趋势来预测机票价格 但这种方法往往存在不
  • Qt宏定义

    1 QT BEGIN NAMESPACE 在qglobal h中 我们可以看到以下两句胡宏定义 define QT BEGIN NAMESPACE namespace QT NAMESPACE define QT END NAMESPACE
  • Java中有关锁的面试题

    sychronized修饰普通方法和静态方法的区别 什么是可见性 对象锁是用于对象实例方法 或者一个对象实例上的 类锁是用于类的静态方法或者一个类的class对象上的 类的对象实例可以有很多个 但是每个类只有一个class对象 所以不同对象
  • 静态成员(static)

    今天整理了一下关于静态的一些知识点 可能有些没有整理到 或者理解有纰漏 大家不妨看看 不足之处 恳请大家斧正 在静态类中 静态类中不能调用非静态类的实例成员 静态类中不能有非静态构造函数 但是可以有静态构造函数 静态构造函数也可以存在于非静
  • D361周赛复盘:模拟分割整数⭐+变为整除的最小次数⭐

    文章目录 2843 统计对称整数的数目 模拟 分割整数为两部分 思路 1 整数换成字符串版本 2 直接用整数的版本 2844 生成特殊数字的最小操作 模拟 x能被Num整除的条件 思路 完整版 2843 统计对称整数的数目 模拟 分割整数为
  • 微信小程序超详细入门简介和使用

    微信小程序 介绍 微信小程序 简称小程序 英文名Mini Program 是一种不需要下载安装即可使用的应用 它实现了应用 触手可及的梦想 用户扫一扫或搜一下即可打开应用 微信小程序做项目的必备基础 小程序的前世今生 小程序开发者工具 小程
  • Chatgpt API调用报错:openai.error.RateLimitError

    Chatgpt API 调用报错 openai error RateLimitError You exceeded your current quota please check your plan and billing details
  • 常用图像增强方法,利用tf.keras来完成图像增强

    学习目标 知道图像增强的常用方法 能够利用tf keras来完成图像增强 大规模数据集是成功应用深度神经网络的前提 例如 我们可以对图像进行不同方式的裁剪 使感兴趣的物体出现在不同位置 从而减轻模型对物体出现位置的依赖性 我们也可以调整亮度
  • allure在python环境下的集成使用

    python环境下 集成allure 首先打开Allure官网 https docs qameta io allure 选择对应平台的安装方式 最下面有通用安装方法 1 去maven central下载一个新版本的zip 2 下载到本地后解
  • 【手写一个RPC框架】simpleRPC-02

    目录 前言 实现 项目创建 依赖配置 common service client server 文件结构 运行 本项目所有代码可见 https github com weiyu zeng SimpleRPC 前言 在simpleRPC 01