基于java+etcd+Vert.x实现高性能的RPC框架

一、前言 

本篇文章主要是用Java,etcd和Vert.x设计一个高性能、扩展性强的的RPC框架。本项目是基于简易版RPC框架进行设计,如果感觉本文章上手难度较高可以参考我的另一篇简易版RPC框架的博客,代码仓库地址如下:

(简易版)代码仓库:https://gitee.com/not-speaking-666/hpq_rpc.git

(拓展版)代码仓库:https://gitee.com/not-speaking-666/hpq_rpc_plus.git

什么是RPC框架

        RPC(Remote Procedure Call),即远程过程调用,是一种远程通信协议,允许程序在不同计算机之间进行通讯和交互的机制或方法,可以通过网络从远程计算机程序上请求服务,而客户端不需要了解底层网络技术的细节。RPC框架的主要目的是让远程服务调用看起来像是本地调用一样简单和透明。

        RPC的主要特点: 

  • 透明性:RPC 提供了一种透明的方式来进行远程调用,使客户端能够像调用本地函数一样调用远程服务。
  • 客户机/服务器模式:RPC 通常基于客户机/服务器架构,其中客户端发起调用,服务器端执行实际的服务逻辑。
  • 请求/响应模式:客户端向服务器发送请求,服务器处理请求后返回响应。
  • 序列化:为了在网络上传输数据,RPC 需要将数据结构或对象状态转换为可以传输的形式,这通常涉及序列化和反序列化过程。
  • 网络通信:RPC 可以使用多种底层网络协议,如 TCP 或 HTTP进行数据传输。
为什么要用RPC? 

        在系统开发过程中随着系统用户量和规模不断扩大,简单的单机系统已经无法处理高并发的服务请求,此时我们采用分布式的架构使用多台服务器负责不同模块,以此来减小每台服务器的压力,但随着业务不断增加,不同功能模块之间也存在着调用关系,此时由于RPC允许在不同计算机之间进行通讯和交互,使开发者像调用本地方法一样调用远程服务,隐藏了网络通信的复杂性,这样就实现了不同模块之间相互通信,大大简化了分布式系统的开发。                                                         同时许多RPC框架支持跨语言调用,这使得不同语言编写的系统组件也可以无缝集成。通过将服务接口和实现分离,RPC框架使得代码更加模块化,便于维护和升级,以及不同模块功能的增加和拓展。

常用的RPC框架
  • Thrift:由Facebook开发的跨语言服务开发框架,结合了强大的软件堆栈和代码生成引擎,支持多种编程语言。
  • Dubbo:阿里巴巴开源的分布式服务框架,提供了服务动态寻址与路由、软负载均衡与容错、依赖分析与降级等功能。
  • Spring Cloud:由众多子项目组成,提供了搭建分布式系统及微服务常用的工具,如配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性token、全局锁、选主、分布式会话和集群状态等。
  • gRPC:由Google开发的远程过程调用系统,基于HTTP 2.0协议,支持多种编程语言。
RPC框架实现原理

简易的RPC调用流程:

       在框架中主要有两个角色:消费者和提供者,消费者想得到相应服务或接口需要向提供者发送请求,提供者接收到请求后为消费者提供给对应的服务或者接口,就像在淘宝上买东西一样,商家为我们提供各种商品,而我们则根据需求向商家指明要买的东西付钱即可。

        而消费者想要想要向提供者发起请求调用,就需要提供者启动Web服务,消费者通过请求服务端发送Http或者其他请求给web服务器,web服务器就将请求信息发送给提供者,这是提供者就会提供对应的服务。

        然而服务提供者一般会有多个服务或者方法,如果为每一个接口都重新编写一个服务调用的方法就太麻烦了,所以考虑为提供者加一个请求处理器,可以根据客户端请求参数调用不同的服务。

        为提供者增加一个本地注册中心,用于存储提供者的服务和方法,当客户端发送请求后,请求处理器收到请求会从注册中心找到对应方法,并通过Java的反射机制调用method指定的方法。

        同时由于java对象无法在网络中传输,在发送和接受请求时需要对数据进行序列化和反序列化

        而为了实现类似于本地调用的效果,我们需要使用代理模式增加一个代理对象,它允许客户端通过一个本地代理对象来间接地调用远程服务上的方法,进而简化消费者发送请求的代码。

        虚线框为RPC模块的基本框架

框架拓展设计

 上述模块仅仅是RPC框架的基本框架,要想成为真正高性能,可使用性的完整框架还需要为其增加其他拓展功能。

1.服务注册和发现
  •  服务注册:服务提供者在启动时将自己的服务信息(如IP地址、端口号等)注册到一个中心化的服务注册中心。

  • 服务发现:服务消费者在需要调用服务时,通过服务注册中心查询可用的服务提供者信息,然后建立连接并发起调用 

服务注册和发现的框架图如下:

为了更好的实现服务注册和发现功能,我们一般选择第三方注册中心Redis、Zookeeper

2.负载均衡

负载均衡,通俗来讲,就像是餐厅里的领位员。想象一下,当一家餐厅生意非常火爆时,会有很多顾客同时到达。如果没有合理的安排,一些桌子可能会坐满人,而另一些桌子却空着。这样不仅效率低,顾客等待的时间也会很长。

        这时候,领位员的作用就体现出来了,他们会根据每张桌子的空位情况,将顾客引导到合适的座位上。这样,餐厅的座位使用率就会更加均匀,顾客的等待时间也会缩短。

        在计算机系统中,负载均衡也是做类似的事情。当有多个服务提供者提供服务时,消费者需要选择一个服务提供者,通过负载均衡,用不同的算法为来决定调用哪一个服务提供者,比如使用轮询算法,随机算法等

3.容错机制

听到容错机制可能一些同学会以为和java代码的报错有关系,其实不然,容错机制是指系统在面对错误或异常情况比如服务调用失败等,能够继续运行并提供服务的能力。它是一种设计策略,旨在提高系统的可靠性和健壮性。容错机制是预防和处理错误的策略,而报错是错误发生后的通知和反馈机制。常见的容错机制包括重试机制,超时和取消,熔断机制,降级服务,限流

​、

4.其他

除了以上主要的扩展设计,其实设计一个完备的RPC框架还要考虑许多问题。比如;

  • 协议设计:定义RPC调用的协议,包括消息格式、序列化和反序列化机制,以及传输协议(如HTTP/2、gRPC等)。

  • 配置中心:集中管理配置信息,支持动态调整配置而无需重启服务。

  • 监控和日志:实现监控机制来跟踪服务的性能和状态,以及日志记录系统来记录关键操作和错误信息。

  • SPI机制:提高整个框架的拓展性,允许在运行时根据配置文件动态地选择服务实现,提高系统的可配置性。

所以想要做一个近乎完美的RPC框架可谓是难如上青天!

二、 项目实现
1.项目整体结构

该项目分为以下几个模块:

  • demo_common        代码公共依赖,包括model和service
  • demo_consumer      服务消费者代码
  • demo_provider         服务提供者代码
  • hpq_rpc_plus            rpc框架代码(拓展版)
2.全局配置加载
(1)配置项

        在完备的RPC框架中,全局配置加载允许开发者在不修改代码的情况下,通过外部配置文件来调整服务的行为和参数,这包括服务地址、端口、超时设置、重试策略等。这种配置的灵活性使得服务可以轻松适应不同的运行环境和需求变化。

        在简易版的RPC框架中我们使用硬编码的方式固定了这些配置,而这也将极大的限制了框架的拓展性,不利于维护,所以我们需要该框架可以通过写配置文件进行自定义配置

RPC框架全局配置RpcConfig

定义了一个名为RpcConfig 的类,用于表示 RPC 框架的全局配置信息。包括主机名,端口号序列化方式等默认配置。整体上,这个类将 RPC 框架所需的各种配置信息集中在一起,方便进行统一的管理、读取和修改。代码如下:

package com.hpq.config;

import lombok.Data;

/**
 * Rpck框架全局配置
 *
 */
@Data
public class RpcConfig {
    //名称
    private String name= "hpq-rpc-plus";
    //版本号
    private String version= "1.0.0";
    //服务器主机名
    private String host= "localhost";
    //服务器端口
    private Integer port= 8080;
}

(2)配置加载        

之后我们需要在utils包下创建 ConfigUtils 工具类,提供加载配置文件到 Java 对象的方法。这个类的作用是简化从属性文件(.properties 文件)中读取配置信息并将其映射到 Java 类的过程,支持不同环境的配置,并且可以轻松地将配置映射到 Java 对象中,从而减少手动处理配置的复杂性。

代码如下:

package com.hpq.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.setting.dialect.Props;

/**
 * 配置工具类
 */
public class ConfigUtils {
    //加载配置对象
    public static <T> T loadConfig(Class<T> clazz,String prefix) {
            return loadConfig(clazz,prefix,"");
}
    //加载配置对象,支持区分环境
    public static <T> T loadConfig(Class<T> clazz,String prefix,String env) {
        // 构建配置文件名称,支持多环境配置
            StringBuilder sb = new StringBuilder("application");
            if(StrUtil.isNotBlank(env)){
                sb.append("-").append(env);
            }
            sb.append(".properties");
        // 根据配置文件名称初始化Props对象
            Props props = new Props(sb.toString());
        // 将配置项转化为指定的POJO对象,并进行返回
            return props.toBean(clazz,prefix);
    }
 }

之后直接调用该工具类的静态方法即可读取配置 

(3)维护全局配置

RPC需要一个全局的配置对象,在引入的RPC框架项目启动时,需要从配置文件中读取配置并创建对象实例,之后就可以集中的从这个对象中读取配置信息了,而不用每次加载配置时重新读取配置并创建实例,节省了性能开销。

首先加入相关工具的依赖:

       
        <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.3.12</version>
        </dependency>

创建一个RpcApplication启动类来作为Rpc框架启动入口,并维护全局的变量,如果配置加载失败,我们就使用默认值。

/**
 * 初始化配置
 */
public static void init() {
    RpcConfig newRpcConfig;
    try{
    newRpcConfig = ConfigUtils.loadConfig(RpcConfig.class, RpcConstant.RPC_CONFIG_PREFIX);
    }catch (Exception e){
        // 配置加载失败,使用默认配置值
        newRpcConfig = new RpcConfig();
    }
    init(newRpcConfig);
}
    /**
     * 初始化框架,支持自定义配置
     */
    public static void init(RpcConfig newRpcConfig) {
        rpcConfig = newRpcConfig;
        log.info("rpc config:{}", rpcConfig);
    }

我们用一个单例模式来获取RpcConfig实例。当rpcConfig为null时,在类级别的锁保护下初始化它,以确保多线程环境下的线程安全。如果rpcConfig已经在其他地方被初始化过,则直接返回已有的rpcConfig对象,避免重复初始化

/**
 * 获取配置
 */
public static RpcConfig getRpcConfig() {
    if (rpcConfig == null) {
        synchronized (RpcApplication.class) {
            if (rpcConfig == null) {
                init();
            }
        }
    }
    return rpcConfig;
}

这段代码就是双检索单例模式的经典实现,支持在获取配置时才调用init()方法实现懒加载,

同时我们也支持自定义传入配置对象,如果传,则默认加载CofigUtils类写好的配置。

在之后的代码中只需要写一行代码,就可加载到配置:

RpcConfig rpc = RpcApplication.getConfig()
       (4)测试

在Consumer模块的resource文件中加入application.properties文件 ,配置信息如下:

rpc.name=yurpc
rpc.version=2.0
rpc.mock=false
rpc.serializer=jdk

然后在消费者启动类ConsumerApplication中加入如下测试代码:

    public static void main(String[] args) {
        RpcConfig rpcConfig = ConfigUtils.loadConfig(RpcConfig.class, RpcConstant.RPC_CONFIG_PREFIX);
        System.out.println(rpcConfig);
}

运行后可以正常输出配置信息即可。

然后在服务提供者启动类ProviderApplication中可以根据动态配置在不同端口启动web服务

package com.hpq;
import com.hpq.register.LocalRegister;
import com.hpq.server.HttpServer;
import com.hpq.server.VertxHttpServerImpl;
import java.io.IOException;

public class ProviderApplication {
    public static void main(String[] args) throws IOException {
        //初始化配置
        RpcApplication.init();
        // 启动注册服务
        LocalRegister.register(UserService.class.getName(), UserServiceImpl.class);
        // 启动web服务
        HttpServer httpServer = new VertxHttpServerImpl();
        httpServer.doStart(RpcApplication.getRpcConfig().getPort());
    }
}
3.接口Mock

什么是Mock?

在rpc框架开发中,我们需要调用其他远程服务,但是在实际开发和测试中是不会提供真实的测试环境的,这时候需要我们根据真实环境模拟出一个虚拟的环境用于正式上线前的开发和测试,这样就可以避免一些不可控的因素或者错误发生。因此mock常常用在测试代码中,用于在单元测试中跑通代码。

那么如何实现这个设计呢?

其实在简易版的RPC的开发中我们曾用动态代理的方式创建远程调用对象,那么同理我们可以用动态代理的方法创建一个调用对象时返回固定值的对象,就可以实现远程调用服务的Mock。

(1).在这里我们使用修改配置类实现mock的开启和关闭,在全局配置类RpcConfig中加入mock的配置字段,初始值设置为true。

 (2).在proxy包下新增ServiceMockProxy类,实现根据服务接口类型返回固定值的方法

package com.hpq.proxy;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

public class ServiceMockProxy implements InvocationHandler {
    private static final Logger log = LoggerFactory.getLogger(ServiceMockProxy.class);

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //根据返回值类型,生成特定的返回值对象
        Class<?> returnType = method.getReturnType();
        log.info("mock proxy invoke method:{}", method.getName());
        return getDefaultObject(returnType);
    }
    private Object getDefaultObject(Class<?> returnType) {
        //基本类型
            if (returnType.isPrimitive()){
                if(returnType == int.class){
                        return 0;
                }else if(returnType == long.class){
                    return 0L;
                }else if (returnType == boolean.class){
                    return false;
                }else if(returnType == short.class){
                    return (short)0;
                }
            }
            //对象类型
        return null;
    }
}

 通过调用getDefaultObject()方法,实现可以根据不同的class返回不同的默认值。

(3).在ServiceProxyFactory中我们新增一个方法getMockProxy方法获取代理对象,同时在真实代理对象中做一个判断看是否需要用模拟对象测试开发环境,如果为true则直接使用模拟对象调用服务,否则使用java的动态代理创建并返回代理对象。

package com.hpq.proxy;

import com.hpq.RpcApplication;
import com.hpq.config.RpcConfig;

import java.lang.reflect.Proxy;

/**
 * 服务代理工厂(创建代理对象)
 */
public class ServiceProxyFactory {
    /**
     * 根据服务类创建代理对象
     */
    public static <T> T getProxy(Class<T> interfaceClass) {
        if (RpcApplication.getRpcConfig().isMock()) {
            return getMockProxy(interfaceClass);
        }
        return (T) Proxy.newProxyInstance(
                interfaceClass.getClassLoader(),
                new Class[]{interfaceClass},
                new ServiceProxy()
        );
    }
    /**
     * 根据服务类创建Mock代理对象
     * @param interfaceClass
     * @return
     * @param <T>
     */
    public static <T> T getMockProxy(Class<T> interfaceClass) {
        return (T) Proxy.newProxyInstance(
                interfaceClass.getClassLoader(),
                new Class[]{interfaceClass},
                new ServiceMockProxy()
        );
    }
}

4.序列化器和SPI机制

序列化器

        在分布式系统中,例如RPC框架,序列化和反序列化是通信过程中不可或缺的部分。客户端发送请求时,需要将请求对象序列化后通过网络发送到服务端;服务端接收到请求后,需要进行反序列化,将字节流转换为服务端可以理解的对象,然后执行相应的操作。操作完成后,服务端将结果序列化后发送回客户端,客户端再进行反序列化以获取结果。

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

常见的序列化格式包括 JSON、Kryo、Protocol Buffers、Hessian等

1).在RPC模块编写Serizlizer接口,便于后续拓展

package com.hpq.serializer;

import java.io.IOException;

public interface Serializer {
    /**
     * 序列化
     * @param object
     * @return
     * @param <T>
     */
    <T> byte[] serialize(T object) throws IOException;

    /**
     * 反序列化
     * @param data
     * @param clazz
     * @return
     * @param <T>
     */
    <T> T deserialize(byte[] data, Class<T> clazz) throws IOException;
}

 2).在这里首先完成java原生的序列化接口的实现类jdkSerializer

package com.hpq.serializer;

import java.io.*;

/**
 * jdk自带的序列化器
 */
public class jdkSerializer implements Serializer{
    /**
     * 序列化
     * @param object
     * @return
     * @param <T>
     * @throws IOException
     */
    @Override
    public <T> byte[] serialize(T object) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        try {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
            objectOutputStream.writeObject(object);
            objectOutputStream.close();
            return byteArrayOutputStream.toByteArray();
        } catch (IOException e) {
            throw e;
        }
    }
    /**
     * 反序列化
     * @param data
     * @param clazz
     * @return
     * @param <T>
     * @throws IOException
     */
    @Override
    public <T> T deserialize(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();
        }
    }
}

这里的序列化和反序列化代码为固定格式,无需死记硬背,知道原理即可 

多种序列化器的实现

        上面我们用java原生的序列化方式,但在不同的使用场景下这未必是最合适的序列化方式,因此我们考虑将JSON、Kryo、Hessian三种主流的序列化方式进行实现,以满足不同的需求

(1)首先需要在pom包中加入依赖:

<!--序列化-->
    <dependency>
      <groupId>com.esotericsoftware</groupId>
      <artifactId>kryo</artifactId>
      <version>5.6.0</version>
    </dependency>
    <dependency>
      <groupId>com.caucho</groupId>
      <artifactId>hessian</artifactId>
      <version>4.0.66</version>
    </dependency>

(2)然后再serializer中实现这三种依赖即可,此处可参考网络上的代码或用AI生成,不用死记硬背。

JSON序列化

json序列化相对较复杂,需要考虑对象转化的兼容性问题,比如Object数组在序列化后会丢失数据,需要我们在反序列化时进行特殊处理

package com.hpq.serializer;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.hpq.model.RpcRequest;
import com.hpq.model.RpcResponse;
import java.io.IOException;

/**
 * json序列化方式
 */
public class JsonSerializer implements Serializer{
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
     @Override
    public <T> byte[] serialize(T object) throws IOException {
        return OBJECT_MAPPER.writeValueAsBytes(object);
    }

    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) throws IOException {
      T  obj =OBJECT_MAPPER.readValue(data, clazz);
      if (obj instanceof RpcRequest) {
          return handleRequest((RpcRequest) obj, clazz);
      } else if (obj instanceof RpcResponse) {
          return handleResponse((RpcResponse) obj, clazz);
      } else {
          return obj;
      }
    }
    /**
     * 由于 Object 的原始对象会被擦除,导致反序列化时会被作为 LinkedHashMap 无法转换成原始对象,因此这里做了特殊处理
     *
     * @param rpcRequest rpc 请求
     * @param type       类型
     * @return {@link T}
     * @throws IOException IO异常
     */
    private <T> T handleRequest(RpcRequest rpcRequest, Class<T> type) throws IOException {
        Class<?>[] parameterTypes = rpcRequest.getParameterTypes();
        Object[] args = rpcRequest.getParameters();

        // 循环处理每个参数的类型
        for (int i = 0; i < parameterTypes.length; i++) {
            Class<?> clazz = parameterTypes[i];
            // 如果类型不同,则重新处理一下类型
            if (!clazz.isAssignableFrom(args[i].getClass())) {
                byte[] argBytes = OBJECT_MAPPER.writeValueAsBytes(args[i]);
                args[i] = OBJECT_MAPPER.readValue(argBytes, clazz);
            }
        }
        return type.cast(rpcRequest);
    }

    /**
     * 由于 Object 的原始对象会被擦除,导致反序列化时会被作为 LinkedHashMap 无法转换成原始对象,因此这里做了特殊处理
     *
     * @param rpcResponse rpc 响应
     * @param type        类型
     * @return {@link T}
     * @throws IOException IO异常
     */
    private <T> T handleResponse(RpcResponse rpcResponse, Class<T> type) throws IOException {
        // 处理响应数据
        byte[] dataBytes = OBJECT_MAPPER.writeValueAsBytes(rpcResponse.getData());
        rpcResponse.setData(OBJECT_MAPPER.readValue(dataBytes, rpcResponse.getDataType()));
        return type.cast(rpcResponse);
    }
}
}

Kryo序列化方式

由于Kryo是线程不安全的,所以使用ThreadLocal保证每一个线程里只有一个Kryo实例 

package com.hpq.serializer;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * Kryo序列化方式
 */
public class KryoSerializer implements Serializer{
    /**
     *Kryo线程不安全,使用ThreadLocal保证每一个线程只有一个Kryo实例
     */
    private static final ThreadLocal<Kryo> KRYO_THREAD_LOCAL = ThreadLocal.withInitial(() -> {
        Kryo kryo = new Kryo();
//设置动态序列化和反序列化类,不提前注册所有类(可能存在安全问题)
        kryo.setRegistrationRequired(false);
        return kryo;
    });

    @Override
    public <T> byte[] serialize(T object) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Output outPut = new Output(byteArrayOutputStream);
        KRYO_THREAD_LOCAL.get().writeObject(outPut, object);
        outPut.close();
        return byteArrayOutputStream.toByteArray();

    }

    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) throws IOException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
        Input input = new Input(byteArrayInputStream);
        T result = KRYO_THREAD_LOCAL.get().readObject(input, clazz);
        input.close();
        return result;
    }
}

 Hessian序列化

Hessian序列化方式实现比较简单,代码如下:

package com.hpq.serializer;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * Hessian序列化器
 */
public class HessianSerializer implements Serializer{

    @Override
    public <T> byte[] serialize(T object) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
        hessianOutput.writeObject(object);
        return byteArrayOutputStream.toByteArray();
    }

    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) throws IOException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
        HessianInput hessianInput = new HessianInput(byteArrayInputStream);
        return (T) hessianInput.readObject();
    }
}

动态使用序列化器 

在之前的序列化中我们硬编码了序列化器,

Serializer serializer = new JdkSerializer();

如果想要替换为其他的序列化器,就很麻烦!所以我们考虑使用配置文件指定序列化器,在使用序列化器是根据配置文件的不同获取不同的序列化器

因此我们可以定义一个序列化器名称=>序列化器实现类对象的Map,然后根据名称获取相应的序列化器即可
(1)首先在SerializerKeys类中定义序列化常量

代码如下:

package com.hpq.serializer;

/**
 * 序列化器常量
 */
public interface SerializerKeys {
    String JDK = "jdk";
    String KRYO = "kryo";
    String JSON = "json";
    String HESSIAN = "hessian";
}

(3)定义序列化工厂

在选择序列化方式时,我们没有必要每次都重复创建一个序列化实例,所以可以使用Map来维护序列化器的实例

package com.hpq.serializer;

import java.util.HashMap;

public class SerializerFactory {
    /**
     * 序列化映射
     */
  private static final HashMap<String, Serializer> SERIALIZER_MAP =  new HashMap<String, Serializer>(){{
      put(SerializerKeys.HESSIAN, new HessianSerializer());
      put(SerializerKeys.JSON, new JsonSerializer());
      put(SerializerKeys.KRYO, new KryoSerializer());
      put(SerializerKeys.JDK, new jdkSerializer());
    }};
    /**
     * 默认序列化器
     */
    private static final Serializer DEFAULT_SERIALIZER = SERIALIZER_MAP.get(SerializerKeys.JDK);

    /**
     * 获取实例
     * @param key
     * @return
     */

  public static Serializer getSerializer(String key){
      return SERIALIZER_MAP.getOrDefault(key, DEFAULT_SERIALIZER);
  }

}

 (3)在全局配置RpcConfig类中加入序列化器的配置

package com.hpq.config;


import com.hpq.serializer.SerializerKeys;
import lombok.Data;

/**
 * Rpck框架全局配置
 *
 */
@Data
public class RpcConfig {
    //名称
    private String name= "hpq-rpc-plus";
    //版本号
    private String version= "1.0.0";
    //服务器主机名
    private String serverHost= "localhost";
    //服务器端口
    private Integer serverPort= 8080;
    //模拟调用
    private boolean mock= false;
    //序列化器
    private String serializer = SerializerKeys.JDK;

}

(4) 动态获取序列化器

需要将之前使用硬编码的部分进行重构,使用工厂模式+读取配置的方法获取序列化器,需要重构的类:

ServiceProxy

HttpServerHandler

更改的代码:

       final Serializer serializer = SerializerFactory.getSerializer(RpcApplication.getRpcConfig().getSerializer());

 自定义序列化器

如果开发者不想用内置的序列化器,而是要自定义一个新的序列化器接口的实现,同时又不修改已经写好的框架代码。要实现这一想法我们只需要让RPC框架能够读取到用户自定义的类路径,然后加载这个类,作为Serializer序列化接口的实现类即可。同时要想实现这个操作,我们需要用到java中的SPI机制。

什么是SPI?

SPI(Service Provider Interface) 是一种在软件开发中用于实现模块化和可扩展性的设计模式。它允许一个服务接口有多个实现,允许服务提供者可以通过特定的配置文件将自己的实现注册到系统中,客户端可以通过某种机制动态地选择和加载所需的服务实现,而不需要修改代码。

如何实现SPI

(1)系统实现

Java内部已经提供了相关的api接口,可以直接使用

  • 首先在rpc模块的resource文件夹下新建META-INF/services目录,并创建一个名称为要实现接口的空文件夹

  • 在文件中写入自己定制的接口实现类的完整类路径,如下所示:

  • 直接使用系统内置的ServiceLoader动态加载指定接口的实现类。
// 指定序列化器
Serializer serializer = null;
ServiceLoader<Serializer> serviceLoader = ServiceLoader.load(Serializer.class);
for (Serializer service : serviceLoader) {
    serializer = service;
}

 该代码可以直接获取到所有文件中编写的实现类对象,选择一个使用即可。

(2) 自定义SPI实现

系统实现虽然简单,但是如果我们想要定制多个接口实现类,就无法指定使用哪一个了,就无法实现通过配置文件动态加载序列化器。因此我们使用自定义的spi机制,支持用户自定义序列化器并制定键名。

  • 指定SPI配置目录

在系统实现中,我们让其自动加载resource文件夹下的META-INF/services目录中的文件,那么在自定义SPI中我i们可以改为加载META-INF/rpc目录

同样我们可以将SPI机制分为系统内置和自定义SPI,目录如下:

custom目录作为自定义SPI,可以新建配置,加载自定义实现类

system目录作为系统内置SPI,里面包括了RPC框架自带的实现类

这样所有的接口都可以通过过SPI动态加载,不需要在代码中硬编码Map来维护实现类。

我们首先在system中编写一个拓展配置文件,文件名为com.hpq.serializer.Serializer,代码如下:

  • 编写SpiLoader加载器

 我们自定义一个SPI加载器,用于用于动态加载和管理特定接口的实现类。

关键实现如下:

1.用Map存储已加载的配置信息

2.扫描指定路径,读取配置文件,获取到键名=>实现类的信息并存储到Map

3.定义获取实例的方法,根据给定的接口类和键值,从缓存中获取或创建其实例,可以维护一个对象实例缓存,创建过的对象从实例缓存中读取即可。

代码如下:

package com.yupi.yurpc.spi;

import cn.hutool.core.io.resource.ResourceUtil;
import com.yupi.yurpc.serializer.Serializer;
import lombok.extern.slf4j.Slf4j;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * SPI 加载器
 * 自定义实现,支持键值对映射
 *
 * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
 * @learn <a href="https://codefather.cn">程序员鱼皮的编程宝典</a>
 * @from <a href="https://yupi.icu">编程导航知识星球</a>
 */
@Slf4j
public class SpiLoader {

    /**
     * 存储已加载的类:接口名 =>(key => 实现类)
     */
    private static final Map<String, Map<String, Class<?>>> loaderMap = new ConcurrentHashMap<>();

    /**
     * 对象实例缓存(避免重复 new),类路径 => 对象实例,单例模式
     */
    private static final Map<String, Object> instanceCache = new ConcurrentHashMap<>();

    /**
     * 系统 SPI 目录
     */
    private static final String RPC_SYSTEM_SPI_DIR = "META-INF/rpc/system/";

    /**
     * 用户自定义 SPI 目录
     */
    private static final String RPC_CUSTOM_SPI_DIR = "META-INF/rpc/custom/";

    /**
     * 扫描路径
     */
    private static final String[] SCAN_DIRS = new String[]{RPC_SYSTEM_SPI_DIR, RPC_CUSTOM_SPI_DIR};

    /**
     * 动态加载的类列表
     */
    private static final List<Class<?>> LOAD_CLASS_LIST = Arrays.asList(Serializer.class);

    /**
     * 加载所有类型
     */
    public static void loadAll() {
        log.info("加载所有 SPI");
        for (Class<?> aClass : LOAD_CLASS_LIST) {
            load(aClass);
        }
    }

    /**
     * 获取某个接口的实例
     *
     * @param tClass
     * @param key
     * @param <T>
     * @return
     */
    public static <T> T getInstance(Class<?> tClass, String key) {
        String tClassName = tClass.getName();
        Map<String, Class<?>> keyClassMap = loaderMap.get(tClassName);
        if (keyClassMap == null) {
            throw new RuntimeException(String.format("SpiLoader 未加载 %s 类型", tClassName));
        }
        if (!keyClassMap.containsKey(key)) {
            throw new RuntimeException(String.format("SpiLoader 的 %s 不存在 key=%s 的类型", tClassName, key));
        }
        // 获取到要加载的实现类型
        Class<?> implClass = keyClassMap.get(key);
        // 从实例缓存中加载指定类型的实例
        String implClassName = implClass.getName();
        if (!instanceCache.containsKey(implClassName)) {
            try {
                instanceCache.put(implClassName, implClass.newInstance());
            } catch (InstantiationException | IllegalAccessException e) {
                String errorMsg = String.format("%s 类实例化失败", implClassName);
                throw new RuntimeException(errorMsg, e);
            }
        }
        return (T) instanceCache.get(implClassName);
    }

    /**
     * 加载某个类型
     *
     * @param loadClass
     * @throws IOException
     */
    public static Map<String, Class<?>> load(Class<?> loadClass) {
        log.info("加载类型为 {} 的 SPI", loadClass.getName());
        // 扫描路径,用户自定义的 SPI 优先级高于系统 SPI
        Map<String, Class<?>> keyClassMap = new HashMap<>();
        for (String scanDir : SCAN_DIRS) {
            List<URL> resources = ResourceUtil.getResources(scanDir + loadClass.getName());
            // 读取每个资源文件
            for (URL resource : resources) {
                try {
                    InputStreamReader inputStreamReader = new InputStreamReader(resource.openStream());
                    BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
                    String line;
                    while ((line = bufferedReader.readLine()) != null) {
                        String[] strArray = line.split("=");
                        if (strArray.length > 1) {
                            String key = strArray[0];
                            String className = strArray[1];
                            keyClassMap.put(key, Class.forName(className));
                        }
                    }
                } catch (Exception e) {
                    log.error("spi resource load error", e);
                }
            }
        }
        loaderMap.put(loadClass.getName(), keyClassMap);
        return keyClassMap;
    }

}
  • 重构序列化工厂

在之前我们采用硬编码存储序列化器和实现类,有了SPI我们可以从SPI加载指定序列化器的对象 ,代码如下:

package com.hpq.serializer;

import com.hpq.spi.SpiLoader;

import java.util.HashMap;

public class SerializerFactory {

    static{
    SpiLoader.load( Serializer.class);
}
    /**
     * 默认序列化器
     */
    private static final Serializer DEFAULT_SERIALIZER = new jdkSerializer();

    /**
     * 获取实例
     * @param key
     * @return
     */

  public static Serializer getSerializer(String key){
      return SpiLoader.getInstance(Serializer.class, key);
  }

}

使用静态代码块,在工厂首次加载时,调用Spiloader的load方法加载序列化器接口的所有实现类,之后通过getInstance方法获取到指定实现类对象。

5.注册中心基本实现

        在之前的设计中我们创建了一个本地的注册中心用于存储和读取注册信息等,但这种注册中心不具有高可用性和持久性,如果服务实例崩溃或重启,注册信息会丢失。在服务实例数量较少且服务调用频率不高的情况下可能足够。但随着服务数量的增加,管理和维护本地注册中心变得复杂。

        因此通常我们考虑使用第三方注册中心,主流的注册中心实现中间件有Redis、Zoookeeper等,而在本项目中我们选用一种更新颖,更适合存储元信息(注册信息)的中间件Etcd,来实现注册中心。

Github:https://github.com/etcd-io/etcd

Etcd安装

进入 Etcd 官方的下载页:https://github.com/etcd-io/etcd/releases

也可以在这里下载:Install | etcd

找到自己操作系统的版本执行即可

安装完成后,会得到 3 个脚本:

  • etcd:etcd 服务本身
  • etcdctl:客户端,用于操作 etcd,比如读写数据
  • etcdutl:备份恢复工具

执行 etcd 脚本后,可以启动 etcd 服务,服务默认占用 2379 和 2380 端口,作用分别如下:

  • 2379:提供 HTTP API 服务,和 etcdctl 交互
  • 2380:集群中节点间通讯

Etcd 可视化工具

一般情况下,我们使用数据存储中间件时,一定要有一个可视化工具,能够更直观清晰地管理已经存储的数据。比如 Redis 的 Redis Desktop Manager。

同样的,Etcd 也有一些可视化工具,比如:

更推荐 etcdkeeper,安装成本更低,学习使用更方便。

进入项目的 GitHub,就能看到安装方式,直接按照指引下载、解压、运行脚本即可:

安装后,执行命令,可以在指定端口启动可视化界面(默认是 8080 端口),比如在 8081 端口启动。

./etcdkeeper -p 8081

 安装后,访问本地 http://127.0.0.1:8081/etcdkeeper/就可以看到可视化界面了

注册中心开发

Etcd Java 客户端

所谓客户端,就是操作 Etcd 的工具。

etcd 主流的 Java 客户端是 jetcd:https://github.com/etcd-io/jetcd。

注意,Java 版本必须大于 11!

用法非常简单,就像 curator 能够操作 ZooKeeper、jedis 能够操作 Redis 一样。

1)首先在项目中引入 jetcd:

<!-- https://mvnrepository.com/artifact/io.etcd/jetcd-core -->
<dependency>
    <groupId>io.etcd</groupId>
    <artifactId>jetcd-core</artifactId>
    <version>0.7.7</version>
</dependency>

(1)注册信息定义

在model包下创建ServiceMeatInfo类用于存储注册信息,如服务版本,服务端口,服务名称等信息。代码如下:

package com.hpq.model;

import lombok.Data;

/**
 * 注册信息
 */
@Data
public class ServiceMetaInfo {
    //服务名称
    private String serviceName;
    //服务版本
    private String serviceVersion = "1.0.0";
    //服务地址
    private String serviceHost;
    //服务端口
    private Integer servicePort;
    //服务分组
    private String serviceGroup = "default";
}

除此以外我们还需要在ServiceMetaInfo类增加工具方法,用于获取服务键名,获取服务注册节点键名。

 /**
     * 获取服务键名
     */
    public String getServiceKey()
    {
        return String.format("%s:%s", serviceName, serviceVersion);
    }
    /**
     * 获取服务节点键名
     */
    public String getServiceNodeKey()
    {
        return String.format("%s/%s:%s",getServiceKey(), serviceHost, servicePort);
    }
    /**
     * 获取完整服务地址
     */
    public String getServiceAddress(){
        if (!StrUtil.contains(serviceHost, "http")){
            return String.format("http://%s:%s", serviceHost, servicePort);
        }
        return String.format("%s:%s", serviceHost, servicePort);
    }

 在注册信息中包含了服务版本号,因此为我们在RpcConstant对象中补充默认服务版本号,暂设为设为"1.0.0"。

package com.hpq.constant;

/**
 * RPC相关常量
 * @author hpq
 */
public interface RpcConstant {
    //默认配置文件加载前缀
    String RPC_CONFIG_PREFIX = "rpc";
    //默认服务版本
    String DEFAULT_SERVICE_VERSION = "1.0.0";

}

在RpcRequest中使用该常量

package com.hpq.model;

import com.hpq.constant.RpcConstant;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * Rpc请求
 */
@Data
@AllArgsConstructor
@Builder
@NoArgsConstructor
public class RpcRequest implements Serializable {
    // 服务接口名
    private String interfaceName;
    // 服务方法名
    private String methodName;
    //服务版本
    private String serviceVersion= RpcConstant.DEFAULT_SERVICE_VERSION;
    // 参数类型列表
    private Class<?>[] parameterTypes;
    // 参数列表
    private Object[] parameters;
}

(2) 注册中心配置 

在config包下创建RegistryConfig类,用于配置连接注册中心所需要的信息,如服务接口名,服务方法名,服务版本,参数列表等。

package com.hpq.config;

import com.hpq.register.RegisterKeys;
import lombok.Data;
/**
 * Rpc注册中心配置
 */
@Data
public class RegisterConfig {
    //注册中心类别
    private String registry = RegisterKeys.ETCD;
    //注册中心地址
    private String address= "http://localhost:2380";
    //用户名
    private String username ;
    //密码
    private String password;
    //超时时间
    private Long timeout = 5000L;

}

        然后在RpcConfig全局配置中补充注册中心配置信息,代码如下:

package com.hpq.config;

import com.hpq.fault.retry.RetryStrategyKeys;
import com.hpq.fault.tolerant.TolerantStrategyKeys;
import com.hpq.loadbalancer.LoadBalancerKeys;
import com.hpq.serializer.SerializerKeys;
import lombok.Data;

/**
 * Rpck框架全局配置
 *
 */
@Data
public class RpcConfig {
   ......
    //注册中心配置
    private RegisterConfig registerConfig = new RegisterConfig();


}

(3)  注册中心接口

为了实现拓展性 ,我们首先定义一个Registry接口,后面就可以实现多种不同的注册中心,并且和序列化器一样,可以使用SPI机制动态加载。

注册中心接口主要实现初始化,注册服务,注销服务,服务发现,销毁服务等。

package com.hpq.register;

import com.hpq.config.RegisterConfig;
import com.hpq.model.ServiceMetaInfo;

import java.util.List;

/**
 * 注册中心
 */
public interface Registry {
    //初始化
    void init(RegisterConfig registerConfig);
    //注册服务(服务端)
    void register(ServiceMetaInfo serviceMetaInfo) throws  Exception;
    //注销服务(服务端)
    void unregister(ServiceMetaInfo serviceMetaInfo) throws  Exception;

    /**
     * 服务发现(服务端)
     */
    List<ServiceMetaInfo> serviceDiscovery(String serviceKey) throws  Exception;
   
    //服务销毁
    void destroy();



}

(4) Etcd注册中心的实现

在registry包下创建EtcdRegistry类,实现Registry接口,首先完成初始化方法,读取注册中心配置和初始化客户端。

public class EtcdRegistry implements  Registry{
    private Client client;
    private KV kvClient;
    //etcd的根节点
    private static final String ETCD_ROOT_PATH = "/rpc/";
    //
    @Override
    public void init(RegisterConfig registerConfig) {
        client= Client.builder()
                .endpoints(registerConfig.getAddress())
                .connectTimeout( Duration.ofMillis(registerConfig.getTimeout()))
                .build();
        kvClient = client.getKVClient();
    }
}

 接下来实现服务注册方法,创建key并设置过期时间,value为服务注册信息的json序列化,代码如下:

    /**
     * 注册服务到ETCD
     * 通过创建一个有限期的租约来确保服务心跳机制,从而实现服务自动下线功能
     */
    @Override
    public void register(ServiceMetaInfo serviceMetaInfo) throws Exception {
        //创建Lease和KV客户端
        Lease leaseClient = client.getLeaseClient();
        //创建一个30秒租约
        long leaseId = leaseClient.grant(30).get().getID();
        //设置要存储的键值对
        String key = ETCD_ROOT_PATH + serviceMetaInfo.getServiceKey();
        ByteSequence keyByteSequence = ByteSequence.from(key, StandardCharsets.UTF_8);
        ByteSequence valueByteSequence = ByteSequence.from(JSONUtil.toJsonStr(serviceMetaInfo), StandardCharsets.UTF_8);
        //将键值对与租约关联,30秒后过期
        PutOption putOption = PutOption.builder().withLeaseId(leaseId).build();
        kvClient.put(keyByteSequence, valueByteSequence, putOption).get();
    }

接下来是服务注销,删除key:


    /**
     * 注销服务
     * @param serviceMetaInfo
     * @throws Exception
     */
    @Override
    public void unregister(ServiceMetaInfo serviceMetaInfo) throws Exception {
        kvClient.delete(ByteSequence.from(ETCD_ROOT_PATH + serviceMetaInfo.getServiceNodeKey(), StandardCharsets.UTF_8));
    }

 然后实现服务发现方法,根据服务名称作为前缀,从Etcd获取服务下的节点列表。

    /**
     * 服务发现
     * @param serviceKey
     * @return
     * @throws Exception
     */
    @Override
    public List<ServiceMetaInfo> serviceDiscovery(String serviceKey) throws Exception {
        //前缀搜索,结尾加“/”
        String searchPrefix = ETCD_ROOT_PATH + serviceKey + "/";
        try{
            //前缀查询
            GetOption getOption =GetOption.builder().isPrefix(true).build();
           List<KeyValue> keyValues =  kvClient.get(ByteSequence.from(searchPrefix, StandardCharsets.UTF_8), getOption).get().getKvs();
            //解析服务信息
            return keyValues.stream()
                    .map(keyValue -> JSONUtil.toBean(keyValue.getValue().toString(), ServiceMetaInfo.class))
                    .collect(Collectors.toList());

        }catch (Exception e){
            throw new RuntimeException("获取服务列表失败",e);
        }
    }

 最后是注册中心销毁,用于项目关闭后释放资源

    @Override
    public void destroy() {
        System.out.println("当前节点下线");
        if (client != null){
            client.close();
        }
        if (kvClient != null){
            kvClient.close();
        }
    }

注册中心配置和拓展 

在这里我们同样可以通过填写配置指定支持多个注册中心,就像序列化器一样,并且支持自定义注册中心,让框架更易于拓展 

(1)注册中心常量

在registry包下新建RegistryKeys类,列举所有支持注册中心的键名

package com.hpq.register;

/**
 * 注册中心常量
 */
public interface RegisterKeys {
    String ETCD = "etcd";
    String ZK = "zookeeper";
}

(2) 使用工厂模式,支持根据key从注册中心获取实例,在registry包下创建RegistryFactory类

package com.hpq.register;

import com.hpq.spi.SpiLoader;

/**
 * 注册中心工厂
 */
public class RegistryFactory {
    static{
        SpiLoader.load(Registry.class);
    }
    /**
     * 默认注册中心
     */
    private static final Registry DEFAULT_REGISTRY=new EtcdRegistry();

    /**
     * 获取注册中心实例
     */
    public static Registry getInstance(String key){
        return SpiLoader.getInstance(Registry.class,key);
    }
}

 (3)在META-INF的rpc/system目录下编写注册接口SPI配置文件。

代码如下:

etcd = com.hpq.registry.EtcdRegistry

(4) 由于在服务提供者和服务消费者都需要启动注册中心,所以我们将注册中心的初始化放在RpcApplication中,代码如下:

        /**
         * 初始化框架,支持自定义配置
         */
        public static void init(RpcConfig newRpcConfig) {
            rpcConfig = newRpcConfig;
            log.info("rpc config:{}", rpcConfig);
            //注册中心初始化
            RegisterConfig registerConfig = rpcConfig.getRegisterConfig();
            Registry registry = RegistryFactory.getInstance(registerConfig.getRegistry());
            registry.init(registerConfig);
            log.info("registry init success, config = {}", registerConfig);
        }

 服务调用

(1)服务消费者需要从注册中心获取信息,得到调用地址并执行。

需要在ServiceMetaInfo类中增加获取调用地址的代码,如下:

        /**
         * 获取完整服务地址
         */
        public String getServiceAddress(){
            if (!StrUtil.contains(serviceHost, "http")){
                return String.format("http://%s:%s", serviceHost, servicePort);
            }
            return String.format("%s:%s", serviceHost, servicePort);
        }

(2)  修改服务代理类ServiceProxy,更改调用逻辑

package com.hpq.proxy;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.hpq.RpcApplication;
import com.hpq.config.RpcConfig;
import com.hpq.constant.RpcConstant;
import com.hpq.model.RpcRequest;
import com.hpq.model.RpcResponse;
import com.hpq.model.ServiceMetaInfo;
import com.hpq.register.Registry;
import com.hpq.register.RegistryFactory;
import com.hpq.serializer.Serializer;
import com.hpq.serializer.SerializerFactory;
import com.hpq.serializer.jdkSerializer;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;

/**
 * jdk动态代理
 */
public class ServiceProxy implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 指定序列化器
        final Serializer serializer = SerializerFactory.getSerializer(RpcApplication.getRpcConfig().getSerializer());
        String serviceName = method.getDeclaringClass().getName();
        // 构造请求
        RpcRequest rpcRequest = RpcRequest.builder()
                .interfaceName(method.getDeclaringClass().getName())
                .methodName(method.getName())
                .parameterTypes(method.getParameterTypes())
                .parameters(args)
                .build();
        try {
            // 序列化
            byte[] bodyBytes = serializer.serialize(rpcRequest);

            RpcConfig rpcConfig = RpcApplication.getRpcConfig();
            Registry registry = RegistryFactory.getInstance(rpcConfig.getRegisterConfig().getRegistry());
            ServiceMetaInfo metaInfo = new ServiceMetaInfo();
            metaInfo.setServiceName(serviceName);
            metaInfo.setServiceVersion(RpcConstant.DEFAULT_SERVICE_VERSION);
            List<ServiceMetaInfo> serviceMetaInfoList = registry.serviceDiscovery(metaInfo.getServiceKey());
            if (CollUtil.isEmpty(serviceMetaInfoList)){
                throw new RuntimeException("未发现服务");
            }
            ServiceMetaInfo serviceMetaInfo = serviceMetaInfoList.get(0);
            // 发送请求
            // ,这里地址被硬编码了(需要使用注册中心和服务发现机制解决)
            try (HttpResponse httpResponse = HttpRequest.post(serviceMetaInfo.getServiceAddress())
                    .body(bodyBytes)
                    .execute()) {
                byte[] result = httpResponse.bodyBytes();
                // 反序列化
                RpcResponse rpcResponse = serializer.deserialize(result, RpcResponse.class);
                return rpcResponse.getData();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}
6.注册中心优化
1).心跳检测和续期机制

        心跳检测是一种定期发送的消息或信号,用于验证连接的另一端是否仍然活跃。这种机制常用于保持会话的持续性,防止连接超时断开,或者快速发现连接中断的问题。
在分布式系统中,心跳机制可以用来监控节点的健康状态,确保集群中的成员能够及时感知其他成员的状态变化,对于维护系统的高可用性至关重要。

        在etcd中实现心跳检测通常是通过租约(Lease)机制来完成的。租约允许客户端与etcd服务器建立一个租约关系,并将键与该租约相关联。当租约过期时,与之相关的所有键都会自动从etcd中删除。这种机制非常适合用于心跳检测,因为一旦客户端停止发送心跳(意味着服务可能已宕机),etcd就会自动清理与该服务相关的状态。

要实现心跳进检测和续期机制,可遵循以下步骤:

1.服务提供者向Etcd提供自己的注册信息,并在注册时设置TTL(生存时间)

2.Etcd接收到服务提供者的注册信息后,会自动维护服务信息的TTL,并在TTL过期时删除服务信息

3.服务提供者定期请求续签自己的注册信息,重写TTL。

开发实现

1.为注册中心的Registry接口添加心跳检测方法,代码如下:

public interface Registry {
    
     ....

    //心跳检测
    void heartbeat();
    }



2.维护续期节点集合

在EtcdRegistry实现类里定义一个本地注册节点的key集合用于维护续期

/**
 * 本机注册的节点 key 集合(用于维护续期)
 */
private final Set<String> localRegisterNodeKeySet = new HashSet<>();

同理,在服务注销时也要移除节点:

    /**
     * 注销服务
     * @param serviceMetaInfo
     * @throws Exception
     */
    @Override
    public void unRegister(ServiceMetaInfo serviceMetaInfo) throws Exception {
        //设置要存储的键值对
        String key = ETCD_ROOT_PATH + serviceMetaInfo.getServiceKey();
        kvClient.delete(ByteSequence.from(ETCD_ROOT_PATH + serviceMetaInfo.getServiceNodeKey(), StandardCharsets.UTF_8));
        LocalRegisteredNodes.remove(key);
    }

 3.在EtcdRegistry中实现heartBeat方法

为实现心跳检测功能,我们可以使用Hutool工具类的CronUtil实现定时任务,对集合中所有的节点进行重新注册的操作,相当于续签了,代码如下:
 

    /**
     * 心跳检测
     */
    @Override
    public void heartbeat() {
        CronUtil.schedule("*/10 * * * * *", new Task() {
            @Override
            public void execute() {
                //遍历本地注册节点的所有key
                for (String nodeKey:LocalRegisteredNodes){
                    try {
                        //获取key对应的value
                        List<KeyValue> keyValues =  kvClient.get(ByteSequence.from(nodeKey, StandardCharsets.UTF_8))
                                .get()
                                .getKvs();
                        // 判断key是否存在,若不存在则认为节点失效(需要重启节点才能重新注册)
                        if(keyValues.size()!=1 || CollUtil.isEmpty(keyValues)){
                            log.warn("{} 已过期,跳过续签", nodeKey);
                            continue;
                        }
                        //若节点未过期,重新注册(相当于续签)
                        KeyValue keyValue = keyValues.get(0);
                        String value = keyValue.getValue().toString();
                        ServiceMetaInfo serviceMetaInfo = JSONUtil.toBean(value,ServiceMetaInfo.class);
                        register(serviceMetaInfo);
                    }catch(Exception e){
                        throw  new RuntimeException(nodeKey + "心跳续约失败",e);
                    }
                }
            }
        });
        //支持秒级别的定时任务
        CronUtil.setMatchSecond(true);
        CronUtil.start();
    }

如果节点信息仍然存在但即将过期,可以通过重新注册来延长节点的有效期。这样即使 Etcd 注册中心的数据出现了丢失,也可以通过心跳检测机制重新注册节点信息。

4.开启heartbeat

 在注册中心初始化的 init 方法中,调用 heartBeat 方法即可

    //初始化
    @Override
    public void init(RegistryConfig registerConfig) {
        client= Client.builder()
                .endpoints(registerConfig.getAddress())
                .connectTimeout( Duration.ofMillis(registerConfig.getTimeout()))
                .build();
        kvClient = client.getKVClient();
        heartBeat();
    }

2) .服务下线机制

当服务提供者节点宕机时,应该从注册中心移除掉已注册的节点,否则会影响消费端调用。所以我们需要设计一套服务节点下线机制。

在这里我们利用 JVM 的 ShutdownHook 就能实现,JVM 的 ShutdownHook 是 Java 虚拟机提供的一种机制,允许开发者在 JVM 即将关闭之前执行一些清理工作或其他必要的操作,例如关闭数据库连接、释放资源、保存临时数据等

接下来我们实现Etcd注册中心的destory方法,完成服务下线逻辑:

    /**
     * 注册中心服务节点下线
     */
    @Override
    public void destroy() {
        System.out.println("当前节点下线");
        //遍历本地注册节点的所有节点
        for(String nodeKey:LocalRegisteredNodes){
            try{
                kvClient.delete(ByteSequence.from(nodeKey, StandardCharsets.UTF_8)).get();

            } catch (Exception e) {
                throw new RuntimeException(nodeKey + "节点下线失败");
            }

        }
        if (client != null){
            client.close();
        }
        if (kvClient != null){
            kvClient.close();
        }
    }

在 RpcApplication 的 init 方法中,注册 Shutdown Hook,当程序正常退出时会执行注册中心的 destroy 方法。

        /**
         * 初始化框架,支持自定义配置
         */
        public static void init(RpcConfig newRpcConfig) {
            ....

            //创建并注册Shutdown Hook,jvm退出时销毁注册中心
            Runtime.getRuntime().addShutdownHook(new Thread(registry::destroy));
        }
3).消费端服务缓存

        正常情况下,服务节点信息列表的更新频率是不高的,所以在服务消费者从注册中心获取到服务节点信息列表后,完全可以 缓存在本地,并在需要时从缓存中获取,而不是每次都从注册中心请求。这样可以减少网络请求,提高系统性能。

增加本地缓存

本地缓存的实现很简单,用一个列表来存储服务信息即可,提供操作列表的基本方法,包括:写缓存、读缓存、清空缓存。

在register包下新建RegistryServiceCache缓存类,代码如下:

package com.hpq.register;

import com.hpq.model.ServiceMetaInfo;
import java.util.List;

/**
 *注册中心服务本地缓存
 */
public class RegistryServiceCache {
    //服务缓存
    List<ServiceMetaInfo> serviceCache;
    //写缓存
    void writeCache(List<ServiceMetaInfo> serviceCache)
    {
        this.serviceCache = serviceCache;
    }
    //读缓存
    List<ServiceMetaInfo> readCache()
    {
        return serviceCache;
    }
    //清除缓存
    void clearCache()
    {
        serviceCache = null;
    }
}

 使用本地缓存

修改 EtcdRegisty 的代码,使用本地缓存对象:

    //注册中心本地缓存
    private final RegistryServiceCache registryServiceCache = new RegistryServiceCache();

修改服务发现逻辑,优先从缓存获取服务;如果没有缓存,再从注册中心获取,并且设置到缓存中。代码如下:

    @Override
    public List<ServiceMetaInfo> serviceDiscovery(String serviceKey) throws Exception {
        //优先从本地缓存获取服务
        List<ServiceMetaInfo> cacheServiceMetaInfoList = registryServiceCache.readCache();
        if (cacheServiceMetaInfoList != null){
            return cacheServiceMetaInfoList;
        }
        //前缀搜索,结尾加“/”
        String searchPrefix = ETCD_ROOT_PATH + serviceKey + "/";
        try{
            //前缀查询
            GetOption getOption =GetOption.builder().isPrefix(true).build();
           List<KeyValue> keyValues =  kvClient.get(ByteSequence.from(searchPrefix, StandardCharsets.UTF_8), getOption).get().getKvs();
            //解析服务信息
            List<ServiceMetaInfo> serviceMetaInfoList = keyValues.stream()
                    .map(keyValue ->{
                        String key = keyValue.getKey().toString(StandardCharsets.UTF_8);
                        watch(key);
                        String value = keyValue.getValue().toString(StandardCharsets.UTF_8);
                        return JSONUtil.toBean(value, ServiceMetaInfo.class);
                    })
                    .collect(Collectors.toList());
            //写入服务缓存
            registryServiceCache.writeCache(serviceMetaInfoList);
            return serviceMetaInfoList;
        }catch (Exception e){
            throw new RuntimeException("获取服务列表失败",e);
        }
    }

 服务缓存更新-监听机制

        当服务注册信息发生变更(比如节点下线)时,需要即时更新消费端缓存。因此我们需要知道服务注册信息什么时候发生变更,这就需要我们使用 Etcd 的 watch 监听机制,当监听的某个 key 发生修改或删除时,就会触发事件来通知监听者。

        我们首先要明确 watch 监听是服务消费者还是服务提供者执行的。由于我们的目标是更新缓存,缓存是在服务消费端维护和使用的,所以也应该是服务消费端去 watch。也就是说,只有服务消费者执行的方法中,可以创建 watch 监听器,那么比较合适的位置就是服务发现方法(serviceDiscovery)。可以对本次获取到的所有服务节点 key 进行监听。

1). 在Registry接口补充监听方法,代码如下:

public interface Registry {
    ....
    
    //监听
    void watch(String serviceNodeKey);

}

2).EtcdRegistry 类中,新增监听 key 的集合。

可以使用 ConcurrentHashSet 防止并发冲突,代码如下:

    //正在监听的key集合
    private final Set<String> watchingKeySets = new ConcurrentHashSet<>();

3). 在 EtcdRegistry 类中实现监听 key 的方法。

通过调用 Etcd 的 WatchClient 实现监听,如果出现了 DELETE key 删除事件,则清理服务注册缓存。

    @Override
    public void watch(String serviceNodeKey) {
        Watch clientWatch = client.getWatchClient();
        //之前未被监听,开启监听
        boolean newWatch = watchingKeySets.add(serviceNodeKey);
        if (newWatch){
            clientWatch.watch(ByteSequence.from(serviceNodeKey, StandardCharsets.UTF_8),response->{
                for(WatchEvent event : response.getEvents()){
                    switch (event.getEventType()){
                        //节点删除时触发
                        case DELETE:
                            //清除注册节点服务
                            registryServiceCache.clearCache();
                            break;
                        case PUT:
                            default:
                                break;
                    }
                }
            });
        }
    }

在消费端获取服务时调用 watch 方法,对获取到的服务节点 key 进行监听,

修改服务发现方法的代码如下:

    @Override
    public List<ServiceMetaInfo> serviceDiscovery(String serviceKey) throws Exception {
        //优先从本地缓存获取服务
        List<ServiceMetaInfo> cacheServiceMetaInfoList = registryServiceCache.readCache();
        if (cacheServiceMetaInfoList != null){
            return cacheServiceMetaInfoList;
        }
        //前缀搜索,结尾加“/”
        String searchPrefix = ETCD_ROOT_PATH + serviceKey + "/";
        try{
            //前缀查询
            GetOption getOption =GetOption.builder().isPrefix(true).build();
           List<KeyValue> keyValues =  kvClient.get(ByteSequence.from(searchPrefix, StandardCharsets.UTF_8), getOption).get().getKvs();
            //解析服务信息
            List<ServiceMetaInfo> serviceMetaInfoList = keyValues.stream()
                    .map(keyValue ->{
                        String key = keyValue.getKey().toString(StandardCharsets.UTF_8);
                        watch(key);
                        String value = keyValue.getValue().toString(StandardCharsets.UTF_8);
                        return JSONUtil.toBean(value, ServiceMetaInfo.class);
                    })
                    .collect(Collectors.toList());
            //写入服务缓存
            registryServiceCache.writeCache(serviceMetaInfoList);
            return serviceMetaInfoList;
        }catch (Exception e){
            throw new RuntimeException("获取服务列表失败",e);
        }
    }
4).zookeeper注册中心实现

这部分不作为学习重点,理解了一种注册中心的实现方式,再用其他技术实现注册中心就很简单了。

引入的依赖代码如下:

    <!-- zookeeper -->
    <dependency>
      <groupId>org.apache.curator</groupId>
      <artifactId>curator-x-discovery</artifactId>
      <version>5.6.0</version>
    </dependency>

 ZooKeeper 注册中心实现,这里不再赘述:

package com.hpq.register;

import cn.hutool.core.collection.ConcurrentHashSet;
import com.hpq.config.RegistryConfig;
import com.hpq.model.ServiceMetaInfo;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.CuratorCache;
import org.apache.curator.framework.recipes.cache.CuratorCacheListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.x.discovery.ServiceDiscovery;
import org.apache.curator.x.discovery.ServiceDiscoveryBuilder;
import org.apache.curator.x.discovery.ServiceInstance;
import org.apache.curator.x.discovery.details.JsonInstanceSerializer;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static io.vertx.core.http.impl.HttpClientConnection.log;

/**
 * zookeeper注册中心实现
 */
public class ZooKeeperRegistry implements Registry{

    private CuratorFramework client;

    private ServiceDiscovery<ServiceMetaInfo> serviceDiscovery;

    /**
     * 本机注册的节点 key 集合(用于维护续期)
     */
    private final Set<String> localRegisterNodeKeySet = new HashSet<>();

    /**
     * 注册中心服务缓存
     */
    private final RegistryServiceCache registryServiceCache = new RegistryServiceCache();

    /**
     * 正在监听的 key 集合
     */
    private final Set<String> watchingKeySet = new ConcurrentHashSet<>();

    /**
     * 根节点
     */
    private static final String ZK_ROOT_PATH = "/rpc/zk";


    @Override
    public void init(RegistryConfig registryConfig) {
        // 构建 client 实例
        client = CuratorFrameworkFactory
                .builder()
                .connectString(registryConfig.getAddress())
                .retryPolicy(new ExponentialBackoffRetry(Math.toIntExact(registryConfig.getTimeout()), 3))
                .build();

        // 构建 serviceDiscovery 实例
        serviceDiscovery = ServiceDiscoveryBuilder.builder(ServiceMetaInfo.class)
                .client(client)
                .basePath(ZK_ROOT_PATH)
                .serializer(new JsonInstanceSerializer<>(ServiceMetaInfo.class))
                .build();

        try {
            // 启动 client 和 serviceDiscovery
            client.start();
            serviceDiscovery.start();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void register(ServiceMetaInfo serviceMetaInfo) throws Exception {
        // 注册到 zk 里
        serviceDiscovery.registerService(buildServiceInstance(serviceMetaInfo));

        // 添加节点信息到本地缓存
        String registerKey = ZK_ROOT_PATH + "/" + serviceMetaInfo.getServiceNodeKey();
        localRegisterNodeKeySet.add(registerKey);
    }

    @Override
    public void unRegister(ServiceMetaInfo serviceMetaInfo) {
        try {
            serviceDiscovery.unregisterService(buildServiceInstance(serviceMetaInfo));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        // 从本地缓存移除
        String registerKey = ZK_ROOT_PATH + "/" + serviceMetaInfo.getServiceNodeKey();
        localRegisterNodeKeySet.remove(registerKey);
    }

    @Override
    public List<ServiceMetaInfo> serviceDiscovery(String serviceKey) {
        // 优先从缓存获取服务
        List<ServiceMetaInfo> cachedServiceMetaInfoList = registryServiceCache.readCache();
        if (cachedServiceMetaInfoList != null) {
            return cachedServiceMetaInfoList;
        }

        try {
            // 查询服务信息
            Collection<ServiceInstance<ServiceMetaInfo>> serviceInstanceList = serviceDiscovery.queryForInstances(serviceKey);

            // 解析服务信息
            List<ServiceMetaInfo> serviceMetaInfoList = serviceInstanceList.stream()
                    .map(ServiceInstance::getPayload)
                    .collect(Collectors.toList());

            // 写入服务缓存
            registryServiceCache.writeCache(serviceMetaInfoList);
            return serviceMetaInfoList;
        } catch (Exception e) {
            throw new RuntimeException("获取服务列表失败", e);
        }
    }

    @Override
    public void heartBeat() {
        // 不需要心跳机制,建立了临时节点,如果服务器故障,则临时节点直接丢失
    }

    /**
     * 监听(消费端)
     *
     * @param serviceNodeKey 服务节点 key
     */
    @Override
    public void watch(String serviceNodeKey) {
        String watchKey = ZK_ROOT_PATH + "/" + serviceNodeKey;
        boolean newWatch = watchingKeySet.add(watchKey);
        if (newWatch) {
            CuratorCache curatorCache = CuratorCache.build(client, watchKey);
            curatorCache.start();
            curatorCache.listenable().addListener(
                    CuratorCacheListener
                            .builder()
                            .forDeletes(childData -> registryServiceCache.clearCache())
                            .forChanges(((oldNode, node) -> registryServiceCache.clearCache()))
                            .build()
            );
        }
    }

    @Override
    public void destroy() {
        log.info("当前节点下线");
        // 下线节点(这一步可以不做,因为都是临时节点,服务下线,自然就被删掉了)
        for (String key : localRegisterNodeKeySet) {
            try {
                client.delete().guaranteed().forPath(key);
            } catch (Exception e) {
                throw new RuntimeException(key + "节点下线失败");
            }
        }

        // 释放资源
        if (client != null) {
            client.close();
        }
    }

    private ServiceInstance<ServiceMetaInfo> buildServiceInstance(ServiceMetaInfo serviceMetaInfo) {
        String serviceAddress = serviceMetaInfo.getServiceHost() + ":" + serviceMetaInfo.getServicePort();
        try {
            return ServiceInstance
                    .<ServiceMetaInfo>builder()
                    .id(serviceAddress)
                    .name(serviceMetaInfo.getServiceKey())
                    .address(serviceAddress)
                    .payload(serviceMetaInfo)
                    .build();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

SPI 增加对 ZooKeeper 的支持:

etcd=com.hpq.register.EtcdRegistry
zookeeper=com.hpq.register.ZooKeeperRegistry

最后,可以更改服务提供者和消费者的注册中心配置来测试。

更改的配置如下:

rpc.registryConfig.registry=zookeeper
rpc.registryConfig.address=localhost:2181
7.自定义协议

目前的 RPC 框架,我们使用 Vert.x 的 HttpServer 作为服务提供者的服务器,代码实现比较简单,其底层网络传输使用的是 HTTP 协议。

而 HTTP 协议中的头部信息、请求响应格式较 “重”,会影响网络传输性能。因此为提高RPC框架的性能,我们接下来会自定义一套 RPC 协议,比如利用 TCP 等传输层协议、自己定义请求响应结构,来实现性能更高、更灵活、更安全的 RPC 框架。

1) .设计方案

自定义RPC协议分为两大部分:

  • 自定义网络传输
  • 自定义消息结构

网络传输设计的目标是:选择一个能够高性能通信的网络协议和传输方式。

前面我们分析,Http协议的头信息很大,影响性能,而且HTTP 本身是应用层协议,性能肯定是不如底层(传输层)的 TCP 协议要高的。所以我们想要追求更高的性能,还是选择使用 TCP 协议完成网络传输。

消息结构设计的目标是:用 最少的 空间传递 需要的 信息。

在自定义消息结构时,想要节省空间,就要尽可能使用更轻量的类型,比如 byte 字节类型,只占用 1 个字节。

分析 HTTP 请求结构,我们能够得到 RPC 消息所需的信息:

  • 魔数:作用是安全校验,防止服务器处理了非框架发来的乱七八糟的消息(类似 HTTPS 的安全证书)
  • 版本号:保证请求和响应的一致性(类似 HTTP 协议有 1.0/2.0 等版本)
  • 序列化方式:来告诉服务端和客户端如何解析数据(类似 HTTP 的 Content-Type 内容类型)
  • 类型:标识是请求还是响应?或者是心跳检测等其他用途。(类似 HTTP 有请求头和响应头)
  • 状态:如果是响应,记录响应的结果(类似 HTTP 的 200 状态代码)

此外,还需要有请求 id,唯一标识某个请求,因为 TCP 是双向通信的,需要有个唯一标识来追踪每个请求。

最后,也是最重要的,要发送 body 内容数据。我们暂时称它为 请求体。因为 TCP 协议本身会存在半包和粘包问题,每次传输的数据可能是不完整的,所以我们需要在消息头中新增一个字段 请求体数据长度,保证能够完整地获取 body 内容信息。

2) .消息结构

新建 protocol 包,用于存放和自定义协议有关的代码

  • 新建协议消息类 ProtocolMessage

将消息头单独封装为一个内部类,消息体可以使用泛型类型,完整代码如下:

package com.hpq.protocol;

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

/**
 * 协议消息结构
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProtocolMessage<T> {
   //消息头
    private Header header;
    //消息体
    private T body;
    //协议消息头
    @Data
    public static class Header{
        //魔数
        private byte magic;
        //版本号
        private byte version;
        //序列化器
        private byte serializer;
        //消息类型(请求/响应)
        private byte type;
        //状态
        private byte status;
        //消息id
        private long id;
        //消息长度
        private int length;
    }

}
  •  新建协议常量类 ProtocolConstant

记录了和自定义协议有关的关键信息,比如消息头长度、魔数、版本号。完整代码如下:

package com.hpq.protocol;

/**
 * 协议常量
 */
public interface ProtocolConstant {
    //协议魔数
    byte PROTOCOL_MAGIC = 0x1;
    //消息头长度
    int MESSAGE_HEADER_LENGTH = 17;
    //协议版本号
    byte PROTOCOL_VERSION = 0x1;
}
  •  新建消息字段的枚举类,比如:

    协议状态枚举,暂时只定义成功、请求失败、响应失败三种枚举值:

package com.hpq.protocol;

import lombok.Getter;

/**
 * 协议状态枚举类
 */
@Getter
public enum ProtocolMessageStatusEnum {
    OK("ok",200),
    BAD_REQUEST("bad request",400),
    BAD_RESPONSE("bad response",500);
    private  final String text;
    private final int value;

    ProtocolMessageStatusEnum(String text, int value) {
        this.text = text;
        this.value = value;
    }
    /**
     * 根据状态码获取枚举
     */
    public static ProtocolMessageStatusEnum getByValue(int value){
        for(ProtocolMessageStatusEnum statusEnum : ProtocolMessageStatusEnum.values()){
            if(statusEnum.value == value){
                return statusEnum;
            }
        }
        return null;
    }
}

协议消息类型枚举,包括请求、响应、心跳、其他。代码如下:

package com.hpq.protocol;

import lombok.Getter;

/**
 * 协议类型枚举
 */
@Getter
public enum ProtocolMessageTypeEnum {
    REQUEST(0),
    RESPONSE(1),
    HEART_BEAT(2),
    OTHERS(3);
    private final int value;
    ProtocolMessageTypeEnum(int value) {
        this.value = value;
    }
    /**
     * 根据value获取枚举
     */
    public static ProtocolMessageTypeEnum getByValue(int value) {
        for (ProtocolMessageTypeEnum typeEnum : ProtocolMessageTypeEnum.values()) {
            if (typeEnum.value == value) {
                return typeEnum;
            }
        }
        return null;
    }

}

 协议消息的序列化器枚举,跟我们 RPC 框架已支持的序列化器对应。代码如下:

package com.hpq.protocol;

import cn.hutool.core.util.ObjectUtil;
import lombok.Getter;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 协议消息序列化枚举
 */
@Getter
public enum ProtocolMessageSerializerEnum {
    JDK(0,"jdk"),
    JSON(1,"json"),
    KRYO(2,"kryo"),
    HESSIAN(3,"hessian");
    private final int key;
    private final String value;

    ProtocolMessageSerializerEnum(int key, String value) {
        this.key = key;
        this.value = value;
    }
    /**
     * 获取值列表
     */
    public static List<String> getValues(){
        return Arrays.stream(values()).map(item->item.value).collect(Collectors.toList());
    }
    /**
     * 根据value获取枚举
     */
    public static ProtocolMessageSerializerEnum getByValue(String value) {
        if (ObjectUtil.isEmpty(value)){
            return null;
        }
        for (ProtocolMessageSerializerEnum typeEnum : ProtocolMessageSerializerEnum.values()){
            if (typeEnum.value.equals(value)){
                return typeEnum;
            }
        }
        return null;
    }
}
3) .网络传输

我们的 RPC 框架使用了高性能的 Vert.x 作为网络传输服务器,之前用的是 HttpServer。同样,Vert.x 也支持 TCP 服务器,相比于 Netty 或者自己写 Socket 代码,更加简单易用。 

新建server.tcp包,将TCP服务相关的代码放在这里。

  • TCP服务器实现

新建一个VertxTcpServer,跟之前写的 VertxHttpServer 类似,先创建 Vert.x 的服务器实例,然后定义处理请求的方法,比如回复 “Hello, client!”,最后启动服务器。

package com.hpq.server.tcp;

import com.hpq.server.HttpServer;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetServer;

public class VertxTcpServer implements HttpServer {
    private byte[] handleRequest(byte[] requestData){
        return "Hello,client!".getBytes();
    }
    @Override
    public void doStart(int port) {
        //创建vert.x实例
        Vertx vertx = Vertx.vertx();
        //创建TCP服务器
        NetServer server = vertx.createNetServer();
        //处理请求
        server.connectHandler(socket -> {
            //读取客户端发送的数据
            socket.handler(buffer -> {
                //获取请求数据
                byte[] requestData = buffer.getBytes();
                //处理请求
                byte[] responseData = handleRequest(requestData);
                //发送响应数据
                socket.write(Buffer.buffer(responseData));
            });
        });
        //启动服务器,并监听端口
        server.listen(port, result ->{
            if(result.succeeded()){
                System.out.println("TCP服务器启动成功,监听端口:" + port);
            }else{
                System.out.println("TCP服务器启动失败:" + result.cause());
            }
        });
    }

    public static void main(String[] args) {
        VertxTcpServer server = new VertxTcpServer();
        server.doStart(8888);
    }
}
  • TCP客户端实现

新建 VertxTcpClient 类,先创建 Vert.x 的客户端实例,然后定义处理请求的方法,比如回复 “Hello, server!”,并建立连接。 

package com.hpq.server.tcp;

import io.vertx.core.Vertx;
import io.vertx.core.net.NetSocket;

/**
 * Tcp客户端
 */
public class VertxTcpClient {
    public void start(){
        //创建vert.x实例
        Vertx vertx = Vertx.vertx();
        //创建TCP客户端
        vertx.createNetClient().connect(8888,"localhost",result -> {
            if(result.succeeded()){
                //连接成功
                System.out.println("连接成功");
                NetSocket socket = result.result();
                socket.write("hello server");
                socket.handler(data -> {
                    System.out.println("收到服务器消息:" + data.toString());
                });

            }else{
                //连接失败
                System.out.println("连接失败");
            }
        });
    }

    public static void main(String[] args) {
        VertxTcpClient client = new VertxTcpClient();
        client.start();
    }
}

完成编写后先启动服务器,再启动客户端,能够在控制台看到它们互相打招呼的输出即可。 

4) .编码/解码器

在上一步中,我们也注意到了,Vert.x 的 TCP 服务器收发的消息是 Buffer 类型,不能直接写入一个对象。因此,我们需要编码器和解码器,将 Java 的消息对象和 Buffer 进行相互转换。

  • 首先实现消息编码器

在protocol包下新建ProtocolMessageEncoder,核心流程是依次向 Buffer 缓冲区写入消息对象里的字段。

package com.hpq.protocol;

import com.hpq.serializer.Serializer;
import com.hpq.serializer.SerializerFactory;
import io.vertx.core.buffer.Buffer;

import java.io.IOException;

/**
 * 消息编码器
 */
public class ProtocolMessageEncoder {
    public static Buffer encode(ProtocolMessage<?> protocolMessage) throws IOException {
        if (protocolMessage == null || protocolMessage.getHeader() == null) {
            return Buffer.buffer();
        }
        ProtocolMessage.Header header = protocolMessage.getHeader();
        // 依次向缓冲区写入字节
        Buffer buffer = Buffer.buffer();
        buffer.appendByte(header.getMagic());
        buffer.appendByte(header.getVersion());
        buffer.appendByte(header.getSerializer());
        buffer.appendByte(header.getType());
        buffer.appendByte(header.getStatus());
        buffer.appendLong(header.getRequestId());
        // 获取序列化器
        ProtocolMessageSerializerEnum serializerEnum = ProtocolMessageSerializerEnum.getByKey(header.getSerializer());
        if (serializerEnum == null) {
            throw new RuntimeException("序列化协议不存在");
        }
        Serializer serializer = SerializerFactory.getInstance(serializerEnum.getValue());
        byte[] bodyBytes = serializer.serialize(protocolMessage.getBody());
        // 写入 body 长度和数据
        buffer.appendInt(bodyBytes.length);
        buffer.appendBytes(bodyBytes);
        return buffer;
    }
}
  • 实现消息解码器

在 protocol 包下新建ProtocolMessageDecoder,核心流程是依次从 Buffer 缓冲区的指定位置读取字段,构造出完整的消息对象。 

package com.yupi.yurpc.protocol;

import com.yupi.yurpc.model.RpcRequest;
import com.yupi.yurpc.model.RpcResponse;
import com.yupi.yurpc.serializer.Serializer;
import com.yupi.yurpc.serializer.SerializerFactory;
import io.vertx.core.buffer.Buffer;

import java.io.IOException;

/**
 * 协议消息解码器
 *
 * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
 * @from <a href="https://yupi.icu">编程导航学习圈</a>
 * @learn <a href="https://codefather.cn">鱼皮的编程宝典</a>
 */
public class ProtocolMessageDecoder {

    /**
     * 解码
     *
     * @param buffer
     * @return
     * @throws IOException
     */
    public static ProtocolMessage<?> decode(Buffer buffer) throws IOException {
        // 分别从指定位置读出 Buffer
        ProtocolMessage.Header header = new ProtocolMessage.Header();
        byte magic = buffer.getByte(0);
        // 校验魔数
        if (magic != ProtocolConstant.PROTOCOL_MAGIC) {
            throw new RuntimeException("消息 magic 非法");
        }
        header.setMagic(magic);
        header.setVersion(buffer.getByte(1));
        header.setSerializer(buffer.getByte(2));
        header.setType(buffer.getByte(3));
        header.setStatus(buffer.getByte(4));
        header.setRequestId(buffer.getLong(5));
        header.setBodyLength(buffer.getInt(13));
        // 解决粘包问题,只读指定长度的数据
        byte[] bodyBytes = buffer.getBytes(17, 17 + header.getBodyLength());
        // 解析消息体
        ProtocolMessageSerializerEnum serializerEnum = ProtocolMessageSerializerEnum.getEnumByKey(header.getSerializer());
        if (serializerEnum == null) {
            throw new RuntimeException("序列化消息的协议不存在");
        }
        Serializer serializer = SerializerFactory.getInstance(serializerEnum.getValue());
        ProtocolMessageTypeEnum messageTypeEnum = ProtocolMessageTypeEnum.getEnumByKey(header.getType());
        if (messageTypeEnum == null) {
            throw new RuntimeException("序列化消息的类型不存在");
        }
        switch (messageTypeEnum) {
            case REQUEST:
                RpcRequest request = serializer.deserialize(bodyBytes, RpcRequest.class);
                return new ProtocolMessage<>(header, request);
            case RESPONSE:
                RpcResponse response = serializer.deserialize(bodyBytes, RpcResponse.class);
                return new ProtocolMessage<>(header, response);
            case HEART_BEAT:
            case OTHERS:
            default:
                throw new RuntimeException("暂不支持该消息类型");
        }
    }

}
  • 测试

编写单元测试类,先编码再解码,以测试编码器和解码器的正确性。

package com.hpq;

import cn.hutool.core.util.IdUtil;
import com.hpq.constant.RpcConstant;
import com.hpq.model.RpcRequest;
import com.hpq.protocol.*;
import io.vertx.core.buffer.Buffer;
import org.junit.Assert;
import org.junit.Test;

import java.io.IOException;

public class ProtocolMessageTest {

    @Test
    public void testEncodeAndDecode() throws IOException {
        // 构造消息
        ProtocolMessage<RpcRequest> protocolMessage = new ProtocolMessage<>();
        ProtocolMessage.Header header = new ProtocolMessage.Header();
        header.setMagic(ProtocolConstant.PROTOCOL_MAGIC);
        header.setVersion(ProtocolConstant.PROTOCOL_VERSION);
        header.setSerializer((byte) ProtocolMessageSerializerEnum.JDK.getKey());
        header.setType((byte) ProtocolMessageTypeEnum.REQUEST.getKey());
        header.setStatus((byte) ProtocolMessageStatusEnum.OK.getValue());
        header.setRequestId(IdUtil.getSnowflakeNextId());
        header.setBodyLength(0);
        RpcRequest rpcRequest = new RpcRequest();
        rpcRequest.setInterfaceName("myService");
        rpcRequest.setMethodName("myMethod");
        rpcRequest.setServiceVersion(RpcConstant.DEFAULT_SERVICE_VERSION);
        rpcRequest.setParameterTypes(new Class[]{String.class});
        rpcRequest.setParameters(new Object[]{"aaa", "bbb"});
        protocolMessage.setHeader(header);
        protocolMessage.setBody(rpcRequest);

        Buffer encodeBuffer = ProtocolMessageEncoder.encode(protocolMessage);
        ProtocolMessage<?> message = ProtocolMessageDecoder.decode(encodeBuffer);
        Assert.assertNotNull(message);
    }

}
 5) .请求处理器(服务提供者)

请求处理器的作用是接受请求,然后通过反射调用服务实现类。

类似之前的 HttpServerHandler,我们需要开发一个 TcpServerHandler,用于处理请求。和 HttpServerHandler 的区别只是在获取请求、写入响应的方式上,需要调用上面开发好的编码器和解码器。

通过实现 Vert.x 提供的 Handler<NetSocket> 接口,可以定义 TCP 请求处理器。

package com.hpq.server.tcp;

import com.hpq.model.RpcRequest;
import com.hpq.model.RpcResponse;
import com.hpq.protocol.ProtocolMessage;
import com.hpq.protocol.ProtocolMessageDecoder;
import com.hpq.protocol.ProtocolMessageEncoder;
import com.hpq.protocol.ProtocolMessageTypeEnum;
import com.hpq.register.LocalRegister;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetSocket;

import java.io.IOException;
import java.lang.reflect.Method;

/**
 * Tcp请求处理器
 */
public class TcpServerHandler implements Handler<NetSocket> {
    @Override
    public void handle(NetSocket netSocket) {
        //处理请求
        netSocket.handler(buffer -> {
            //接受请求,解码
            ProtocolMessage<RpcRequest> protocolMessage;
            try{
                protocolMessage = (ProtocolMessage<RpcRequest>) ProtocolMessageDecoder.decode(buffer);
            } catch (IOException e) {
                throw new RuntimeException("协议消息解码错误");
            }
            RpcRequest rpcRequest = protocolMessage.getBody();
            //处理请求,构造响应结果对象
            RpcResponse rpcResponse =new RpcResponse();
            try {
                // 获取要调用的服务实现类,通过反射调用
                Class<?> implClass = LocalRegister.get(rpcRequest.getInterfaceName());
                Method method = implClass.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes());
                Object result = method.invoke(implClass.newInstance(), rpcRequest.getParameters());
                // 封装返回结果
                rpcResponse.setData(result);
                rpcResponse.setDataType(method.getReturnType());
                rpcResponse.setMessage("ok");
            } catch (Exception e) {
                e.printStackTrace();
                rpcResponse.setMessage(e.getMessage());
                rpcResponse.setException(e);
            }
            //发送响应,编码
            ProtocolMessage.Header header = protocolMessage.getHeader();
            header.setType((byte) ProtocolMessageTypeEnum.RESPONSE.getKey());
            ProtocolMessage<RpcResponse> responseProtocolMessage = new ProtocolMessage<>(header, rpcResponse);
            try{
                Buffer encode = ProtocolMessageEncoder.encode(responseProtocolMessage);
                netSocket.write(encode);
            } catch (IOException e) {
                throw new RuntimeException("协议消息编码错误");
            }

        });
    }
}
6) .请求发送(服务消费端)

调整服务消费者发送请求的代码,改 HTTP 请求为 TCP 请求。代码如下:

package com.hpq.proxy;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.hpq.RpcApplication;
import com.hpq.config.RpcConfig;
import com.hpq.constant.RpcConstant;
import com.hpq.model.RpcRequest;
import com.hpq.model.RpcResponse;
import com.hpq.model.ServiceMetaInfo;
import com.hpq.protocol.*;
import com.hpq.register.Registry;
import com.hpq.register.RegistryFactory;
import com.hpq.serializer.Serializer;
import com.hpq.serializer.SerializerFactory;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetSocket;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.CompletableFuture;

/**
 * jdk动态代理
 */
public class ServiceProxy implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 指定序列化器
        final Serializer serializer = SerializerFactory.getInstance(RpcApplication.getRpcConfig().getSerializer());
        String serviceName = method.getDeclaringClass().getName();
        // 构造请求
        RpcRequest rpcRequest = RpcRequest.builder()
                .interfaceName(method.getDeclaringClass().getName())
                .methodName(method.getName())
                .parameterTypes(method.getParameterTypes())
                .parameters(args)
                .build();
        try {
            // 序列化
            byte[] bodyBytes = serializer.serialize(rpcRequest);

            RpcConfig rpcConfig = RpcApplication.getRpcConfig();
            Registry registry = RegistryFactory.getInstance(rpcConfig.getRegisterConfig().getRegistry());
            ServiceMetaInfo metaInfo = new ServiceMetaInfo();
            metaInfo.setServiceName(serviceName);
            metaInfo.setServiceVersion(RpcConstant.DEFAULT_SERVICE_VERSION);
            List<ServiceMetaInfo> serviceMetaInfoList = registry.serviceDiscovery(metaInfo.getServiceKey());
            if (CollUtil.isEmpty(serviceMetaInfoList)) {
                throw new RuntimeException("未发现服务");
            }
            ServiceMetaInfo serviceMetaInfo = serviceMetaInfoList.get(0);
            // 发送TCP请求
            Vertx vertx = Vertx.vertx();
            NetClient client = vertx.createNetClient();
            // 异步回调
            CompletableFuture<RpcResponse> responseFuture = new CompletableFuture<>();
            // 连接
            client.connect(serviceMetaInfo.getServicePort(), serviceMetaInfo.getServiceHost(), result -> {
                if (result.succeeded()) {
                    System.out.println("TCP server连接成功");
                    NetSocket socket = result.result();
                    ProtocolMessage<RpcRequest> protocolMessage = new ProtocolMessage<>();
                    ProtocolMessage.Header header = protocolMessage.getHeader();
                    header.setMagic(ProtocolConstant.PROTOCOL_MAGIC);
                    header.setVersion(ProtocolConstant.PROTOCOL_VERSION);
                    header.setSerializer(ProtocolMessageSerializerEnum.getByValue(RpcApplication.getRpcConfig().getSerializer()));
                    header.setType((byte) ProtocolMessageTypeEnum.REQUEST.getKey());
                    header.setRequestId(IdUtil.getSnowflakeNextId());
                    header.setBodyLength(bodyBytes.length);
                    protocolMessage.setHeader(header);
                    protocolMessage.setBody(rpcRequest);
                    //编码请求
                    try {
                        Buffer encoderBuffer = ProtocolMessageEncoder.encode(protocolMessage);
                        socket.write(encoderBuffer);
                    } catch (IOException e) {
                        throw new RuntimeException("协议消息编码错误");
                    }
                    //接受响应
                    socket.handler(buffer -> {
                        try {
                            ProtocolMessage<RpcResponse> response = (ProtocolMessage<RpcResponse>) ProtocolMessageDecoder.decode(buffer);
                        } catch (IOException e) {
                            throw new RuntimeException("协议消息解码错误");
                        }
                    });
                } else {
                    System.out.println("TCP server连接失败");
                }
            });
            RpcResponse rpcResponse = responseFuture.get();
            //记得关闭连接
            client.close();
            return rpcResponse.getData();
            } 
        catch (IOException e) {
            e.printStackTrace();
            }
         return null;
    }
}

 上述代码主要完成了TCP发送请求部分,由于 Vert.x 提供的请求处理器是异步、反应式的,我们为了更方便地获取结果,可以使用 CompletableFuture 转异步为同步,参考代码如下:

CompletableFuture<RpcResponse> responseFuture = new CompletableFuture<>();
netClient.connect(xxx,
    result -> {
        // 完成了响应
        responseFuture.complete(rpcResponseProtocolMessage.getBody());
    });
);
// 阻塞,直到响应完成,才会继续向下执行
RpcResponse rpcResponse = responseFuture.get();

 编写好上述代码后,我们就可以先测试请求响应流程是否跑通了。

修改服务提供者 ProviderExample 代码,改为启动 TCP 服务器。完整代码如下:

 8.粘包半包问题解决
1) .什么是粘包和半包

使用 TCP 协议网络通讯时,由于TCP协议并不保留消息边界,可能会出现半包和粘包问题。

我举个例子大家就明白了。

理想情况下,假如我们客户端 连续 2 次 要发送的消息是:

// 第一次
Hello, server!Hello, server!Hello, server!Hello, server!
// 第二次
Hello, server!Hello, server!Hello, server!Hello, server!

但服务端收到的消息情况可能是: 

  • 每次收到的数据更少了,这种情况叫做半包
// 第一次
Hello, server!Hello, server!
// 第二次
Hello, server!Hello, server!Hello, server!
  • 每次收到的数据更多了,这种情况叫做粘包
// 第三次
Hello, server!Hello, server!Hello, server!Hello, server!Hello, server!
2) .粘包半包问题演示
  •  修改 TCP 客户端代码,连续发送 1000 次消息
package com.hpq.server.tcp;

import io.vertx.core.Vertx;
import io.vertx.core.net.NetSocket;

/**
 * Tcp客户端
 */
public class VertxTcpClient {
    public void start(){
        //创建vert.x实例
        Vertx vertx = Vertx.vertx();
        //创建TCP客户端
        vertx.createNetClient().connect(8888,"localhost",result -> {
            if(result.succeeded()){
                //连接成功
                System.out.println("连接成功");
                NetSocket socket = result.result();
                for (int i = 0; i < 1000; i++) {
                    socket.write("hello server!, hello server!, hello server! ");
                }
                socket.handler(data -> {
                    System.out.println("收到服务器消息:" + data.toString());
                });

            }else{
                //连接失败
                System.out.println("连接失败");
            }
        });
    }

    public static void main(String[] args) {
        VertxTcpClient client = new VertxTcpClient();
        client.start();
    }
}
  •  修改 TCP 服务端代码,打印出每次收到的消息:
package com.hpq.server.tcp;

import com.hpq.server.HttpServer;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetServer;


import static io.vertx.core.http.impl.HttpClientConnection.log;


@Slf4j
public class VertxTcpServer implements HttpServer {

    @Override
    public void doStart(int port) {
        // 创建 Vert.x 实例
        Vertx vertx = Vertx.vertx();

        // 创建 TCP 服务器
        NetServer server = vertx.createNetServer();

        // 处理请求
//        server.connectHandler(new TcpServerHandler());
        server.connectHandler(socket -> {
            socket.handler(buffer -> {
                String testMessage = "Hello, server!Hello, server!Hello, server!Hello, server!";
                int messageLength = testMessage.getBytes().length;
                if (buffer.getBytes().length < messageLength) {
                    System.out.println("半包, length = " + buffer.getBytes().length);
                    return;
                }
                if (buffer.getBytes().length > messageLength) {
                    System.out.println("粘包, length = " + buffer.getBytes().length);
                    return;
                }
                String str = new String(buffer.getBytes(0, messageLength));
                System.out.println(str);
                if (testMessage.equals(str)) {
                    System.out.println("good");
                }
            });
        });

        // 启动 TCP 服务器并监听指定端口
        server.listen(port, result -> {
            if (result.succeeded()) {
                log.info("TCP server started on port " + port);
            } else {
                log.info("Failed to start TCP server: " + result.cause());
            }
        });
    }

    public static void main(String[] args) {
        new VertxTcpServer().doStart(8888);
    }
}

  • 测试运行,查看服务端控制台,发现服务端接受消息时,出现了半包和粘包:
 3) .如何解决粘包和半包问题

        解决半包的核心思路是:在消息头中设置请求体的长度,服务端接收时,判断每次消息的长度是否符合预期,不完整就不读,留到下一次接收到消息时再读取。

        解决粘包的核心思路也是类似的:每次只读取指定长度的数据,超过长度的留着下一次接收到消息时再读取。

但这两种方法自己实现起来还是比较麻烦的,要记录每次接收到的消息位置,维护字节数组缓存,

因此在这里我们选择用Vert.x框架中内置的RecordParser完美解决粘包和半包问题,它的作用是:保证下次读取到 特定长度 的字符。

基础代码

1)先小试牛刀,使用RecordParser来读取固定长度的消息,示例代码如下:

package com.hpq.server.tcp;

import com.hpq.protocol.ProtocolConstant;
import com.hpq.server.HttpServer;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetServer;
import io.vertx.core.parsetools.RecordParser;

import static io.vertx.core.http.impl.HttpClientConnection.log;

public class VertxTcpServer implements HttpServer {
    private byte[] handleRequest(byte[] requestData){
        return "Hello,client!".getBytes();
    }
    @Override
    public void doStart(int port) {
        //创建vert.x实例
        Vertx vertx = Vertx.vertx();
        //创建TCP服务器
        NetServer server = vertx.createNetServer();
        //处理请求
        server.connectHandler(socket -> {
            //读取客户端发送的数据

                String testMessage = "hello server!, hello server!, hello server! ";
                int messageLength = testMessage.getBytes().length;
                RecordParser parser = RecordParser.newFixed(messageLength);
                parser.setOutput(buffer -> {
                    String str = new String(buffer.getBytes());
                    System.out.println(str);
                    if (testMessage.equals(str)) {
                        System.out.println("good");
                    }

                });
                socket.handler(parser);


            });
        //启动服务器,并监听端口
        server.listen(port, result ->{
            if (result.succeeded()) {
                log.info("TCP server started on port " + port);
            } else {
                log.info("Failed to start TCP server: " + result.cause());
            }
        });
    }

    public static void main(String[] args) {
        VertxTcpServer server = new VertxTcpServer();
        server.doStart(8888);
    }
}

上述代码的核心是 RecordParser.newFixed(messageLength),为 Parser 指定每次读取固定值长度的内容。

重新测试可以发现,这次的输出结果非常整齐,解决了半包和粘包。

2) .实际运用中,消息体的长度是不固定的,所以要将RecordParser改为变长来解决。

那我们的思路可以是,将读取完整的消息拆分为 2 次:

  • 先完整读取请求头信息,由于请求头信息长度是固定的,可以使用 RecordParser 保证每次都完整读取。
  • 再根据请求头长度信息更改 RecordParser 的固定长度,保证完整获取到请求体。

修改测试 TCP Server 代码如下:

 // 构造 parser
            RecordParser parser = RecordParser.newFixed(8);
            parser.setOutput(new Handler<Buffer>() {
                // 初始化
                int size = -1;
                // 一次完整的读取(头 + 体)
                Buffer resultBuffer = Buffer.buffer();

                @Override
                public void handle(Buffer buffer) {
                    if (-1 == size) {
                        // 读取消息体长度
                        size = buffer.getInt(4);
                        parser.fixedSizeMode(size);
                        // 写入头信息到结果
                        resultBuffer.appendBuffer(buffer);
                    } else {
                        // 写入体信息到结果
                        resultBuffer.appendBuffer(buffer);
                        System.out.println(resultBuffer.toString());
                        // 重置一轮
                        parser.fixedSizeMode(8);
                        size = -1;
                        resultBuffer = Buffer.buffer();
                    }
                }

修改测试 TCP client 代码如下,自己构造了一个变长、长度信息不在 Buffer 最开头(而是有一定偏移量)的消息 

package com.hpq.server.tcp;

import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetSocket;

/**
 * Tcp客户端
 */
public class VertxTcpClient {
    public void start(){
        //创建vert.x实例
        Vertx vertx = Vertx.vertx();
        //创建TCP客户端
        vertx.createNetClient().connect(8888,"localhost",result -> {
            if (result.succeeded()) {
                System.out.println("Connected to TCP server");
                io.vertx.core.net.NetSocket socket = result.result();
                for (int i = 0; i < 1000; i++) {
                    // 发送数据
                    Buffer buffer = Buffer.buffer();
                    String str = "Hello, server!Hello, server!Hello, server!Hello, server!";
                    buffer.appendInt(0);
                    buffer.appendInt(str.getBytes().length);
                    buffer.appendBytes(str.getBytes());
                    socket.write(buffer);
                }
                socket.handler(data -> {
                    System.out.println("收到服务器消息:" + data.toString());
                });

            }else{
                //连接失败
                System.out.println("连接失败");
            }
        });
    }

    public static void main(String[] args) {
        VertxTcpClient client = new VertxTcpClient();
        client.start();
    }
}

 测试结果应该也是能够正常读取到消息的,不会出现半包和粘包。

4) .封装半包粘包处理器

于 ServiceProxy(消费者)和请求 Handler(提供者)都需要接受 Buffer,所以都需要半包粘包问题处理。在这里需要对代码进行封装复用了,我们可以使用设计模式中的 装饰者模式,使用 RecordParser 对原有的 Buffer 处理器的能力进行增强。装饰者模式可以简单理解为给对象穿装备,增强对象的能力。

package com.hpq.server.tcp;

import com.hpq.protocol.ProtocolConstant;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.parsetools.RecordParser;

/**
 * 装饰者模式,使用RecordParser对原有buffer进行增强
 */
public class TcpBufferHandlerWrapper implements Handler<Buffer> {

    /**
     * 解析器,用于解决半包、粘包问题
     */
    private final RecordParser recordParser;

    public TcpBufferHandlerWrapper(Handler<Buffer> bufferHandler) {
        recordParser = initRecordParser(bufferHandler);
    }

    @Override
    public void handle(Buffer buffer) {
        recordParser.handle(buffer);
    }

    /**
     * 初始化解析器
     *
     * @param bufferHandler
     * @return
     */
    private RecordParser initRecordParser(Handler<Buffer> bufferHandler) {
        // 构造 parser
        RecordParser parser = RecordParser.newFixed(ProtocolConstant.MESSAGE_HEADER_LENGTH);

        parser.setOutput(new Handler<Buffer>() {
            // 初始化
            int size = -1;
            // 一次完整的读取(头 + 体)
            Buffer resultBuffer = Buffer.buffer();

            @Override
            public void handle(Buffer buffer) {
                // 1. 每次循环,首先读取消息头
                if (-1 == size) {
                    // 读取消息体长度
                    size = buffer.getInt(13);
                    parser.fixedSizeMode(size);
                    // 写入头信息到结果
                    resultBuffer.appendBuffer(buffer);
                } else {
                    // 2. 然后读取消息体
                    // 写入体信息到结果
                    resultBuffer.appendBuffer(buffer);
                    // 已拼接为完整 Buffer,执行处理
                    bufferHandler.handle(resultBuffer);
                    // 重置一轮
                    parser.fixedSizeMode(ProtocolConstant.MESSAGE_HEADER_LENGTH);
                    size = -1;
                    resultBuffer = Buffer.buffer();
                }
            }
        });

        return parser;
    }
}
5) .优化客户端代码
  •  修改 TCP 请求处理器。

使用 TcpBufferHandlerWrapper 来封装之前处理请求的代码,请求逻辑不用变,需要修改的部分代码如下:

/**
 * Tcp请求处理器
 */
public class TcpServerHandler implements Handler<NetSocket> {
    @Override
    public void handle(NetSocket netSocket) {
        
        TcpBufferHandlerWrapper tcpBufferHandlerWrapper = new TcpBufferHandlerWrapper(buffer -> {
            //处理请求

        });
        netSocket.handler(tcpBufferHandlerWrapper);

    }
}

其实就是使用一个 Wrapper 对象 包装 了之前的代码,就解决了半包粘包。 

  •  修改客户端处理响应的代码。

之前我们是把所有发送请求、处理响应的代码都写到了 ServiceProxy 中,使得这个类的代码 “臃肿不堪”。

我们干脆做个优化,把所有的请求响应逻辑提取出来,封装为单独的 VertxTcpClient 类,放在 server.tcp 包下。

VertxTcpClient 的完整代码如下:

package com.hpq.server.tcp;

import cn.hutool.core.util.IdUtil;
import com.hpq.RpcApplication;
import com.hpq.model.RpcRequest;
import com.hpq.model.RpcResponse;
import com.hpq.model.ServiceMetaInfo;
import com.hpq.protocol.*;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetSocket;

import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/**
 * Tcp客户端
 */
public class VertxTcpClient {
    public static RpcResponse doRequest(RpcRequest rpcRequest, ServiceMetaInfo serviceMetaInfo) throws ExecutionException, InterruptedException {
        // 发送 TCP 请求
        Vertx vertx = Vertx.vertx();
        NetClient netClient = vertx.createNetClient();
        CompletableFuture<RpcResponse> responseFuture = new CompletableFuture<>();
        netClient.connect(serviceMetaInfo.getServicePort(), serviceMetaInfo.getServiceHost(),
                result -> {
                    if (!result.succeeded()) {
                        System.out.println("Failed to connect to TCP server");
                        return ;
                    }
                        System.out.println("Connected to TCP server");
                        io.vertx.core.net.NetSocket socket = result.result();
                        // 发送数据
                        // 构造消息
                        ProtocolMessage<RpcRequest> protocolMessage = new ProtocolMessage<>();
                        ProtocolMessage.Header header = new ProtocolMessage.Header();
                        header.setMagic(ProtocolConstant.PROTOCOL_MAGIC);
                        header.setVersion(ProtocolConstant.PROTOCOL_VERSION);
                        header.setSerializer((byte) ProtocolMessageSerializerEnum.getByValue(RpcApplication.getRpcConfig().getSerializer()).getKey());
                        header.setType((byte) ProtocolMessageTypeEnum.REQUEST.getKey());
                        header.setRequestId(IdUtil.getSnowflakeNextId());
                        protocolMessage.setHeader(header);
                        protocolMessage.setBody(rpcRequest);
                        // 编码请求
                        try {
                            Buffer encodeBuffer = ProtocolMessageEncoder.encode(protocolMessage);
                            socket.write(encodeBuffer);
                        } catch (IOException e) {
                            throw new RuntimeException("协议消息编码错误");
                        }

                        // 接收响应
                        TcpBufferHandlerWrapper tcpBufferHandlerWrapper = new TcpBufferHandlerWrapper(buffer -> {
                            try {
                                ProtocolMessage<RpcResponse> rpcResponseProtocolMessage = (ProtocolMessage<RpcResponse>) ProtocolMessageDecoder.decode(buffer);
                                responseFuture.complete(rpcResponseProtocolMessage.getBody());
                            } catch (IOException e) {
                                throw new RuntimeException("协议消息解码错误");
                            }
                        });
                        socket.handler(tcpBufferHandlerWrapper);
                });
        RpcResponse rpcResponse = responseFuture.get();
        // 记得关闭连接
        netClient.close();
        return rpcResponse;

    }
}
  •  修改 ServiceProxy 代码,调用 VertxTcpClient,修改后的代码如下:
package com.hpq.proxy;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.hpq.RpcApplication;
import com.hpq.config.RpcConfig;
import com.hpq.constant.RpcConstant;
import com.hpq.model.RpcRequest;
import com.hpq.model.RpcResponse;
import com.hpq.model.ServiceMetaInfo;
import com.hpq.protocol.*;
import com.hpq.register.Registry;
import com.hpq.register.RegistryFactory;
import com.hpq.serializer.Serializer;
import com.hpq.serializer.SerializerFactory;
import com.hpq.server.tcp.VertxTcpClient;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetSocket;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.CompletableFuture;

/**
 * jdk动态代理
 */
public class ServiceProxy implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 指定序列化器
        final Serializer serializer = SerializerFactory.getInstance(RpcApplication.getRpcConfig().getSerializer());
        String serviceName = method.getDeclaringClass().getName();
        // 构造请求
        RpcRequest rpcRequest = RpcRequest.builder()
                .interfaceName(method.getDeclaringClass().getName())
                .methodName(method.getName())
                .parameterTypes(method.getParameterTypes())
                .parameters(args)
                .build();
        try {
            // 序列化
            byte[] bodyBytes = serializer.serialize(rpcRequest);

            RpcConfig rpcConfig = RpcApplication.getRpcConfig();
            Registry registry = RegistryFactory.getInstance(rpcConfig.getRegisterConfig().getRegistry());
            ServiceMetaInfo metaInfo = new ServiceMetaInfo();
            metaInfo.setServiceName(serviceName);
            metaInfo.setServiceVersion(RpcConstant.DEFAULT_SERVICE_VERSION);
            List<ServiceMetaInfo> serviceMetaInfoList = registry.serviceDiscovery(metaInfo.getServiceKey());
            if (CollUtil.isEmpty(serviceMetaInfoList)) {
                throw new RuntimeException("未发现服务");
            }
            ServiceMetaInfo serviceMetaInfo = serviceMetaInfoList.get(0);
            //发送TCP请求
            RpcResponse rpcResponse = VertxTcpClient.doRequest(rpcRequest, serviceMetaInfo);
            return rpcResponse.getData();

        } catch (IOException e) {
           throw new RuntimeException("调用失败");
        }
    }
}
9.负载均衡
1) .什么是负载均衡?

就好比你去一个很热闹的餐厅吃饭。餐厅有很多服务员,顾客(请求)不断进来。如果没有负载均衡,那可能所有顾客都冲着一个服务员去,这个服务员忙得不可开交,而其他服务员却很闲。这样顾客等待时间就会很长,服务质量也不好。

有了负载均衡呢,就相当于有一个聪明的经理(负载均衡器)。这个经理会观察每个服务员的忙碌程度,然后把新进来的顾客合理地分配给不那么忙的服务员。这样每个服务员都能分担一部分工作,大家都不会太累,顾客也能更快地得到服务。

简单来说,负载均衡就是把工作任务均匀地分配给多个资源,让它们一起干活,提高效率,避免有的忙死有的闲死。

同样将这一机制引入到RPC框架,我们完全可以从服务提供者节点中,选择一个服务提供者发起请求,而不是每次都请求同一个服务提供者,从而达到充分利用资源的目的。

常用的负载均衡实现技术有 Nginx(七层负载均衡)、LVS(四层负载均衡)等

2) . 常见的负载均衡

不同的负载均衡算法,适用的场景也不同,一定要根据实际情况选取,主流的负载均衡算法如下:

  • 轮询(Round Robin):按照循环的顺序将请求分配给每个服务器,适用于各服务器性能相近的情况。
  • ​​随机(Random):随机选择一个服务器来处理请求,适用于服务器性能相近且负载均匀的情况。
  • 加权轮询(Weighted Round Robin):根据服务器的性能或权重分配请求,性能更好的服务器会获得更多的请求,适用于服务器性能不均的情况。
  • 加权随机(Weighted Random):根据服务器的权重随机选择一个服务器处理请求,适用于服务器性能不均的情况。
  • 最小连接数(Least Connections):选择当前连接数最少的服务器来处理请求,适用于长连接场景。
  • IP Hash:根据客户端 IP 地址的哈希值选择服务器处理请求,确保同一客户端的请求始终被分配到同一台服务器上,适用于需要保持会话一致性的场景。

当然,也可以根据请求中的其他参数进行 Hash,比如根据请求接口的地址路由到不同的服务器节点。

下面,再给大家分享一个很重要的分布式知识点:一致性 Hash。

一致性 Hash

一致性哈希(Consistent Hashing)是一种经典的哈希算法,用于将请求分配到多个节点或服务器上,所以非常适用于负载均衡。

它的核心思想是将整个哈希值空间划分成一个环状结构,每个节点或服务器在环上占据一个位置,每个请求根据其哈希值映射到环上的一个点,然后顺时针寻找第一个大于或等于该哈希值的节点,将请求路由到该节点上。

除此之外,一致性哈希还解决了 节点下线 和 倾斜问题

了解了负载均衡算法后,我们来开发实现。

3) .多种负载均衡器的实现

在学习负载均衡算法时我们可以参考Nginx的负载均衡算法的实现,此处我们只实现轮询,随机,一致性Hash三种常用的负载均衡算法

在 RPC 项目中新建 loadbalancer 包,将所有负载均衡器相关的代码放到该包下。

  • 先编写负载均衡器通用接口。

提供一个选择服务方法,接受请求参数和可用服务列表,可以根据这些信息进行选择。代码如下:

package com.hpq.loadbalancer;

import com.hpq.model.ServiceMetaInfo;
import java.util.List;
import java.util.Map;

/**
 * 负载均衡器(消费者使用)
 */
public interface LoadBalancer {
    /**
     * 选择服务调用
     * @param params
     * @param serviceMetaInfoList
     * @return
     */
    ServiceMetaInfo select(Map<String, Object> params, List<ServiceMetaInfo> serviceMetaInfoList);
}
  • 轮询负载均衡器。

使用 JUC 包的 AtomicInteger 实现原子计数器,防止并发冲突问题。

package com.hpq.loadbalancer;

import com.hpq.model.ServiceMetaInfo;

import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 轮询负载均衡器
 */
public class RoundRobinLoadBalancer implements LoadBalancer{
    //当前轮询下标
    private final AtomicInteger  currentIndex = new AtomicInteger(0);
    @Override
    public ServiceMetaInfo select(Map<String, Object> params, List<ServiceMetaInfo> serviceMetaInfoList) {
       if (serviceMetaInfoList.isEmpty()){
           return null;
       }
        int size = serviceMetaInfoList.size();
       if (size == 1){
           return serviceMetaInfoList.get(0);
       }
       //取模算法轮询
        int index = currentIndex.getAndIncrement() % size;
       return serviceMetaInfoList.get(index);
    }
}
  • 随机负载均衡器。

使用 Java 自带的 Random 类实现随机选取即可,代码如下:

package com.hpq.loadbalancer;

import com.hpq.model.ServiceMetaInfo;

import java.util.List;
import java.util.Map;
import java.util.Random;

/**
 * 随机负载均衡器
 */
public class RandomLoadBalancer implements LoadBalancer{
    private final Random  random = new Random();

    @Override
    public ServiceMetaInfo select(Map<String, Object> params, List<ServiceMetaInfo> serviceMetaInfoList) {
       int size = serviceMetaInfoList.size();
       if (size == 0){
           return null;
       }
       //只有一个服务,不用随机
       if (size == 1){
           return serviceMetaInfoList.get(0);
       }
       return serviceMetaInfoList.get(random.nextInt(size));
    }
}
  • 实现一致性 Hash 负载均衡器。

可以使用 TreeMap 实现一致性 Hash 环,该数据结构提供了 ceilingEntry 和 firstEntry 两个方法,便于获取符合算法要求的节点。

package com.hpq.loadbalancer;

import com.hpq.model.ServiceMetaInfo;

import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class ConsistentHashLoadBalancer implements LoadBalancer{
    //一致性哈希环, 存放虚拟节点
    private final TreeMap<Integer, ServiceMetaInfo> virtualNodes = new TreeMap<>();
    //虚拟节点个数
    private static final int VIRTUAL_NODE_SIZE = 100;
    @Override
    public ServiceMetaInfo select(Map<String, Object> requestParams, List<ServiceMetaInfo> serviceMetaInfoList) {
        if(serviceMetaInfoList.isEmpty()){
            return null;
        }
        //构建虚拟节点环
        for(ServiceMetaInfo serviceMetaInfo: serviceMetaInfoList){
            for(int i=0;i<VIRTUAL_NODE_SIZE;i++){
                int hash = getHash(serviceMetaInfo.getServiceNodeKey() +'#'+ i);
                virtualNodes.put(hash, serviceMetaInfo);
            }
        }
        //获取调用请求的Hash值
        int hash = getHash(requestParams);
        //选择最接近且大于等于调用请求Hash值的虚拟节点
        Map.Entry<Integer, ServiceMetaInfo> entry = virtualNodes.ceilingEntry(hash);
        if(entry == null){
            // 如果没有大于等于调用请求 hash 值的虚拟节点,则返回环首部的节点
            entry = virtualNodes.firstEntry();
        }
            return entry.getValue();
    }
    //获取Hash值
    private int getHash(Object  key) {
        return key.hashCode();
    }
}

上述代码每次调用负载均衡器时,都会重新构造 Hash 环,这是为了能够即时处理节点的变化。

5) .支持配置和拓展

        对于负载均衡器,我们同样也像序列化器,注册中心一样支持多个,让开发者能够填写配置来指定使用的负载均衡器,并且支持自定义负载均衡器,让框架更易用、更利于扩展。同样都可以使用工厂创建对象、使用 SPI 动态加载自定义的注册中心。

  • 负载均衡器常量

在 loadbalancer 包下新建 LoadBalancerKeys 类,列举所有支持的负载均衡器键名。

代码如下:

package com.hpq.loadbalancer;

/**
 * 负载均衡键名常量
 */
public interface LoadBalancerKeys {
    //轮询
    String ROUND_ROBIN = "roundRobin";
    //随机
    String RANDOM = "random";
    //一致性哈希
    String CONSISTENT_HASH = "consistentHash";

}
  • 使用工厂模式,支持根据 key 从 SPI 获取负载均衡器对象实例

在 loadbalancer 包下新建 LoadBalancerFactory 类,代码如下:

package com.hpq.loadbalancer;

import com.hpq.spi.SpiLoader;

public class LoadBalancerFactory {
    static{
        SpiLoader.load(LoadBalancer.class);
    }
    //默认负载均衡算法
    private static final LoadBalancer DEFAULT_LOAD_BALANCER = new RoundRobinLoadBalancer();
    //获取实例对象
    public static LoadBalancer getInstance(String key) {
            return SpiLoader.getInstance(LoadBalancer.class, key);
    }
}
  • 在 META-INF 的 rpc/system 目录下编写负载均衡器接口的 SPI 配置文件

代码如下:

roundRobin=com.hpq.loadbalancer.RoundRobinLoadBalancer
random=com.hpq.loadbalancer.RandomLoadBalancer
consistentHash=com.hpq.loadbalancer.ConsistentHashLoadBalancer
  • 为 RpcConfig 全局配置新增负载均衡器的配置
package com.hpq.config;

import com.hpq.loadbalancer.LoadBalancerKeys;
import com.hpq.serializer.SerializerKeys;
import lombok.Data;

/**
 * Rpck框架全局配置
 *
 */
@Data
public class RpcConfig {
    .....
    
    //负载均衡
    private String loadBalance = LoadBalancerKeys.ROUND_ROBIN;

}

6) .应用负载均衡器

完成上面的代码后我们就可以开始使用负载均衡器了,修改 ServiceProxy 的代码,将 “固定调用第一个服务节点” 改为 “调用负载均衡器获取一个服务节点”。代码如下:

上述代码中,我们给负载均衡器传入了一个 requestParams HashMap,并且将请求方法名作为参数放到了 Map 中。如果使用的是一致性 Hash 算法,那么会根据 requestParams 计算 Hash 值,调用相同方法的请求 Hash 值肯定相同,所以总会请求到同一个服务器节点上。

10.重试策略

RPC框架在服务消费者调用接口时可能会失败,这时候就会报错,导致服务瘫痪,导致失败的原因有很多,可能是服务提供者返回了错误,但有时可能只是网络不稳定或服务提供者重启等临时性问题。这种情况下,我们可能更希望服务消费者拥有自动重试的能力,提高系统的可用性

重试机制

1).重试时间

主流的重试时间算法有:

  • 固定重试间隔:在每次重试之间使用固定的时间间隔
  • 指数退避重试:在每次失败后,重试的时间间隔会以指数级增加,以避免请求过于密集。
  • 随机延迟重试:在每次重试之间使用随机的时间间隔,以避免请求的同时发生。
  • 可变延迟重试:这种策略更 “高级” 了,根据先前重试的成功或失败情况,动态调整下一次重试的延迟时间。比如,根据前一次的响应时间调整下一次重试的等待时间

值得一提的是,以上的策略是可以组合使用的,一定要根据具体情况和需求灵活调整。比如可以先使用指数退避重试策略,如果连续多次重试失败,则切换到固定重试间隔策略。

2) .停止重试

一般来说,重试次数是有上限的,否则随着报错的增多,系统同时发生的重试也会越来越多,造成雪崩。

主流的停止重试策略有:

  • 最大尝试次数:一般重试当达到最大次数时不再重试。
  • 超时停止:重试达到最大时间的时候,停止重试。

3).重试工作

最后一点是重试后要做什么事情?一般来说就是重复执行原本要做的操作,比如发送请求失败了,那就再发一次请求。

需要注意的是,当重试次数超过上限时,往往还要进行其他的操作,比如:

  • 通知告警:让开发者人工介入
  • 降级容错:改为调用其他接口、或者执行其他操作
方案设计

 在RPC框架中,我们完全可以将 VertxTcpClient.doRequest 封装为一个可重试的任务,如果请求失败(重试条件),系统就会自动按照重试策略再次发起请求,不用开发者关心

对于重试算法,我们就选择主流的重试算法好了,Java 中可以使用 Guava-Retrying 库轻松实现多种不同的重试算法,非常简单,后文直接带大家实战。

和序列化器、注册中心、负载均衡器一样,重试策略本身也可以使用 SPI + 工厂的方式,允许开发者动态配置和扩展自己的重试策略。

最后,如果重试超过一定次数,我们就停止重试,并且抛出异常。在下节教程中,还会给大家分享重试失败后的另一种选择 —— 容错机制。

重试策略实现

 在 RPC 项目中新建 fault.retry 包,将所有重试相关的代码放到该包下。

1).先编写重试策略通用接口。提供一个重试方法,接受一个具体的任务参数,可以使用 Callable 类代表一个任务。

代码如下:

package com.hpq.fault.retry;

import com.hpq.model.RpcResponse;

import java.util.concurrent.Callable;

/**
 * 重试策略
 */
public interface RetryStrategy {
    /**
     * 重试
     * @param callable
     * @return
     */
    RpcResponse doRetry(Callable<RpcResponse> callable);
}

2).引入 Guava-Retrying 重试库,代码如下:

<!-- https://github.com/rholder/guava-retrying -->
<dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>

3) .不重试策略实现。

就是直接执行一次任务,代码如下:

package com.hpq.fault.retry;

import com.hpq.model.RpcResponse;
import java.util.concurrent.Callable;

/**
 * 不重试-重试策略
 */
public class NoRetryStrategy implements RetryStrategy{
    @Override
    public RpcResponse doRetry(Callable<RpcResponse> callable) throws Exception {
        return callable.call();
    }
}

 4) .固定重试间隔策略实现

使用 Guava-Retrying 提供的 RetryerBuilder 能够很方便地指定重试条件、重试等待策略、重试停止策略、重试工作等。

代码如下:

package com.hpq.fault.retry;

import com.github.rholder.retry.*;
import com.hpq.model.RpcResponse;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
 * 固定时间间隔
 */
@Slf4j
public class FixedIntervalRetryStrategy implements RetryStrategy{
    @Override
    public RpcResponse doRetry(Callable<RpcResponse> callable) throws ExecutionException, RetryException  {
        // 构建一个重试器,用于在指定条件下自动重试callable
        Retryer<RpcResponse> retryer = RetryerBuilder.<RpcResponse>newBuilder()
                // 设置重试条件为Exception类型,即只要执行过程中抛出Exception就尝试重试
                .retryIfExceptionOfType(Exception.class)
                // 设置等待策略为每次重试间隔3秒
                .withWaitStrategy(WaitStrategies.fixedWait(3L, TimeUnit.SECONDS))
                // 设置停止策略为最多尝试3次后停止重试
                .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                // 设置重试监听器,在每次重试前记录日志信息
                .withRetryListener(new RetryListener(){
                    @Override
                    public <V> void onRetry(Attempt<V> attempt) {
                        // 记录每次重试的日志,包括当前重试次数
                        log.info("第{}次重试", attempt.getAttemptNumber());
                    }
                })
                // 完成重试器的构建
                .build();
        // 使用构建好的重试器来执行callable,自动处理重试逻辑
        return retryer.call(callable);
    }
}
 支持配置和拓展

像序列化器、注册中心、负载均衡器一样,我们的需求是,让开发者能够填写配置来指定使用的重试策略,并且支持自定义重试策略,让框架更易用、更利于扩展。同样可以使用工厂创建对象、使用 SPI 动态加载自定义的注册中心。

 1) .重试策略常量

在 fault.retry 包下新建 RetryStrategyKeys 类,列举所有支持的重试策略键名。

代码如下:

package com.hpq.fault.retry;

/**
 * 重试策略常量
 */
public interface RetryStrategyKeys {
    /**
     * 不重试
     */
    String NO = "no";
    /**
     * 固定时间间隔
     */
    String FIXED_INTERVAL = "fixedInterval";


}

 2) .使用工厂模式,支持根据 key 从 SPI 获取重试策略对象实例。

在 fault.retry 包下新建 RetryStrategyFactory 类,代码如下:

package com.hpq.fault.retry;

import com.hpq.spi.SpiLoader;
/**
 * 重试策略工厂
 */
public class RetryStrategyFactory {
    static{
        SpiLoader.load(RetryStrategy.class);
    }
    //默认重试策略
    private static final RetryStrategy DEFAULT_RETRY_STRATEGY = new NoRetryStrategy();
    //根据Key获取实例
    public static RetryStrategy getInstance(String key){
        return SpiLoader.getInstance(RetryStrategy.class, key);
    }
}

3) .在 META-INF 的 rpc/system 目录下编写重试策略接口的 SPI 配置文件

代码如下: 

no=com.hpq.fault.retry.NoRetryStrategy
fixedInterval=com.hpq.fault.retry.FixedIntervalRetryStrategy

4)为 RpcConfig 全局配置新增重试策略的配置,代码如下:

package com.hpq.config;

import com.hpq.fault.retry.RetryStrategyKeys;
import lombok.Data;

/**
 * Rpc框架全局配置
 *
 */    
@Data
public class RpcConfig {
   
    //重试策略
    private String retryStrategy = RetryStrategyKeys.NO;
   
}

5) .应用重试机制

修改 ServiceProxy 的代码,从工厂中获取重试器,并且将请求代码封装为一个 Callable 接口,作为重试器的参数,调用重试器即可。

上述代码中,使用 Lambda 表达式将 VertxTcpClient.doRequest 封装为了一个匿名函数,简化了代码。

11.容错机制
设计方案

上节教程中,我们为了提升了服务消费端的可靠性和健壮性给 RPC 框架增加了重试机制,但如果重试超过了一定次数仍然失败,或者说当调用出现失败时我们也可以用另一种方式处理——容错机制。

容错是指系统在出现异常情况时,可以通过一定的策略保证系统仍然稳定运行,从而提高系统的可靠性和健壮性。

在分布式系统中,容错机制尤为重要,因为分布式系统中的各个组件都可能存在网络故障、节点故障等各种异常情况。要顾全大局,尽可能消除偶发 / 单点故障对系统带来的整体影响。

1)容错策略

容错策略有很多种,常用的容错策略主要是以下几个:

  • Fail-Over 故障转移:一次调用失败后,切换一个其他节点再次进行调用,也算是一种重试。
  • Fail-Back 失败自动恢复:系统的某个功能出现调用失败或错误时,通过其他的方法,恢复该功能的正常。可以理解为降级,比如重试、调用其他服务等。
  • Fail-Safe 静默处理:系统出现部分非重要功能的异常时,直接忽略掉,不做任何处理,就像错误没有发生过一样。
  • Fail-Fast 快速失败:系统出现调用错误时,立刻报错,交给外层调用方处理。

2) .容错实现方式

容错其实是个比较广泛的概念,除了上面几种策略外,很多技术都可以起到容错的作用。

比如:

  • 重试:重试本质上也是一种容错的降级策略,系统错误后再试一次。
  • 限流:当系统压力过大、已经出现部分错误时,通过限制执行操作(接受请求)的频率或数量,对系统进行保护。
  • 降级:系统出现错误后,改为执行其他更稳定可用的操作。也可以叫做 “兜底” 或 “有损服务”,这种方式的本质是:即使牺牲一定的服务质量,也要保证系统的部分功能可用,保证基本的功能需求得到满足。
  • 熔断:系统出现故障或异常时,暂时中断对该服务的请求,而是执行其他操作,以避免连锁故障。
  • 超时控制:如果请求或操作长时间没处理完成,就进行中断,防止阻塞和资源占用。

注意,在实际项目中,根据对系统可靠性的需求,我们通常会结合多种策略或方法实现容错机制。

        之前已经给系统增加重试机制了,算是实现了一部分的容错能力。现在,我们可以正式引入容错机制,通过更多策略来进一步增加系统可靠性。

这里我们提供 2 种方案:

  • 先容错再重试。

当系统发生异常时,首先会触发容错机制,比如记录日志、进行告警等,然后可以选择是否进行重试。

  • 先重试再容错。

在发生错误后,首先尝试重试操作,如果重试多次仍然失败,则触发容错机制,比如记录日志、进行告警等。

多种容错策略实现

在 RPC 项目中新建 fault.tolerant 包,将所有容错相关的代码放到该包下。

1) .先编写容错策略通用接口

先编写容错策略通用接口。提供一个容错方法,使用 Map 类型的参数接受上下文信息(可用于灵活地传递容错处理需要用到的数据),并且接受一个具体的异常类参数。

由于容错是应用到发送请求操作的,所以容错方法的返回值是 RpcResponse(响应)

代码如下:

package com.hpq.fault.tolerant;

import com.hpq.model.RpcResponse;

import java.util.Map;

/**
 * 容错策略
 */
public interface TolerantStrategy {
     RpcResponse  doTolerant(Map<String,Object> context,Exception e);
}

2) .快速失败容错策略

很好理解,就是遇到异常后,将异常再次抛出,交给外层处理。

代码如下:

package com.hpq.fault.tolerant;

import com.hpq.model.RpcResponse;

import java.util.Map;

/**
 * 快速失败容错策略(立刻通知外层调用方)
 */
public class FailFastTolerantStrategy implements TolerantStrategy{
    @Override
    public RpcResponse doTolerant(Map<String, Object> context, Exception e) {
        throw new RuntimeException("服务报错",e);
    }
}

3) .静默处理容错策略实现。

也很好理解,就是遇到异常后,记录一条日志,然后正常返回一个响应对象,就好像没有出现过报错。

代码如下:

package com.hpq.fault.tolerant;

import com.hpq.model.RpcResponse;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;

/**
 * 静默处理异常——容错策略
 */
@Slf4j
public class FailSafeTolerantStrategy implements TolerantStrategy{
    @Override
    public RpcResponse doTolerant(Map<String, Object> context, Exception e) {
        log.info("静默处理异常", e);
        return new RpcResponse();
    }
}

4) .故障恢复策略,降级到其他服务上去,可自行拓展

package com.hpq.fault.tolerant;

import com.hpq.model.RpcResponse;

import java.util.Map;

/**
 * 降级到其他服务——容错策略
 */
public class FailBackTolerantStrategy implements TolerantStrategy{
    @Override
    public RpcResponse doTolerant(Map<String, Object> context, Exception e) {
        // TODO  可自行扩展,获取降级的服务并调用
        return null;
}
}

 5) .故障转移策略,降级到其他服务上去,可自行拓展

package com.hpq.fault.tolerant;

import com.hpq.model.RpcResponse;

import java.util.Map;

/**
 * 转移到其他服务节点
 */
public class FailOverTolerantStrategy implements TolerantStrategy{
    @Override
    public RpcResponse doTolerant(Map<String, Object> context, Exception e) {
        // TODO  可自行扩展,获取转移的服务并调用
        return null;
    }
}
支持配置和扩展

像序列化器、注册中心、负载均衡器一样,我们的需求是,让开发者能够填写配置来指定使用的容错策略,并且支持自定义容错策略,让框架更易用、更利于扩展。

要实现这点,开发方式和序列化器、注册中心、负载均衡器都是一样的,都可以使用工厂创建对象、使用 SPI 动态加载自定义的注册中心。

1)容错策略常量。

在 fault.tolerant 包下新建 TolerantStrategyKeys 类,列举所有支持的容错策略键名。

代码如下:

package com.hpq.fault.tolerant;

/**
 * 容错策略键名常量
 */
public interface TolerantStrategyKeys {
    /**
     * 故障恢复
     */
    String FAIL_BACK = "failBack";
    /**
     * 快速失败
     */
    String FAIL_FAST = "failFast";
    /**
     * 故障转移
     */
    String FAIL_OVER = "failOver";
    /**
     * 静默处理
     */
    String FAIL_SAFE = "failSafe";
}

2)使用工厂模式,支持根据 key 从 SPI 获取容错策略对象实例。

在 fault.tolerant 包下新建 TolerantStrategyFactory 类,代码如下:

package com.hpq.fault.tolerant;

import com.hpq.spi.SpiLoader;

/**
 * 重试策略工厂
 */
public class TolerantStrategyFactory {
    static{
        SpiLoader.load(TolerantStrategy.class);
    }
    // 默认重试策略
    private static final TolerantStrategy DEFAULT_RETRY_STRATEGY = new FailFastTolerantStrategy();
    // 获取重试策略实例
    public static TolerantStrategy getInstance(String key){
        return SpiLoader.getInstance(TolerantStrategy.class,key);
    }
}

3)在 META-INF 的 rpc/system 目录下编写容错策略接口的 SPI 配置文件

代码如下:

failOver=com.hpq.fault.tolerant.FailOverTolerantStrategy
failBack=com.hpq.fault.tolerant.FailBackTolerantStrategy
failFast=com.hpq.fault.tolerant.FailFastTolerantStrategy
failSafe=com.hpq.fault.tolerant.FailSafeTolerantStrategy

4)为 RpcConfig 全局配置新增容错策略的配置,代码如下:

package com.hpq.config;

import com.hpq.fault.retry.RetryStrategyKeys;
import com.hpq.loadbalancer.LoadBalancerKeys;
import com.hpq.serializer.SerializerKeys;
import lombok.Data;

/**
 * Rpck框架全局配置
 *
 */
@Data
public class RpcConfig {
    .....
    
    //容错策略
    private String tolerantStrategy = TolerantStrategyKeys.FAIL_FAST;

}

应用容错机制 

容错功能的应用非常简单,我们只需要修改 ServiceProxy 的部分代码,在重试多次抛出异常时,从工厂中获取容错策略并执行即可。

修改的代码如下:

package com.hpq.proxy;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.hpq.RpcApplication;
import com.hpq.config.RpcConfig;
import com.hpq.constant.RpcConstant;
import com.hpq.fault.retry.RetryStrategy;
import com.hpq.fault.retry.RetryStrategyFactory;
import com.hpq.fault.tolerant.TolerantStrategy;
import com.hpq.fault.tolerant.TolerantStrategyFactory;
import com.hpq.loadbalancer.LoadBalancer;
import com.hpq.loadbalancer.LoadBalancerFactory;
import com.hpq.model.RpcRequest;
import com.hpq.model.RpcResponse;
import com.hpq.model.ServiceMetaInfo;
import com.hpq.protocol.*;
import com.hpq.register.Registry;
import com.hpq.register.RegistryFactory;
import com.hpq.serializer.Serializer;
import com.hpq.serializer.SerializerFactory;
import com.hpq.server.tcp.VertxTcpClient;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetSocket;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * jdk动态代理
 */
public class ServiceProxy implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String serviceName = method.getDeclaringClass().getName();
        // 构造请求
        RpcRequest rpcRequest = RpcRequest.builder()
                .interfaceName(serviceName)
                .methodName(method.getName())
                .parameterTypes(method.getParameterTypes())
                .parameters(args)
                .build();
        // 从注册中心获取服务提供者请求地址
        RpcConfig rpcConfig = RpcApplication.getRpcConfig();
        Registry registry = RegistryFactory.getInstance(rpcConfig.getRegisterConfig().getRegistry());
        ServiceMetaInfo metaInfo = new ServiceMetaInfo();
        metaInfo.setServiceName(serviceName);
        metaInfo.setServiceVersion(RpcConstant.DEFAULT_SERVICE_VERSION);
        // 打印日志
        System.out.println("正在从注册中心获取服务实例:" + metaInfo.getServiceKey());
        List<ServiceMetaInfo> serviceMetaInfoList = registry.serviceDiscovery(metaInfo.getServiceKey());
        if (CollUtil.isEmpty(serviceMetaInfoList)) {
            // 打印日志
            System.out.println("未发现服务:" + metaInfo.getServiceKey());
            throw new RuntimeException("未发现服务");
        }
        // 打印日志
        System.out.println("发现的服务实例数量:" + serviceMetaInfoList.size());
        //负载均衡
        LoadBalancer loadBalancer = LoadBalancerFactory.getInstance(rpcConfig.getLoadBalancer());
        // 将调用方法名(请求路径)作为负载均衡参数
        Map<String, Object> requestParams = new HashMap<>();
        requestParams.put("methodName", rpcRequest.getMethodName());
        ServiceMetaInfo serviceMetaInfo = loadBalancer.select(requestParams, serviceMetaInfoList);
        //发送TCP请求
        RpcResponse rpcResponse;
        try {
            //使用重试机制
            RetryStrategy retryStrategy = RetryStrategyFactory.getInstance(rpcConfig.getRetryStrategy());
            rpcResponse = retryStrategy.doRetry(() ->
                    VertxTcpClient.doRequest(rpcRequest, serviceMetaInfo)
            );
        } catch (IOException e) {
            //容错机制
            TolerantStrategy tolerantStrategy = TolerantStrategyFactory.getInstance(rpcConfig.getTolerantStrategy());
            return tolerantStrategy.doTolerant(null, e);
        }
        return rpcResponse.getData();
    }
}
12.启动机制和注解驱动

其实框架目前是不够易用的,单是我们的示例服务提供者,就要写很长的代码!本节教程,我们就来优化框架的易用性,通过建立合适的启动机制和注解驱动机制,帮助开发者最少只用一行代码,就能轻松使用框架!

方案设计

1) .启动机制设计

把所有启动代码封装成一个 专门的启动类 或方法,然后由服务提供者 / 服务消费者调用即可。

但有一点我们需要注意,服务提供者和服务消费者需要初始化的模块是不同的,比如服务消费者不需要启动 Web 服务器。

所以我们需要针对服务提供者和消费者分别编写一个启动类,如果是二者都需要初始化的模块,可以放到全局应用类 RpcApplication 中,复用代码的同时保证启动类的可维护、可扩展性。

2) .注解驱动设计

        除了启动类外,其实还有一种更牛的方法,能帮助开发者使用框架。学过 Dubbo 这款 RPC 框架的同学应该会有印象,Dubbo 中的做法是 注解驱动,开发者只需要在服务提供者实现类打上一个 DubboService 注解,就能快速注册服务;同样的,只要在服务消费者字段打上一个 DubboReference 注解,就能快速使用服务。

        由于现在的 Java 项目基本都使用 Spring Boot 框架,所以 Dubbo 还贴心地推出了 Spring Boot Starter,用更少的代码在 Spring Boot 项目中使用框架。

那我们也可以仿照Dubbo,创建一个 Spring Boot Starter 项目,并通过注解驱动框架的初始化,完成服务注册和获取引用。

启动机制

我们在 rpc 项目中新建包名 bootstrap,所有和框架启动初始化相关的代码都放到该包下

2).服务提供者启动类

新建 ProviderBootstrap 类,先直接复制之前服务提供者示例项目中的初始化代码,然后略微改造,支持用户传入自己要注册的服务。

在注册服务时,我们需要填入多个字段,比如服务名称、服务实现类,参考代码如下:

// 注册服务
String serviceName = UserService.class.getName();
LocalRegistry.register(serviceName, UserServiceImpl.class);

我们可以将这些字段进行封装,在 model 包下新建 ServiceRegisterInfo 类,代码如下:

package com.hpq.model;

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

/**
 * 服务注册信息
 * @param <T>
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ServiceRegisterInfo<T> {
    //服务名称
    private String serviceName;
    //实现类
    private Class<? extends T> implClass;
}

这样一来,服务提供者的初始化方法只需要接受封装的注册信息列表作为参数即可,简化了方法。

服务提供者完整代码如下:

package com.hpq.bootstrap;

import com.hpq.RpcApplication;
import com.hpq.config.RegistryConfig;
import com.hpq.config.RpcConfig;
import com.hpq.model.ServiceMetaInfo;
import com.hpq.model.ServiceRegisterInfo;
import com.hpq.register.LocalRegister;
import com.hpq.register.Registry;
import com.hpq.register.RegistryFactory;
import com.hpq.server.tcp.VertxTcpServer;
import java.util.List;

public class ProviderBootStrap {
    public static void init(List<ServiceRegisterInfo> serviceRegisterInfoList){
        //Rpc框架初始化
        RpcApplication.init();
        //全局配置
        final RpcConfig rpcConfig = RpcApplication.getRpcConfig();
        //注册服务
        for(ServiceRegisterInfo<?> serviceRegisterInfo:serviceRegisterInfoList){
            String  serviceName = serviceRegisterInfo.getServiceName();
            LocalRegister.register(serviceName, serviceRegisterInfo.getImplClass());
            //注册服务到注册中心
            RegistryConfig registryConfig = rpcConfig.getRegisterConfig();
            Registry registry = RegistryFactory.getInstance(registryConfig.getRegistry());
            ServiceMetaInfo serviceMetaInfo = new ServiceMetaInfo();
            serviceMetaInfo.setServiceName(serviceName);
            serviceMetaInfo.setServiceHost(rpcConfig.getServerHost());
            serviceMetaInfo.setServicePort(rpcConfig.getServerPort());
            try{
                registry.register(serviceMetaInfo);
            } catch (Exception e) {
                throw new RuntimeException(serviceName + " 服务注册失败", e);
            }
        }
        //启动服务器
        VertxTcpServer vertxTcpServer = new VertxTcpServer();
        vertxTcpServer.doStart(rpcConfig.getServerPort());

    }
}

现在,我们想要在服务提供者项目中使用 RPC 框架,就非常简单了。只需要定义要注册的服务列表,然后一行代码调用 ProviderBootstrap.init 方法即可完成初始化。

package com.hpq;
import com.hpq.bootstrap.ProviderBootStrap;
import com.hpq.model.ServiceRegisterInfo;
import com.hpq.register.LocalRegister;
import com.hpq.server.tcp.VertxTcpServer;
import com.hpq.service.UserService;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class ProviderApplication {
    public static void main(String[] args) throws IOException {
        //要注册的服务
        List<ServiceRegisterInfo> serviceRegisterInfoList = new ArrayList<>();
        ServiceRegisterInfo serviceRegisterInfo = new ServiceRegisterInfo(UserService.class.getName(), UserServiceImpl.class);
        serviceRegisterInfoList.add(serviceRegisterInfo);
        //服务提供者初始化
        ProviderBootStrap.init(serviceRegisterInfoList);
    }
}

3) .服务消费者启动类

服务消费者启动类的实现就更简单了,因为它不需要注册服务、也不需要启动 Web 服务器,只需要执行  RpcApplication.init 完成框架的通用初始化即可。

package com.hpq.bootstrap;

import com.hpq.RpcApplication;

public class ConsumerBootStrap {
    public static void init(){
        //Rpc框架初始化
        RpcApplication.init();
    }
}

服务消费者示例项目的代码不会有明显的变化,只不过改为调用启动类了。

package com.hpq;

import com.hpq.bootstrap.ConsumerBootStrap;
import com.hpq.model.User;
import com.hpq.proxy.ServiceProxyFactory;
import com.hpq.service.UserService;

public class ConsumerApplication {
    public static void main(String[] args) {
     ConsumerBootStrap.init();
        //动态代理
        UserService userService = ServiceProxyFactory.getProxy(UserService.class);
        User user = new User();
        user.setName("hpq");
        //调用服务
      User newUser = userService.getUser(user);
      if (newUser != null){
          System.out.println("用户名:"+newUser.getName());
      }else{
          System.out.println("用户不存在");
      }
    }
}
注解驱动

1).项目初始化

为了便于大家学习,不要和已有项目的代码混淆,我们再来创建一个新的项目模块,专门用于实现 Spring Boot Starter 注解驱动的 RPC 框架。

在项目根目录下点击右键新建模块,选择 Spring Initializr,将 Server URL 更改为 start.aliyun.com,然后创建一个名为hpq-rpc-spring-boot-starter的模块,JDK 和 Java 版本选择 大于等于8即可。

 选择 Spring Boot 版本为 2.6,项目依赖如下:

 创建好模块后,修改 pom.xml 文件,移除无用的插件代码:

 <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <mainClass>com.hpq.hpq-rpc-spring-boot-starter.HpqRpcSpringBootStarterApplication</mainClass>
                    <skip>true</skip>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

引入我们开发的 RPC 框架:

<dependency>
    <groupId>com.yupi</groupId>
    <artifactId>yu-rpc-core</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

至此,Spring Boot Starter 项目已经完成初始化。

2) .定义注解

实现注解驱动的第一步是定义注解,要定义哪些注解呢?可以参考知名框架 Dubbo 的注解

比如:

  1. @EnableDubbo:在 Spring Boot 主应用类上使用,用于启用 Dubbo 功能。
  2. @DubboComponentScan:在 Spring Boot 主应用类上使用,用于指定 Dubbo 组件扫描的包路径。
  3. @DubboReference:在消费者中使用,用于声明 Dubbo 服务引用。
  4. @DubboService:在提供者中使用,用于声明 Dubbo 服务。
  5. @DubboMethod:在提供者和消费者中使用,用于配置 Dubbo 方法的参数、超时时间等。
  6. @DubboTransported:在 Dubbo 提供者和消费者中使用,用于指定传输协议和参数,例如传输协议的类型、端口等。

这些注解我们不需要全部用到,我们只需要定义 3 个注解。

  • 在项目下新建 annotation 包,将所有注解代码放到该包下。

如下图:

  • @EnableRpc:用于全局标识项目需要引入 RPC 框架、执行初始化方法。

由于服务消费者和服务提供者初始化的模块不同,我们需要在 EnableRpc 注解中,指定是否需要启动服务器等属性。

代码如下:

package com.hpq.rpc.springboot.starter.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 启用Rpc注解
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableRpc {
    //需要启动server
    boolean needServer() default true;
}
  • @RpcService:服务提供者注解,在需要注册和提供的服务类上使用。

RpcService 注解中,需要指定服务注册信息属性,比如服务接口实现类、版本号等(也可以包括服务名称)。

代码如下:

package com.hpq.rpc.springboot.starter.annotation;

import com.hpq.constant.RpcConstant;
import org.springframework.stereotype.Component;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 服务提供者注解(用于注册服务)
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface RpcService {
    //服务接口类
    Class<?> interfaceClass() default void.class;
    //版本
    String serviceVersion() default RpcConstant.DEFAULT_SERVICE_VERSION;
}
  • @RpcReference:服务消费者注解,在需要注入服务代理对象的属性上使用,类似 Spring 中的 @Resource 注解。

RpcReference 注解中,需要指定调用服务相关的属性,比如服务接口类(可能存在多个接口)、版本号、负载均衡器、重试策略、是否 Mock 模拟调用等。

package com.hpq.rpc.springboot.starter.annotation;

import com.hpq.constant.RpcConstant;
import com.hpq.fault.retry.RetryStrategyKeys;
import com.hpq.fault.tolerant.TolerantStrategyKeys;
import com.hpq.loadbalancer.LoadBalancerKeys;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 服务消费者注解(用于注入服务)
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RpcReference {
     //服务接口类
    Class<?> interfaceClass() default void.class;
    //版本
    String serviceVersion() default RpcConstant.DEFAULT_SERVICE_VERSION;
     //负载均衡器
    String loadBalancer() default LoadBalancerKeys.ROUND_ROBIN;
     //重试策略
    String retryStrategy() default RetryStrategyKeys.NO;
     //容错策略
    String tolerantStrategy() default TolerantStrategyKeys.FAIL_FAST;
     //模拟调用
    boolean mock() default false;

}

 3) .注解驱动

在 starter 项目中新建 bootstrap 包,并且分别针对上面定义的 3 个注解新建启动类。

项目的目录结构如图:

  • Rpc 框架全局启动类 RpcInitBootstrap 

在 Spring 框架初始化时,可以通过实现 Spring 的 ImportBeanDefinitionRegistrar 接口,并且在 registerBeanDefinitions 方法中,获取到项目的注解和注解属性,初始化 RPC 框架

package com.hpq.rpc.springboot.starter.bootstrap;

import com.hpq.RpcApplication;
import com.hpq.config.RpcConfig;
import com.hpq.rpc.springboot.starter.annotation.EnableRpc;
import com.hpq.server.tcp.VertxTcpServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

@Slf4j
public class RpcInitBootstrap implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //获取EnableRpc注解信息
        boolean needServer = (boolean) importingClassMetadata.getAnnotationAttributes(EnableRpc.class.getName()).get("needServer");
        //RPC框架初始化
        RpcApplication.init();
        //全局配置
        final RpcConfig rpcConfig = RpcApplication.getRpcConfig();
        //启动服务器
        if (needServer) {
            VertxTcpServer tcpServer = new VertxTcpServer();
            tcpServer.doStart(rpcConfig.getServerPort());
        }else{
            log.info("RPC Server is not started");
        }
    }
}

此处代码与之前相比只是增加了服务器是否启动的逻辑,其他并无变化

  • Rpc 服务提供者启动类 RpcProviderBootstrap

 服务提供者启动类的作用是,获取到所有包含 @RpcService 注解的类,并且通过注解的属性和反射机制,获取到要注册的服务信息,并且完成服务注册。

只需要让启动类实现 BeanPostProcessor 接口的 postProcessAfterInitialization 方法,就可以在某个服务提供者 Bean 初始化后,执行注册服务等操作了。

package com.hpq.rpc.springboot.starter.bootstrap;

import com.hpq.RpcApplication;
import com.hpq.config.RegistryConfig;
import com.hpq.config.RpcConfig;
import com.hpq.model.ServiceMetaInfo;
import com.hpq.register.LocalRegister;
import com.hpq.register.Registry;
import com.hpq.register.RegistryFactory;
import com.hpq.rpc.springboot.starter.annotation.RpcService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.BeanPostProcessor;

/**
 * Rpc 服务提供者启动
 */
@Slf4j  
public class RpcProviderBootstrap implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        Class<?> beanClass = bean.getClass();
        RpcService rpcService = beanClass.getAnnotation(RpcService.class);
        if(rpcService != null){
            //获取服务基本信息
            Class<?> interfaceClass = rpcService.interfaceClass();
            //默认值处理
            if (interfaceClass == void.class){
                interfaceClass = beanClass.getInterfaces()[0];
            }
            String serviceName = interfaceClass.getName();
            String serviceVersion = rpcService.serviceVersion();
            //注册服务
            LocalRegister.register(serviceName, beanClass);
            //全局配置
            final RpcConfig rpcConfig = RpcApplication.getRpcConfig();
            // 注册服务到注册中心
            RegistryConfig registryConfig = rpcConfig.getRegisterConfig();
            Registry registry = RegistryFactory.getInstance(registryConfig.getRegistry());
            ServiceMetaInfo serviceMetaInfo = new ServiceMetaInfo();
            serviceMetaInfo.setServiceName(serviceName);
            serviceMetaInfo.setServiceVersion(serviceVersion);
            serviceMetaInfo.setServiceHost(rpcConfig.getServerHost());
            serviceMetaInfo.setServicePort(rpcConfig.getServerPort());
            try {
                registry.register(serviceMetaInfo);
            }catch ( Exception e){
                throw new RuntimeException(serviceName +"服务注册失败"+e);
            }
        }
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

}
  • Rpc 服务消费者启动类 RpcConsumerBootstrap

服务提供者启动类的实现方式类似,在 Bean 初始化后,通过反射获取到 Bean 的所有属性,如果属性包含 @RpcReference 注解,那么就为该属性动态生成代理对象并赋值。

package com.hpq.rpc.springboot.starter.bootstrap;

import com.hpq.proxy.ServiceProxyFactory;
import com.hpq.rpc.springboot.starter.annotation.RpcReference;
import org.springframework.beans.factory.config.BeanPostProcessor;

import java.lang.reflect.Field;

// Rpc 服务消费者启动
public class RpcConsumerBootstrap implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        Class<?> beanClass = bean.getClass();
        //遍历对象的所有字段
        Field[] declaredFields = beanClass.getDeclaredFields();
        for (Field field : declaredFields) {
            RpcReference rpcReference = field.getAnnotation(RpcReference.class);
            if (rpcReference != null) {
                // 为属性生成代理对象
                Class<?> interfaceClass = rpcReference.interfaceClass();
                if (interfaceClass == void.class) {
                    interfaceClass = field.getType();
                }
                field.setAccessible(true);
                Object proxyObject = ServiceProxyFactory.getProxy(interfaceClass);
                try {
                    field.set(bean, proxyObject);
                    field.setAccessible(false);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException("为字段注入代理对象失败", e);
                }
            }
        }
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }
}
  • 注册已编写的启动类

别忘了在 Spring 中加载我们已经编写好的启动类,仅在用户使用 @EnableRpc 注解时,才启动 RPC 框架。可以通过给 EnableRpc 增加 @Import 注解,来注册我们自定义的启动类,实现灵活的可选加载。

修改后的 EnableRpc 注解代码如下:

至此,一个基于注解驱动的 RPC 框架 Starter 开发完成 

测试

让我们使用 IDEA 新建 2 个使用 Spring Boot 2 框架的项目。

  • 示例 Spring Boot 消费者:example-springboot-consumer
  • 示例 Spring Boot 提供者:example-springboot-provider

 每个项目都加上依赖:

 <dependency>
            <groupId>com.hpq</groupId>
            <artifactId>hpq-rpc-spring-boot-starter</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.hpq</groupId>
            <artifactId>demo_common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

1) . 示例服务提供者项目的入口类加上 @EnableRpc 注解,代码如下:

package com.hpq.examplespringbootprovider;

import com.hpq.rpc.springboot.starter.annotation.EnableRpc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableRpc
public class ExampleSpringbootProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(ExampleSpringbootProviderApplication.class, args);
    }

}

2) .服务提供者提供一个简单的服务,代码如下: 

package com.hpq.examplespringbootprovider;

import com.hpq.model.User;
import com.hpq.rpc.springboot.starter.annotation.RpcService;
import com.hpq.service.UserService;
import org.springframework.stereotype.Service;

@Service
@RpcService
public class UserServiceImpl implements UserService {
    public User getUser(User user) {
        System.out.println("用户名:" + user.getName());
        return user;
    }
}

3) .示例服务消费者的入口类加上 @EnableRpc(needServer = false) 注解,标识启动 RPC 框架,但不启动服务器。代码如下:

package com.hpq.examplespringbootconsumer;

import com.hpq.rpc.springboot.starter.annotation.EnableRpc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableRpc(needServer = false)
public class ExampleSpringbootConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ExampleSpringbootConsumerApplication.class, args);
    }

}

5) .消费者编写一个 Spring 的 Bean,引入 UserService 属性并打上 @RpcReference 注解,表示需要使用远程服务提供者的服务。

代码如下:

package com.hpq.examplespringbootconsumer;

import com.hpq.model.User;
import com.hpq.rpc.springboot.starter.annotation.RpcReference;
import com.hpq.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class ExampleServiceImpl {
    @RpcReference
    private UserService userService;
    public void test() {
        User user = new User();
        user.setName("hpq");
       User result = userService.getUser(user);
        System.out.println(result.getName());
    }
}

 服务消费者编写单元测试,验证能否调用远程服务:

package com.hpq.examplespringbootconsumer;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
class ExampleSpringbootConsumerApplicationTests {
    @Resource
    private ExampleServiceImpl exampleServiceImpl ;

    @Test
    void test() {
        exampleServiceImpl.test();
    }

}

启动服务提供者入口类和服务消费者的入口类,如下图:

可以看到 server 并没有启动,符合预期。

最后,执行服务消费者的单元测试,验证能否跑通整个流程。

如下图,调用成功:

至此,我们就能够通过使用注解的方式,轻松地给项目引入 RPC 框架了~ 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值