Bootstrap

RPC框架(小白易上手)

RPC框架 (Simple)

  1. 定义:

    远程过程调用,是一种计算机通信协议,允许程序在不同的计算机之间像本地调用一样进行通信和交互(允许一个程序像调用自己的方法一样调用另外一个程序,不需要了解数据的传输过程,网络通信细节等,能够快速开发分布式系统)

  2. 实现思路:

    1. 基本设计

      • 假设需要调用的一方是消费者,被调用的一方(提供服务的一方)是分享者

      • 消费者想调用分享者就需要分享者创建一个web服务,然后消费者通过请求客户端,发送HTTP或者其他协议这样去调用分享者提供的接口服务

      • 问题:如果分享者提供了多个接口方法怎么办? 【是不是需要分享者每个接口和方法都单独写一个接口?消费者每个接口都写一段HTTP调用的逻辑?】

      • 解决:当然不是,如果这样后端接口维护就太麻烦了,并且前端调用也非常的繁琐,接口多参数多,调用繁琐。【其实可以提供一个统一的服务调用接口,通过请求处理器根据客户端的请求参数进行不同的处理,调用不同的接口和方法(注册器模式)】

      • 比如:消费者需要调用分享者的message服务的connect方法,那么就可以发送请求,参数为server=message, method=connect,然后请求处理器就会根据service的值从服务注册器里面找到对应的服务实现类,并通过java反射机制调用method的指定方法

        如图所示

        image-20240302103829500

      • 上面基本上的实现逻辑已经完成,但是对于消费者来说还是稍微有一点繁琐,还需要自己完成请求和响应,这里可以使用一个代理对象来帮助完成这些事情

        如图所示

        image-20240303092623072

    2. 持续优化v1.0:

      • 问题1:消费者如何知道分享者的调用地址?

      • 解决:需要一个注册中心来存储分享者提供的地址,消费者要调用服务的时候,只需要从注册中心获取对应的地址即可完成调用操作

        如图所示

        image-20240303093329319

      • 问题2:多个分享者提供接口进行调用,消费者应该如何选择?

      • 解决:通过给消费者增加负载均衡能力,指定不同算法来决定调用哪一个服务提供者,方法可以有:轮询、随机、根据性能动态调用等

        如图所示

        image-20240303093714295

      • 问题3:如果消费者在调用分享者的接口时调用出现了问题,比如网络拥堵,服务器宕机等问题,如何处理?

      • 解决:给消费者增加一些容错机制,比如失败重试、降级调用其他接口等

        如图所示

        image-20240303094312726

    3. 总结

      想要开发一个完整的RPC框架还需要考虑很多的问题,比如消费者每次从注册中心拿取调用地址性能会不会受到影响?如何让整个框架更加的易于扩展等等

      框架图

      image-20240303094701124

  3. RPC(简易版)实践

    实践项目中,以一个最简单的用户服务为例子,演示整个服务的调用过程

    1. 创建四个meaven项目

      • example-common 存放公共依赖,接口,Model等
      • example-consumer 消费者代码
      • example-provider 分享者代码
      • way-rpc-easy 简易rpc框架
    2. 各个项目模块的具体实现

      • 公共模块 example-common

        需要被消费者和分享者同时引入,负责编写服务相关的接口和数据模型

        1. 文件结构如下

          image-20240309082607135

        2. User实体类

          public class User implements Serializable {
              private static final long serialVersionUID = -936712004335434748L;
          
              // 01. 创建一个用户名
              private String username;
          
              // 02. 创建一个setName方法
              public void setName(String username) {
                  this.username = username;
              }
          
              // 03. 创建一个获取name 的方法   直接返回当前类的属性
              public String getUsername() {
                  return username;
              }
          }
          
        3. 接口(提供一个获取用户的接口)

          User getUser(User user);
          
      • 服务提供者 example-provider (真正的实现了接口)

        1. 依赖文件引入

          <dependency>
                <groupId>com.way</groupId>
                <artifactId>example-common</artifactId>
                <version>1.0-SNAPSHOT</version>
              </dependency>
          
              <dependency>
                <groupId>com.way</groupId>
                <artifactId>way-rpc-easy</artifactId>
                <version>1.0-SNAPSHOT</version>
              </dependency>
              <!--hutool-->
              <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>5.8.16</version>
              </dependency>
              <!--lombok-->
              <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.30</version>
                <scope>provided</scope>
              </dependency>
          
        2. 实现公共模块中定义的获取用户的接口 (输出用户名称,返回参数中的用户对象)

          public User getUser(User user) {
                  System.out.println("用户名为:" + user.getUsername());
                  return user;
              }
          
        3. 初步编写服务提供者的类方法

          public class EasyProviderExample {
              // main方法  后面提供供服务的代码在这里面实现
              public static void main(String[] args) {
                  
              }
          }
          

          文件结构

          image-20240309083446500

      • 消费者 (example-consummer) 需要调用服务提供者的方法

        1. pom依赖文件的引入

           <dependency>
                <groupId>com.way</groupId>
                <artifactId>way-rpc-easy</artifactId>
                <version>1.0-SNAPSHOT</version>
              </dependency>
              <dependency>
                <groupId>com.way</groupId>
                <artifactId>example-common</artifactId>
                <version>1.0-SNAPSHOT</version>
              </dependency>
              <!--hutool-->
              <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>5.8.16</version>
              </dependency>
              <!--lombok-->
              <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.30</version>
                <scope>provided</scope>
              </dependency>
          
        2. 创建消费者启动类,编写调用接口的代码

          public static void main(String[] args) {
              // 这里暂时置为空,后面使用rpc框架实现动态代理获取对象
              UserService userService = null;
              User user = new User();
              user.setName("way");
              User result = userService.getUser(user);
              if(result != null) {
                  // 输出用户名
                  sout(result.getName());
              }else {
                  sout("user == null");
              }
          }
          

          文件目录

          image-20240309084405623

    3. web服务器

      需要实现提供者提供的服务是可以远程访问的,那么就需要一个web服务器,能够接受处理请求、并返回响应,这里选择使用的是Vert.x来实现rpc框架的web服务器

      • 引入依赖

         <!--https://vertx.io/docs/vertx-core/java/-->
            <dependency>
              <groupId>io.vertx</groupId>
              <artifactId>vertx-core</artifactId>
              <version>4.5.1</version>
            </dependency>
            <!--hutool-->
            <dependency>
              <groupId>cn.hutool</groupId>
              <artifactId>hutool-all</artifactId>
              <version>5.8.16</version>
            </dependency>
            <!--lombok-->
            <dependency>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
              <version>1.18.30</version>
              <scope>provided</scope>
            </dependency>
        
      • 编写一个接口,定义统一启动服务器的方法

        void doStart(int port);
        
      • 编写VertxHttpServer,能够监听指定端口并处理客户端的请求

        public class VertxHttpServer implements HttpServer{
            public void doStart(int port) {
                // 01. 创建vertx的实例
                Vertx vertx = Vertx.vertx();
                // 02. 创建http服务器
                io.vertx.core.http.HttpServer server = vertx.createHttpServer();
                // 03. 处理请求
                server.requestHandler(request -> {
                    // 处理请求
                    sout("收到请求" + request.method() + " " + request.uri());
                    // 发送HTTP响应
                    request.response()
                        .putHeader("content-type", "text/plain")
                        .end("I'm from Vertx http server!");
                });
                // 04. 启动http服务器并监听指定端口
                server.listen(port, result -> {
                    if(result.succeeded()) {
                        sout("server is running on port " + port);
                    }else {
                        sout("failed to running server" + result.cause());
                    }
                });
            }
        }
        

        rpc文件目录

        image-20240309090102577

      • 验证web服务器能否启动成功并接收客户端的请求 (修改提供者的EasyProviderExample类)

        ...
        main() {
            // 启动web服务
            HttpServer httpServer = new VertxHttpServer();
            httpServer.doStart(8080);
        }
        

      小结:

      到目前为止我们实现了公共模块,服务分享者,消费者,web服务器 还有以下模块没有实现:服务注册器、序列化与反序列化、客户端请求代理(动态代理)提供者的请求处理器

      如图所示

      image-20240309090724853

    4. 本地服务注册器 (只需要直接把服务注册到本地即可,不需要第三方的注册中心)

      • 创建注册器模块 (在rpc模块中新建一个文件registry/LocalRegistry编写注册器模块)

        // 本地注册服务是根据服务名获取到对应的实现类
        public class LocalRegistry {
            /**
            * 注册信息存储  使用线程安全的ConcurrentHashMap村粗,key为服务名称,value为服务实现类
            */
            private static final Map<String, Class<?>> map = new ConcurrentHashMap<>();
            /**
            * 注册服务
            */
            public static void register(String serviceName, Class<?> impClass) {
                map.put(serviceName, impClass);
            }
            /**
            * 获取服务
            */
            public static Class<?> get(String serviceName) {
                return map.get(serviceName);
            }
            /**
            * 删除服务
            */
            public static void remove(String  serviceName) {
                map.remove(serviceName);
            }
        }
        
      • 在分享者启动的文件类里面取注册服务到注册器中,修改源代码EasyProviderExample

        ...
        // 注册服务
        LocalRegistry.register(UserService.class.getName(), UserServiceImpl.class);
        // 启动web服务
        HttpServer httpServer = new VertxHttpServer();
        httpServer.doStart(8080);
        
    5. 序列化器

      1. 服务注册到本地之后,在实现分享者的请求处理逻辑之前,我们还需要先完成序列化模块;参数传递的过程中需要进行序列化和反序列化。

      2. 序列化:将java对象转为字节数组 反序列化: 将字节数组转为java对象

      3. 这里我们使用java原生的序列化器,不需要记住代码,直接copy就是

      • 在rpc模块中创建一个序列化接口,然后提供两个方法即可

        # 接口代码
        public interface Serializer {
            /**
             * 序列化
             * @param object
             * @return
             * @param <T>
             * @throws IOException
             */
            <T> byte[] serializer(T object) throws IOException;
        
            /**
             * 反序列化
             * @param bytes
             * @param type
             * @return
             * @param <T>
             * @throws IOException
             */
            <T> T unserializer(byte[] bytes, Class<T> type) throws IOException;
        }
        
        # 方法
        public class SerializerImpl implements Serializer {
            /**
             * 序列化 输入
             * @param object
             * @return
             * @param <T>
             * @throws IOException
             */
            @Override
            public <T> byte[] serializer(T object) throws IOException {
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
                objectOutputStream.writeObject(object);
                objectOutputStream.close();
                return outputStream.toByteArray();
            }
        
            @Override
            public <T> T unserializer(byte[] bytes, Class<T> type) throws IOException {
                ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
                ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
                try {
                    return (T) objectInputStream.readObject();
                } catch (ClassNotFoundException e) {
                    throw new RuntimeException(e);
                } finally {
                    objectInputStream.close();
                }
            }
        }
        
    6. 分享者的请求处理器

      请求处理器是rpc框架的关键,处理接收到的请求,根据参数寻找对应的服务和方法,通过反射实现调用,最后封装返回结果并响应请求

      • 编写请求和响应的封装类(rpc模块中创建文件夹model/RpcRequest + RpcResponse)

        # RpcRequest
        /**
         * 封装调用所需要的信息: 服务名称、方法名称、调用参数的类型列表、参数列表
         * java反射机制所需要的参数
         */
        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        @Builder
        public class RpcRequest implements Serializable {
        
            private static final long serialVersionUID = 2919771999544492378L;
        
            private String serviceName;
        
            private String methodName;
        
            /**
             * 参数类型列表
             */
            private Class<?>[] parameterTypes;
        
            /**
             * 参数列表
             */
            private Object[] args;
        }
        
        
        # RpcResponse
        /**
         * 封装调用方法得到的返回值、调用信息等
         */
        
        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        @Builder
        public class RpcResponse {
        
            /**
             * 响应数据
             */
            private Object data;
        
            /**
             * 响应数据类型
             */
            private Class<?> dataType;
        
            /**
             * 响应信息
             */
            private String message;
        
            /**
             * 响应的异常信息
             */
            private Exception exception;
        }
        
        
      • 编写请求处理器HttpServerHandler

        /**
        * 业务流程:
        * 	1. 反序列化请求对象(字节转为java对象),并获取请求参数
        * 	2. 根据服务名称供本地注册器中获取到对应的服务实现类
        *	3. 通过反射机制调用方法,得到返回结果
        *	4. 对返回结果进行封装和序列化,写入到响应中 (java对象转字节数组)
        *
        */
        
        public class HttpServerHandler implements Handler<HttpServerRequest> {
        
            @Override
            public void handle(HttpServerRequest request) {
                // 指定序列化器
                final Serializer serializer = new SerializerImpl();
                // 记录日志
                System.out.println("Received request:" + request.method() + " " + request.uri());
        
                // 异步处理HTTP请求
                request.bodyHandler(body -> {
                    byte[] bytes = body.getBytes();
                    RpcRequest rpcRequest = null;
                    try {
                        rpcRequest = serializer.unserializer(bytes, RpcRequest.class);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    // 构造响应结果对象
                    RpcResponse rpcResponse = new RpcResponse();
                    // 如果请求为null  直接返回
                    if(rpcRequest == null) {
                        rpcResponse.setMessage("rpcRequest is null");
                        // 调用下面的响应方法
                        doResponse(request, rpcResponse, serializer);
                        return;
                    }
        
                    try {
                        // 通过反射调用服务实现类
                        Class<?> clazz = LocalRegistry.get(rpcRequest.getServiceName());
                        Method method = clazz.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes());
                        Object result = method.invoke(clazz.newInstance(), rpcRequest.getArgs());
                        // 封装返回结果
                        rpcResponse.setData(result);
                        rpcResponse.setDataType(method.getReturnType());
                        rpcResponse.setMessage("ok");
                    } catch (Exception e) {
                        e.printStackTrace();
                        rpcResponse.setMessage(e.getMessage());
                        rpcResponse.setException(e);
                    }
                    // 响应
                    doResponse(request, rpcResponse, serializer);
                });
            }
        
            /**
             * 响应的方法
             */
            void doResponse(HttpServerRequest request, RpcResponse rpcResponse, Serializer serializer) {
                HttpServerResponse httpServerResponse = request.response().putHeader("content-type", "application/json");
                try {
                    // 序列化
                    byte[] serializer1 = serializer.serializer(rpcResponse);
                    httpServerResponse.end(Buffer.buffer(serializer1));
                } catch (IOException e) {
                    // 在命令行打印异常信息在程序中出错的位置及原因
                    e.printStackTrace();
                    httpServerResponse.end(Buffer.buffer());
                }
            }
        }
        
      • 给HttpServer绑定请求处理器

        # 修改rpc框架中的vertxHttpserverImpl的代码,通过server.requestHandler绑定请求处理器
        public class VertxHttpServerImpl implements HttpServer {
            public void doStart(int port) {
                // 01. 创建vertx实例
                Vertx vertx = Vertx.vertx();
                // 02. 创建http服务器
                io.vertx.core.http.HttpServer httpServer = vertx.createHttpServer();
                // 更改处理请求的代码位置
                // 03. 监听端口并处理请求   引入了rpc框架服务提供者的模块能够接收请求并且完成服务调用
                httpServer.requestHandler(new HttpServerHandler());
                // 04. 启动http服务器并监听指定端口
                httpServer.listen(port, result -> {
                    if(result.succeeded()) {
                        System.out.println("server is now listening on port" + port);
                    }else {
                        System.out.println("Failed to start server" + result.cause());
                    }
                });
            }
        }
        
        

        小节:

        支持,引入了rpc框架的服务分享者已经能够接收请求并完成服务的调用,剩下需要做的工作就是完成消费者这边的请求代理即可

    7. 消费者(请求代理)

      我们在前面已经预留了一段调用服务的代码,只要能够获取到Userservice对象即可实现整个流程,下面就通过生成动态代理对象来简化消费方的调用

      消费方之前的代码

      image-20240309141425412

      • 动态代理实现

        作用:根据要生成的对象的类型,自动胜场一个代理对象

        方式:JDK动态代理(简单易用,只能对接口进行代理) CGLIB动态代理(能对任何类进行代理)

        # 在rpc模块中编写动态代理类ServiceProxy实现invoke方法
        public class ServiceProxy implements InvocationHandler {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 指定序列化器
                SerializerImpl serializer = new SerializerImpl();
                // 构造请求
                RpcRequest rpcRequest = RpcRequest.builder()
                        .serviceName(method.getDeclaringClass()
                                .getName()).methodName(method.getName())
                        .parameterTypes(method.getParameterTypes())
                        .args(args).build();
        
                try {
                    // 序列化
                    byte[] bodyBytes = serializer.serializer(rpcRequest);
                    // 发送请求
                    // TODO: 硬编码需要注册中心和服务中心解决
                    try (HttpResponse httpResponse = HttpRequest.post("http://localhost:8080").body(bodyBytes).execute()) {
                        byte[] result = httpResponse.bodyBytes();
                        // 反序列化
                        RpcResponse unserializer = serializer.unserializer(result, RpcResponse.class);
                        return unserializer.getData();
                    }
                }catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }	
        }
        
      • 创建动态代理工厂ServiceProxyFactory,作用是根据指定类创建动态代理对象

        使用了工厂设计模式,简化对象的创建过程

        public class ServiceProxyFactory {
            /**
             * 根据服务类创建代理对象
             * @param serviceClass
             * @return
             * @param <T>
             */
            public static <T> T getProxy(Class<T> serviceClass) {
                return (T) Proxy.newProxyInstance(
                        serviceClass.getClassLoader(),
                        new Class[]{serviceClass},
                        new ServiceProxy());
            }
        }
        
      • 最后在消费者启动类里面将获取UserService实例的位置使用工厂模式.动态代理方法获取到userService实例

        UserService userService = ServiceProxyFactory.getProxy(UserService.class);
        
    8. 总结:

      调试:分别启动分享者和消费者的main方法,如果能够在控制台看到分别输出了user的值就说明整个调用过程成功

    感谢鱼皮大佬的教程,大家有需要可以上b站搜索程序员鱼皮进行学习…

;