RPC框架(二)服务端篇

服务端

服务端调用逻辑:

服务端:NettyServerMain.java--->Rpc框架下:NettyRpcServer.java ---> Rpc框架下:NettyRpcServerHandler.java 

先来看目录结构:服务端的目录结构如下:

再看看HelloServiceImpl的内容:这个主要是复写了API里面HelloService的hello方法。

package com.cjd.service;

import com.cjd.anno.RpcService;
import com.cjd.api.Hello;
import com.cjd.api.HelloService;

import lombok.extern.slf4j.Slf4j;

//暴露服务接口
@Slf4j
@RpcService(version = "version1",group = "group1")
public class HelloServiceImpl implements HelloService {

    static {
        System.out.println("HelloServiceImpl被创建");
    }

    @Override
    public String hello(Hello hello) {
        /**
         * 服务器端收到客户端发送的hello信息,服务器端日志输出message的内容,
         * 并把description的内容发送给客户端
         */
        log.info("HelloServiceImpl收到:{}",hello.getMessage());
        String result = "Hello description is(Service)"+hello.getDescription();
        log.info("HelloServiceImpl返回:{}",result);
        return result;
    }
}

这段代码中出现了一个不曾见过的@RpcServer。这个后面会提到,包括在下面中也出现了一个类似的RpcScan。

接着说NettyServerMain.java的内容:

package com.cjd.service;

import com.cjd.anno.RpcScan;
import com.cjd.api.HelloService;
import com.cjd.config.RpcServiceConfig;
import com.cjd.server.NettyRpcServer;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * 启动netty,并注册服务
 */
// 这种方式可以让你在项目中使用自定义的注解来指定需要扫描的包,从而实现自动注册 RPC 服务的功能,而无需手动一个个去注册。
// 告诉系统去扫描 com.cjd 包下的类,并将其注册为 RPC 服务的组件。
@RpcScan(basePackage = {"com.cjd"})
public class NettyServerMain {
    public static void main(String[] args) {
        // AnnotationConfigApplicationContext 是 Spring Framework 提供的用于基于注解的应用上下文,它允许通过注解来注册和管理 Bean。
//        通过传入 NettyServerMain.class,这行代码告诉 Spring 去扫描 NettyServerMain 类及其相关的注解,以此来构建应用上下文。
//        在初始化过程中,Spring 会扫描 NettyServerMain 类及其相关的注解,然后根据这些注解来注册和管理 Bean。
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(NettyServerMain.class);
//        通过 applicationContext.getBean("nettyRpcServer") 获取名为 "nettyRpcServer" 的 Bean,并将其强制类型转换为 NettyRpcServer 类型。
//        如果有一个类名为NettyRpcServer,当使用@Component注解进行注册时,默认的Bean名称就是nettyRpcServer。
//        applicationContext.getBean(NettyRpcServer.class); 也可以考虑这样实现
        NettyRpcServer nettyRpcServer = (NettyRpcServer) applicationContext.getBean("nettyRpcServer");
        HelloService helloService =new HelloServiceImpl();
        RpcServiceConfig rpcServiceConfig = RpcServiceConfig.builder()
                .group("test1").version("version1").service(helloService).build();
        nettyRpcServer.registerService(rpcServiceConfig);
        nettyRpcServer.start();
    }
}

其中里面出现了很多暂时没出现的内容,例如RpcScan、NettyRpcServer、RpcServiceConfig。

 从RpcScan开始引申

其中RpcScan和RpcServer涉及到Spring的知识,NettyRpcServer和RpcServiceConfig是Rpc框架中自己写的内容,然后再看看整体的框架。

第一个是RPC的框架,第二个是RPC框架的一些工具包之类的。

anno目录

其中RpcScan、RpcServer都在anno中。

RpcScan:

代码内容如下:

package com.cjd.anno;

import com.cjd.spring.CustomScannerRegistrar;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})   // 注解的使用范围,可以用在类和方法上
@Retention(RetentionPolicy.RUNTIME)  //注解的生命周期为运行时,在运行时保留并可以通过反射来获取
@Import(CustomScannerRegistrar.class)   // 在使用了 @RpcScan 注解的类上自动导入 CustomScannerRegistrar 类。
@Documented  // 该注解会被包含在 Javadoc 中
public @interface RpcScan {
    // 用于指定扫描的基础包,可以方便地在指定包下进行扫描和注册 RPC 服务。
    String[] basePackage();

}

 Rpcservice:

代码如下:

package com.cjd.anno;

import java.lang.annotation.*;

@Documented  //该注解会包含在java文档中
@Retention(RetentionPolicy.RUNTIME) //注解在运行时可用,通过反射来获取注解信息
@Target(ElementType.TYPE) //该注解应用于类、接口、枚举
@Inherited  //注解可以被子类继承
public @interface RpcService {
    //可以指定版本和分组
    String version() default "";
    String group() default "";
}

最后剩一个RpcReference,这个是关于客户端的内容,等到后面再讲。

在RpcScan中,有一行@Import(CustomScannerRegister.class);

@Import(CustomScannerRegistrar.class) 是一个 Spring Framework 中的注解,它用于向 Spring 容器中导入一个或多个配置类或者 Bean。在这个例子中,CustomScannerRegistrar.class 是一个用于配置扫描器的自定义类。

当使用 @Import 注解时,Spring 在初始化应用程序上下文时会处理这个注解,并将 CustomScannerRegistrar 中定义的配置类或 Bean 导入到 Spring 容器中。这样做可以让你在一个地方集中管理相关的配置,并且可以在多个地方重用这些配置。

需要注意的是,CustomScannerRegistrar 类需要实现 ImportBeanDefinitionRegistrar 接口,以便在 registerBeanDefinitions 方法中定义需要导入的配置类或 Bean。

Spring目录

此类代码都放在了sprin目录下:

CustomScannerRegistrar:

下面的代码是CustomScannerRegistrar.java

package com.cjd.spring;

import com.cjd.anno.RpcScan;
import com.cjd.anno.RpcService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.StandardAnnotationMetadata;
import org.springframework.stereotype.Component;

/**
 * 实现了 ImportBeanDefinitionRegistrar 和 ResourceLoaderAware 接口的 CustomScannerRegistrar 类。
 * ImportBeanDefinitionRegistrar 是一个接口,用于动态注册 Bean 定义到 Spring 的 BeanFactory 中。
 * ResourceLoaderAware 是一个接口,用于设置 ResourceLoader 实例,用于加载资源。
 */
@Slf4j
public class CustomScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
    // 表示 Spring Bean 扫描的基础包。
    private static final String SPRING_BEAN_BASE_PACKAGE = "com.cjd";
    // 表示 RpcScan 注解中 basePackage 属性的名称。
    private static final String BASE_PACKAGE_ATTRIBUTE_NAME = "basePackage";
    private ResourceLoader resourceLoader;


    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    /**
     * ImportBeanDefinitionRegistrar 接口的实现方法,用于注册 Bean 定义到 BeanDefinitionRegistry 中
     * @param annotationMetadata
     * @param beanDefinitionRegistry
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
//      通过 annotationMetadata 获取 RpcScan 注解的属性和值。
        AnnotationAttributes rpcScanAnnotationAttributes = AnnotationAttributes.fromMap(annotationMetadata.getAnnotationAttributes(RpcScan.class.getName()));
        String[] rpcScanBasePackages = new String[0];
        if (rpcScanAnnotationAttributes != null){
            rpcScanBasePackages = rpcScanAnnotationAttributes.getStringArray(BASE_PACKAGE_ATTRIBUTE_NAME);
        }
        // 如果 rpcScanBasePackages 属性为空,则将当前类所在的包作为基础包进行扫描
        if (rpcScanBasePackages.length == 0){
            rpcScanBasePackages = new String[]{((StandardAnnotationMetadata) annotationMetadata).getIntrospectedClass().getPackage().getName()};
        }
        // 创建 CustomScanner 实例,分别用于扫描带有 RpcService 和 Component 注解的类。
        CustomScanner rpcServiceScanner = new CustomScanner(beanDefinitionRegistry, RpcService.class);
        CustomScanner springBeanScanner = new CustomScanner(beanDefinitionRegistry, Component.class);
//        如果 resourceLoader 不为空,则将其设置到扫描器中。
        if (resourceLoader != null) {
            rpcServiceScanner.setResourceLoader(resourceLoader);
            springBeanScanner.setResourceLoader(resourceLoader);
        }
        // 用扫描器的 scan 方法进行扫描,并返回扫描到的类数量。
        int springBeanAmount = springBeanScanner.scan(SPRING_BEAN_BASE_PACKAGE);
        log.info("springBeanScanner扫描的数量 [{}]", springBeanAmount);
        int rpcServiceCount = rpcServiceScanner.scan(rpcScanBasePackages);
        log.info("rpcServiceScanner扫描的数量 [{}]", rpcServiceCount);

    }
}

CustomScanner:

此时出现了一个CustomScanner

代码如下

package com.cjd.spring;

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.core.type.filter.AnnotationTypeFilter;

import java.lang.annotation.Annotation;

public class CustomScanner extends ClassPathBeanDefinitionScanner {

    // 构造方法接受一个 BeanDefinitionRegistry 实例和一个注解类型 annoType,并调用父类的构造方法进行初始化

    /**
     *
     * @param registry  这是一个接口,它代表了一个 Bean 定义注册表,负责注册和管理 Bean 定义。
     *                  在构造方法中,它被用来初始化 CustomScanner 类的父类 ClassPathBeanDefinitionScanner。
     * @param annoType  这是一个注解类型的 Class 对象,它表示要扫描的目标注解类型。
     *                  在构造方法中,它被用来指定需要扫描的注解类型。
     */
    public CustomScanner(BeanDefinitionRegistry registry, Class<? extends Annotation> annoType) {
        super(registry);
        // 将指定注解类型的类作为要扫描的目标
        super.addIncludeFilter(new AnnotationTypeFilter(annoType));
    }

    // 调用父类的 super.scan(basePackages) 方法进行扫描,并返回扫描到的类数量。
    @Override
    public int scan(String... basePackages) {
        return super.scan(basePackages);
    }
}

spring目录下还差最后一个SpringBeanPostProcessor。

@Component 注解用于将 SpringBeanPostProcessor 类标识为 Spring 容器中的一个组件,这意味着它将被自动扫描并注册到应用程序上下文中。

同时,SpringBeanPostProcessor 类实现了 BeanPostProcessor 接口。这个接口允许你在 Spring bean 实例化、依赖注入和初始化的过程中插入自定义的处理逻辑。通过实现 BeanPostProcessor 接口,你可以在 bean 初始化前后执行额外的逻辑,比如日志记录、性能监控等。

总之,这段代码的作用是将 SpringBeanPostProcessor 类注册为一个 Spring 组件,并且它将在 bean 初始化的前后执行额外的逻辑。

SpringBeanPostProcessor:

package com.cjd.spring;

import com.cjd.anno.RpcReference;
import com.cjd.anno.RpcService;
import com.cjd.api.RpcRequestTransport;
import com.cjd.config.RpcServiceConfig;
import com.cjd.enums.RpcRequestTransportEnum;
import com.cjd.factory.SingletonFactory;
import com.cjd.extension.ExtensionLoader;
import com.cjd.provider.Impl.ZkServiceProviderImpl;
import com.cjd.provider.ServiceProvider;
import com.cjd.proxy.RpcClientProxy;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;

// 标识这个类是一个 Spring 组件,会被 Spring 容器自动扫描并注册为 Bean。

/**
 * @RpcReference 和 @RpcService 注解的使用场景和作用不同,因此在处理时采取了不同的方式。
 * 对于 @RpcReference 注解:
 * 因为 @RpcReference 注解通常用于标注在需要远程服务引用的字段上,例如需要注入的远程服务接口 helloController,
 * 所以在处理时需要遍历目标 Bean 的所有字段,检查每个字段是否被 @RpcReference 注解标记。
 * 
 * 对于 @RpcService 注解:
 * 通常 @RpcService 注解标注在实际提供远程服务的类上,表示该类是一个远程服务 helloServiceImpl,
 * 因此在处理时直接判断目标 Bean 的类上是否直接标注了 @RpcService 注解即可。
 */
@Slf4j
@Component
public class SpringBeanPostProcessor implements BeanPostProcessor {

    private final ServiceProvider serviceProvider;
    private final RpcRequestTransport rpcClient;

    public SpringBeanPostProcessor() {
        this.serviceProvider = SingletonFactory.getInstance(ZkServiceProviderImpl.class);
        this.rpcClient = ExtensionLoader.getExtensionLoader(RpcRequestTransport.class).getExtension(RpcRequestTransportEnum.NETTY.getName());
    }

    /**
     * 这个方法在目标 Bean 初始化之前被调用。在这个方法中,首先判断目标 Bean 是否被 @RpcService 注解标记,
     * 如果是,则获取该注解信息,构建 RpcServiceConfig,并通过 ServiceProvider 发布该服务。
     * @param bean
     * @param beanName
     * @return
     * @throws BeansException
     */
    @SneakyThrows
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        // 首先判断目标 Bean 的类上是否标注了 @RpcService 注解
        if (bean.getClass().isAnnotationPresent(RpcService.class)) {
            log.info("[{}] is annotated with  [{}]", bean.getClass().getName(), RpcService.class.getCanonicalName());
            // 获取目标 Bean 类上的 @RpcService 注解实例。
            RpcService rpcService = bean.getClass().getAnnotation(RpcService.class);
            // 根据获取到的 @RpcService 注解信息group,version,构建了一个 RpcServiceConfig 对象,设置了服务的组、版本和实际的服务对象。
            RpcServiceConfig rpcServiceConfig = RpcServiceConfig.builder()
                    .group(rpcService.group())
                    .version(rpcService.version())
                    .service(bean).build();
            // 通过 ServiceProvider 发布该服务,将服务注册到服务提供者中。
            serviceProvider.publishService(rpcServiceConfig);
        }
        // 返回原始的目标 Bean 对象。
        return bean;
    }

    /**
     * 这个方法在目标 Bean 初始化之后被调用。
     * 在这个方法中,首先获取目标 Bean 的类信息和声明的字段,然后遍历所有字段,检查是否带有 @RpcReference 注解。
     * 如果某个字段带有 @RpcReference 注解,则构建 RpcServiceConfig,并创建 RpcClientProxy,
     * 通过 RpcClientProxy 获取代理对象,最后将代理对象设置到字段中。
     * @param bean
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // 与RpcReference内容有关,暂时省略
    }


}

这段代码中就出现了不少新的关联性的东西,例如在rpc-framework-common项目下的SingletonFactory、ExtensionLoader。这两个工具类暂时先不提及,后续同一写一个章节讲,先将按照服务端的运行路线往下走。

服务端的部分,出现了三个之前没出现过的,一个是RpcServiceConfig,另一个是serviceProvider

,还一个是RpcRequestTransport。

其中RpcServiceConfig的内容比较简单,就是一些关于rpc的基础信息。

config目录

放在config目录下。

package com.cjd.config;

import lombok.*;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
@ToString
public class RpcServiceConfig {
    //    rpc服务版本 默认空
    private String version = "";
    //    rpc服务分组,默认空
    private String group = "";
    //    rpc服务的实例对象
    private Object service;
    //    获取rpc服务方法名称。服务名称由:服务名字+分组+版本号拼接  helloServer + group1 + version1
    public String getRpcServiceName() {
        return this.getServiceName() + this.getGroup() + this.getVersion();
    }
    /**
     * 获取rpc服务接口的全限定名(它包含了包名和类名的完整路径)
     * 通过 this.service 获取到保存的RPC服务实例对象。
     * 通过调用 getClass() 方法获取该实例对象的运行时类对象。
     * 通过调用 getInterfaces() 方法获取该运行时类对象所实现的所有接口。
     * 由于一个类可以实现多个接口,这里假设该RPC服务实例对象只实现了一个接口,因此我们使用 [0] 来获取第一个接口。
     * 最后,通过调用 getCanonicalName() 方法获取该接口的全限定名,即包含包名的类名。
     * 将获取到的全限定名作为服务名称进行返回
     * @return
     */
    public String getServiceName() {
        return this.service.getClass().getInterfaces()[0].getCanonicalName();
    }
}

remoting目录

RpcRequestTransport的内容也相对简单,这个是定义了一个接口,方便日后维护使用Netty来连接还是通过Socket来连接。本文采用Netty。

放在remoting目录下:

package com.cjd.remoting.transport;

import com.cjd.extension.SPI;
import com.cjd.remoting.dto.RpcRequest;

// 选择由socket 连接 还是netty连接
@SPI
public interface RpcRequestTransport {
    Object sendRpcRequest(RpcRequest rpcRequest);
}

此时出现了一个@SPI,它是 Java 中的一个注解,通常与 Apache Dubbo 这样的 RPC(远程过程调用)框架一起使用。它的作用是标识一个接口或抽象类为一个扩展点,允许框架在运行时动态地加载实现这个接口或抽象类的扩展。这个代码的内容在common包下,也放到后面讲。

最后剩下一个最重要的serviceProvider。

provider目录

目录结构如下

ServiceProvider:

package com.cjd.provider;


import com.cjd.config.RpcServiceConfig;

public interface ServiceProvider {
    //    添加rpc服务,它接收一个 RpcServiceConfig 参数,
//    包含了RPC服务的相关属性配置。通过该方法,可以将RPC服务添加到服务提供者中。
    void addService(RpcServiceConfig rpcServiceConfig);
    //    方法用于获取RPC服务对象。它接收一个 rpcServiceName 参数,表示RPC服务的名称。
//    通过该方法,可以根据服务名称获取对应的服务对象。
    Object getService(String rpcServiceName);
    //     方法用于发布RPC服务。它接收一个 RpcServiceConfig 参数,包含了RPC服务的相关属性配置。
//     通过该方法,可以将RPC服务发布到网络中,以供客户端调用
    void publishService(RpcServiceConfig rpcServiceConfig);

}

ZkServiceProviderImpl:

然后是ZkServiceProviderImpl复写了上面三个方法。

package com.cjd.provider.impl;

import com.cjd.config.RpcServiceConfig;
import com.cjd.enums.RpcErrorMessageEnum;
import com.cjd.enums.ServiceRegistryEnum;

import com.cjd.exception.RpcException;
import com.cjd.extension.ExtensionLoader;
import com.cjd.provider.ServiceProvider;
import com.cjd.registry.ServiceRegistry;
import com.cjd.remoting.transport.server.NettyRpcServer;
import lombok.extern.slf4j.Slf4j;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
public class ZkServiceProviderImpl implements ServiceProvider {
    /**
     * key: rpc服务名称(接口+版本+分组)
     * value: 服务的对象
     */
    private final Map<String, Object> serviceMap;
    //    存储已经注册的服务名称
    private final Set<String> registeredService;
    //    用于服务注册
    private final ServiceRegistry serviceRegistry;
    public ZkServiceProviderImpl() {
        // 用了 ConcurrentHashMap 来保证线程安全性
        serviceMap = new ConcurrentHashMap<>();
        // 用了 ConcurrentHashMap 的 newKeySet() 方法来创建一个线程安全的 Set 集合
        registeredService = ConcurrentHashMap.newKeySet();
        //先通过 getExtensionLoader 方法获取了一个 ServiceRegistry 类型的扩展加载器
        //然后getExtension 方法会根据传入的扩展名称,在扩展加载器中查找’zk‘的扩展实例。
        //最终,serviceRegistry 变量被赋值为找到的扩展实例
        serviceRegistry = ExtensionLoader.getExtensionLoader(ServiceRegistry.class).getExtension(ServiceRegistryEnum.ZK.getName());
    }

    /**
     * addService 方法实现了 ServiceProvider 接口的方法。
     * 它用于添加一个RPC服务。首先判断该服务是否已经注册,如果已经注册则直接返回。
     * 否则,将服务名称和服务对象放入 serviceMap 中,并将服务名称加入到 registeredService 集合中。
     * 最后,打印日志记录添加的服务名称和服务对象的接口信息
     *
     * @param rpcServiceConfig
     */
    @Override
    public void addService(RpcServiceConfig rpcServiceConfig) {
        //取服务的名称  服务名称由:服务名字+分组+版本号拼接
        String rpcServiceName = rpcServiceConfig.getRpcServiceName();
        if (registeredService.contains(rpcServiceName)) {
            return;
        }
        registeredService.add(rpcServiceName);
        //rpcServiceConfig.getService()返回全限定名(包名和类名的完整路径)
        serviceMap.put(rpcServiceName, rpcServiceConfig.getService());
        log.info("Add service: {} and interfaces:{}",
                rpcServiceName, rpcServiceConfig.getService().getClass().getInterfaces());
    }

    /**
     * 根据服务名称获取对应的服务对象。
     * 首先通过服务名称从 serviceMap 中获取服务对象,如果服务对象为空,则抛出 RpcException 异常。
     * 否则,返回服务对象
     *
     * @param rpcServiceName
     * @return
     */
    @Override
    public Object getService(String rpcServiceName) {
        Object service = serviceMap.get(rpcServiceName);
        if (service == null) {
            throw new RpcException(RpcErrorMessageEnum.SERVICE_CAN_NOT_BE_FOUND);
        }
        return service;
    }

    /**
     * 用于发布一个RPC服务。首先通过 InetAddress.getLocalHost().getHostAddress() 获取本机的IP地址。
     * 然后调用 addService 方法将服务添加到 serviceMap 中。
     * 最后,通过 serviceRegistry.registerService 方法将服务注册到服务注册中心,指定了服务名称和本机的 IP 地址。
     *
     * @param rpcServiceConfig
     */
    @Override
    public void publishService(RpcServiceConfig rpcServiceConfig) {
        try {
            String host = InetAddress.getLocalHost().getHostAddress();
            this.addService(rpcServiceConfig);
            serviceRegistry.registerService(rpcServiceConfig.getRpcServiceName(), new InetSocketAddress(host, NettyRpcServer.PORT));
        } catch (UnknownHostException e) {
            log.error("occur exception when getHostAddress", e);
        }
    }
}

这段代码的代码逻辑没什么难点,就是出现了ServiceRegistry和ServiceRegistryEnum。

关于Enum的暂时都不讲,因为不影响逻辑的走向,它只是为了方便后面维护代码进行的一些操作。

registry目录

现在讲讲ServiceRegistry,这一部分是关于注册的部分,是Zookeeper的内容。

目录结构如下

ServiceRegistry:

代码内容如下:实现了服务的注册。

package com.cjd.registry;

import com.cjd.extension.SPI;

import java.net.InetSocketAddress;
@SPI
public interface ServiceRegistry {
    void registerService(String rpcServiceName, InetSocketAddress inetSocketAddress);

}

ZkServiceRegistryImpl:

接下来是复写的内容,ZkServiceRegistryImpl

package com.cjd.registry.zk.impl;

import com.cjd.registry.ServiceRegistry;
import com.cjd.registry.zk.util.CuratorUtils;
import org.apache.curator.framework.CuratorFramework;

import java.net.InetSocketAddress;

public class ZkServiceRegistryImpl implements ServiceRegistry {
    //把服务注册到zookeeper上
    @Override
    public void registerService(String rpcServiceName, InetSocketAddress inetSocketAddress) {
        String serverPath = CuratorUtils.ZK_REGISTER_ROOT_PATH + "/" + rpcServiceName + inetSocketAddress.toString();
        CuratorFramework zkClient =CuratorUtils.getZkClient();
        CuratorUtils.createPersistentNode(zkClient,serverPath);
    }
}

CuratorUtils:

其中出现了一个 CuratorUtils工具类。

其代码内容如下:

package com.cjd.registry.zk.util;

import lombok.extern.slf4j.Slf4j;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.imps.CuratorFrameworkState;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;

import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Slf4j
public class CuratorUtils {
    // 定义了基础的休眠时间,单位为毫秒。这个值用于在重试连接或操作时进行休眠。
    private static final int BASE_SLEEP_TIME = 1000;
    //    定义了最大的重试次数。当与 ZooKeeper 进行连接或操作时,如果失败,最多尝试的次数为 3
    private static final int MAX_RETRIES = 3;
    //    定义了注册服务的根路径。在 ZooKeeper 中,所有注册的服务都会被放在这个根路径下。
    public static final String ZK_REGISTER_ROOT_PATH = "/my-rpc";
    //    定义了一个线程安全的 ConcurrentHashMap,用于存储服务名称与服务地址列表的映射关系。
//    每个服务名称对应一个注册的服务地址列表。
    private static final Map<String, List<String>> SERVICE_ADDRESS_MAP = new ConcurrentHashMap<>();
    //     定义了一个线程安全的 ConcurrentHashMap,用于存储已注册的路径。每个注册的服务地址对应一个注册的路径。
    private static final Set<String> REGISTERED_PATH_SET = ConcurrentHashMap.newKeySet();
    //    定义了一个 CuratorFramework 类型的静态变量 zkClient,用于管理与 ZooKeeper 的连接。
    private static CuratorFramework zkClient;
    //    定义了默认的 ZooKeeper 地址,格式为 host:port。
    private static final String DEFAULT_ZOOKEEPER_ADDRESS = "127.0.0.1:2181";

    private CuratorUtils() {
    }

    public static void createPersistentNode(CuratorFramework zkClient,String path){
        try {
            if (REGISTERED_PATH_SET.contains(path) || zkClient.checkExists().forPath(path) != null){
                log.info("The node already exists. The node is:[{}]", path);
            }else {
                // 创建一个持久化的节点,并且如果父节点不存在,则自动创建父节点
                zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath(path);
                log.info("The node was created successfully. The node is:[{}]", path);
            }
            REGISTERED_PATH_SET.add(path);
        } catch (Exception e) {
            log.error("create persistent node for path [{}] fail", path);
        }
    }

    public static List<String> getChildrenNodes(CuratorFramework zkClient, String rpcServiceName) {
        if (SERVICE_ADDRESS_MAP.containsKey(rpcServiceName)){
            return SERVICE_ADDRESS_MAP.get(rpcServiceName);
        }
        List<String> result = null;
        String servicePath =ZK_REGISTER_ROOT_PATH + "/" + rpcServiceName;
        try {
            result = zkClient.getChildren().forPath(servicePath);
            SERVICE_ADDRESS_MAP.put(rpcServiceName,result);
            // 调用 registerWatcher 方法,为指定服务名称的节点注册一个监听器,用于监听节点变化。
            registerWatcher(rpcServiceName,zkClient);
        } catch (Exception e) {
            log.error("get children nodes for path [{}] fail", servicePath);
        }
        return result;
    }

    private static void registerWatcher(String rpcServiceName, CuratorFramework zkClient) throws Exception {
        // 服务路径
        String servicePath = ZK_REGISTER_ROOT_PATH + "/" + rpcServiceName;
        // 创建一个 PathChildrenCache 对象,用于监控指定路径下的子节点变化。
        // zkClient 是与 ZooKeeper 建立连接的 CuratorFramework 对象。
        PathChildrenCache pathChildrenCache = new PathChildrenCache(zkClient, servicePath, true);
        // 创建一个 PathChildrenCacheListener 对象,用于定义子节点变化时的逻辑处理。
        PathChildrenCacheListener pathChildrenCacheListener = (curatorFramework, pathChildrenCacheEvent) -> {
            // 在子节点变化时,获取最新的子节点列表。
            List<String> serviceAddresses = curatorFramework.getChildren().forPath(servicePath);
            // 更新 SERVICE_ADDRESS_MAP 中指定服务名称的子节点列表
            SERVICE_ADDRESS_MAP.put(rpcServiceName, serviceAddresses);
        };
        // 将定义好的监听器 pathChildrenCacheListener 添加到 pathChildrenCache 的监听器列表中。
        pathChildrenCache.getListenable().addListener(pathChildrenCacheListener);
        // 启动 pathChildrenCache,开始监听指定路径下的子节点变化。
        pathChildrenCache.start();
    }

    public static void clearRegistry(CuratorFramework zkClient, InetSocketAddress inetSocketAddress) {
        // 使用流并行处理的方式遍历 REGISTERED_PATH_SET 中的所有注册路径。
        REGISTERED_PATH_SET.stream().parallel().forEach(p -> {
            try {
                // 检查当前注册路径是否以指定服务地址结尾,即是否为要清除的注册信息
                if (p.endsWith(inetSocketAddress.toString())) {
                    zkClient.delete().forPath(p);
                }
            } catch (Exception e) {
                log.error("clear registry for path [{}] fail", p);
            }
        });
        log.info("All registered services on the server are cleared:[{}]", REGISTERED_PATH_SET.toString());
    }

    public static CuratorFramework getZkClient() {
        // 使用默认的 ZooKeeper 地址。
        String zookeeperAddress = DEFAULT_ZOOKEEPER_ADDRESS;
        // 检查 zkClient 是否已经启动并处于连接状态。如果是,则直接返回已存在的 zkClient。
        if (zkClient != null && zkClient.getState() == CuratorFrameworkState.STARTED) {
            return zkClient;
        }
        // 如果 zkClient 未启动或未连接,则创建一个 CuratorFramework 对象。
        // 并使用指定的重试策略和 ZooKeeper 地址进行配置。休眠1000ms和3次重连
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(BASE_SLEEP_TIME, MAX_RETRIES);
        zkClient = CuratorFrameworkFactory.builder()
                // the server to connect to (can be a server list)
                .connectString(zookeeperAddress)
                .retryPolicy(retryPolicy)
                .build();
        zkClient.start();
        try {
            // 等待最多 30 秒,直到与 ZooKeeper 建立连接。如果在指定时间内未成功连接,则抛出运行时异常。
            if (!zkClient.blockUntilConnected(30, TimeUnit.SECONDS)) {
                throw new RuntimeException("Time out waiting to connect to ZK!");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return zkClient;
    }
}

到这里,从RpcScan一路嵌套下来的内容就截至了。此时可以点击目录,让我们回到服务端

从NettyRpcServer 开始引申

这里的内容就是关于网络连接的部分

NettyRpcServer的内容放在remoting目录下,上面也有看到,它的目录结构是这样的:

Server目录

在上文中,已经讲了RpcRequestTransport的内容。现在我们来看NettyRpcServer 的代码内容:

作用:启动Netty连接,创建心跳请求,确定解码编码方式,处理来自客户端的请求。

NettyRpcServer:

package com.cjd.remoting.transport.server;

import com.cjd.config.RpcServiceConfig;
import com.cjd.factory.SingletonFactory;
import com.cjd.provider.ServiceProvider;
import com.cjd.provider.impl.ZkServiceProviderImpl;
import com.cjd.remoting.transport.codec.RpcMessageDecoder;
import com.cjd.remoting.transport.codec.RpcMessageEncoder;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.net.InetAddress;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class NettyRpcServer {
    public static final int PORT = 9998;
    private final ServiceProvider serviceProvider = SingletonFactory.getInstance(ZkServiceProviderImpl.class);

    public void registerService(RpcServiceConfig rpcServiceConfig) {
        serviceProvider.publishService(rpcServiceConfig);
    }
    //@SneakyThrows 是 Lombok 库提供的一个注解,它可以简化在方法中抛出受检异常的处理
    @SneakyThrows
    public void start(){
        // InetAddress类代表了网络中的IP地址。
        // getLocalHost() 方法返回本地主机的实例,
        // 然后可以通过 getHostAddress() 方法获取本地主机的IP地址。
        String host = InetAddress.getLocalHost().getHostAddress();
        //1、创建bossGroup线程组:处理网络事件--连接事件
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        //2、创建workerGroup线程组:处理网络事件--读取事件,默认的nThreads是逻辑处理器*2
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            //3、创建服务端启动助手
            ServerBootstrap bootstrap = new ServerBootstrap();
            //4、设置bossGroup线程组和workerGroup线程组
            bootstrap.group(bossGroup,workerGroup)
                    //5、设置服务端通道实现为NIO
                    .channel(NioServerSocketChannel.class)
                    // 是否开启 TCP 底层心跳机制
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    // 6、通道初始化对象。当客户端第一次进行请求的时候才会进行初始化
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //7、向pipeline中添加自定义业务处理handler
                            ChannelPipeline p = ch.pipeline();
                            // 通过添加 IdleStateHandler,可以在连接的空闲状态下进行相应的处理,例如发送心跳包或关闭连接等操作。
                            p.addLast(new IdleStateHandler(30, 0, 30, TimeUnit.SECONDS));
                            // 添加编/解码器
                            p.addLast(new RpcMessageEncoder());
                            p.addLast(new RpcMessageDecoder());
                            // 业务处理handler
                            p.addLast(new NettyRpcServerHandler());
                        }
                    });
            // 8、绑定端口,同步等待绑定成功。sync()将异步改为同步
            ChannelFuture channelFuture = bootstrap.bind(host, PORT).sync();
            System.out.println("================Netty服务端启动成功=====================");
            // 9、监听通道的关闭状态
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            log.error("occur exception when start server:", e);
        } finally {
            log.error("shutdown bossGroup and workerGroup");
            //9、关闭线程池
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

整个代码中,ServiceProvider在上文已经讲到,SingletonFactory单例工厂在后文中会讲,此时出现的未知参数只有,RpcMessageEncoder、RpcMessageDecoder、NettyRpcServerHandler。

其中Encoder和Decoder是我们自己设计的一种编码解码的方式,用基础的String的方法也可以。

我们先来讲NettyRpcServerHandler。它是自定义的处理类,简单的说就是如何处理业务。

NettyRpcServerHandler:

package com.cjd.remoting.transport.server;

import com.cjd.enums.CompressTypeEnum;
import com.cjd.enums.RpcResponseCodeEnum;
import com.cjd.enums.SerializationTypeEnum;
import com.cjd.factory.SingletonFactory;
import com.cjd.remoting.constants.RpcConstants;
import com.cjd.remoting.dto.RpcMessage;
import com.cjd.remoting.dto.RpcRequest;
import com.cjd.remoting.dto.RpcResponse;
import com.cjd.remoting.handler.RpcRequestHandler;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.ReferenceCountUtil;

/**
 * 自定义业务处理类
 */
@Slf4j
public class NettyRpcServerHandler extends ChannelInboundHandlerAdapter {

    private final RpcRequestHandler rpcRequestHandler;

    public NettyRpcServerHandler() {
        this.rpcRequestHandler = SingletonFactory.getInstance(RpcRequestHandler.class);
    }


    /**
     *
     * @param ctx
     * @param msg RpcRequest
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 处理接收到的消息
        System.out.println("Received message: " + msg);
        try {
            //客户端发送的时候,是先把RpcRequest封装成RpcMessage进行发送
            if (msg instanceof RpcMessage){
                log.info("Server receive msg:[{}]",msg);
                //读取msg的MessageType类型,心跳请求还是
                byte messageType = ((RpcMessage) msg).getMessageType();
                //新建一个rpcMessage作为响应消息
                RpcMessage rpcMessage = new RpcMessage();
                //设置序列化方式为kryo /code:0x01
                rpcMessage.setCodec(SerializationTypeEnum.KRYO.getCode());
                //设置压缩方式为gzip  /code:0x01
                rpcMessage.setCompress(CompressTypeEnum.GZIP.getCode());
                //如果是心跳请求
                if (messageType == RpcConstants.HEARTBEAT_REQUEST_TYPE){
                    //设置类型为心跳响应
                    rpcMessage.setMessageType(RpcConstants.HEARTBEAT_RESPONSE_TYPE);
                    //设置数据为 pong
                    rpcMessage.setData(RpcConstants.PONG);
                }else {
                    //否则就是客户端传来的请求调用方法
                     RpcRequest rpcRequest = (RpcRequest) ((RpcMessage) msg).getData();
                    //将所需的方法执行,并返回方法的结果。
                    Object result =rpcRequestHandler.handle(rpcRequest);
                    log.info(String.format("server get result: %s", result.toString()));
                    rpcMessage.setMessageType(RpcConstants.RESPONSE_TYPE);
                    if (ctx.channel().isActive() && ctx.channel().isWritable()){
                        RpcResponse<Object> response =RpcResponse.success(result,rpcRequest.getRequestId());
                        rpcMessage.setData(response);
                    }else {
                        RpcResponse<Object> response =RpcResponse.fail(RpcResponseCodeEnum.FAIL);
                        rpcMessage.setData(response);
                        log.error("not writable now, message dropped");
                    }
                }
                // 读取完毕,给客户端响应。将响应消息写入管道,并在写入失败时关闭通道。
                ctx.writeAndFlush(rpcMessage).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            }
        }finally {
            // 释放接收到的消息的资源。
            ReferenceCountUtil.release(msg);
        }
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error("server catch exception");
        // 异常处理逻辑
        cause.printStackTrace();
        ctx.close();
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        /**
         * 在 Netty 中,空闲状态事件是由 IdleStateHandler 处理器生成的。
         * IdleStateHandler 是一个用于检测连接空闲状态的处理器,它可以根据指定的时间间隔检测连接的读或写操作是否超时。
         * 当连接的读或写操作超过指定的时间间隔时,IdleStateHandler 会触发一个空闲状态事件。
         * IdleStateEvent 包含了空闲状态的具体信息,
         * 例如空闲状态类型(IdleState)和触发该状态的事件源(Channel)。
         * 通过检查空闲状态事件的类型,可以判断连接的具体空闲状态,例如读取空闲或写入空闲。
         */
        if (evt instanceof IdleStateEvent) {
            IdleState state = ((IdleStateEvent) evt).state();
            // 读取操作超时
            if (state == IdleState.READER_IDLE || state== IdleState.ALL_IDLE) {
                log.info("idle check happen, so close the connection");
                ctx.close();
            }
        } else {
            // 如果事件不是空闲状态事件,
            // 即无法被当前处理器处理,将调用父类的 userEventTriggered 方法,将事件传递给处理器链中的下一个处理器来处理。
            super.userEventTriggered(ctx, evt);
        }
    }

}

此时出现了几个新的函数名。例如RpcRequestHandler、RpcMessage、RpcRequest、RpcResponse、RpcConstants。

constants目录

在NettyRpcServer标题下,我们可以看到remoting目录下有一个constants目录,里面只包含一个文件,那就是RpcConstants。它主要是定义了RPC的一些常量,用于标识和处理RPC请求。

代码如下:

package com.cjd.remoting.constants;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;


public class RpcConstants {
    //魔数,用于验证RpcMessage的有效性,由grpc四个字节组成
    public static final byte[] MAGIC_NUMBER = {(byte) 'g', (byte) 'r', (byte) 'p', (byte) 'c'};
    //    默认的编码解码方式 utf-8
    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
    // RPC版本号
    public static final byte VERSION = 1;
    //    RPC消息总长度,包括消息头和消息体,指定为16字节
    public static final byte TOTAL_LENGTH = 16;
    //    RPC消息的类型标识,分别表示请求消息和响应消息
    public static final byte REQUEST_TYPE = 1;
    public static final byte RESPONSE_TYPE = 2;
    //ping 心跳请求
    public static final byte HEARTBEAT_REQUEST_TYPE = 3;
    //pong 心跳响应
    public static final byte HEARTBEAT_RESPONSE_TYPE = 4;
    //    RPC消息头的总长度,是16个字节
    public static final int HEAD_LENGTH = 16;
    //心跳消息的内容,心跳请求ping和心跳响应pong
    public static final String PING = "ping";
    public static final String PONG = "pong";
    //    最大帧长度,限制消息大小,指定为8MB
    public static final int MAX_FRAME_LENGTH = 8 * 1024 * 1024;
}

其中有不少东西可能大家还看不懂,例如魔数、RPC消息总长度之类,这些都与后面的编码RpcMessageEncoder和解码RpcMessageDecoder的方式有关。放到后面讲。

dto目录

RpcMessage:

首先是RpcMessage,整个是用于统一区分是RpcRequest还是RpcResponse还是心跳请求ping/pong。

代码如下:

package com.cjd.dto;

import lombok.*;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
@ToString
public class RpcMessage {
    /**
     * rpc message type
     * 参照 RpcConstants
     */
    private byte messageType;
    /**
     * serialization type
     */
    private byte codec;
    /**
     * compress type
     */
    private byte compress;
    /**
     * request id
     */
    private int requestId;
    /**
     * request data
     */
    private Object data;
}

RpcRequest:

然后是RpcRequest,这就是客户端发来的请求,有请求的方法名,版本之类的参数。

代码如下:

package com.cjd.dto;

import lombok.*;

import java.io.Serializable;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@ToString
public class RpcRequest implements Serializable {
    private static final long serialVersionUID = 1905122041950251207L;
    private String requestId;
    private String interfaceName;
    private String methodName;
    private Object[] parameters;
    private Class<?>[] paramTypes;
    private String version;
    private String group;

    public String getRpcServiceName() {
        return this.getInterfaceName() + this.getGroup() + this.getVersion();
    }
}

RpcResponse:

接着是RpcResponse,主要是来自服务端的反馈。

代码如下:

package com.cjd.dto;

import com.cjd.enums.RpcResponseCodeEnum;
import lombok.*;

import java.io.Serializable;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
@ToString
public class RpcResponse<T> implements Serializable {
    private static final long serialVersionUID = 715745410605631233L;
    private String requestId;
    /**
     * response code
     * 200
     * 500
     */
    private Integer code;
    /**
     * response message
     * The remote call is successful
     * The remote call is fail
     */
    private String message;

    private T data;

    public static <T> RpcResponse<T> success(T data, String requestId) {
        RpcResponse<T> response = new RpcResponse<>();
        response.setCode(RpcResponseCodeEnum.SUCCESS.getCode());
        response.setMessage(RpcResponseCodeEnum.SUCCESS.getMessage());
        response.setRequestId(requestId);
        if (null != data) {
            response.setData(data);
        }
        return response;
    }

    public static <T> RpcResponse<T> fail(RpcResponseCodeEnum rpcResponseCodeEnum) {
        RpcResponse<T> response = new RpcResponse<>();
        response.setCode(rpcResponseCodeEnum.getCode());
        response.setMessage(rpcResponseCodeEnum.getMessage());
        return response;
    }

}

其中有一个RpcResponseCodeEnum,暂时也不管。后续统一讲enums目录下的文件。

handler目录

接着到RpcRequestHandler,作用:调用使用方法!

代码如下:

package com.cjd.remoting.handler;

import com.cjd.exception.RpcException;
import com.cjd.factory.SingletonFactory;
import com.cjd.provider.ServiceProvider;
import com.cjd.provider.impl.ZkServiceProviderImpl;
import com.cjd.remoting.dto.RpcRequest;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

@Slf4j
public class RpcRequestHandler {
    //   用于提供RPC服务的管理和查找功能。
    private final ServiceProvider serviceProvider;


    public RpcRequestHandler() {
//  getInstance(ZkServiceProviderImpl.class) 创建了一个 ZkServiceProviderImpl 的实例,
//  并将其赋值给 serviceProvider 成员变量。这里使用了 SingletonFactory 类来创建单例对象,
//  确保只有一个 ZkServiceProviderImpl 实例被创建和使用
        serviceProvider =  SingletonFactory.getInstance(ZkServiceProviderImpl.class);
    }

    // 处理接收到的rpc请求,这段代码的作用是根据 RPC 请求中的服务名称,从 serviceProvider 中获取对应的服务实例,
    // 并调用目标方法进行处理。最终返回方法的执行结果。
    public Object handle(RpcRequest rpcRequest) {
        Object service = serviceProvider.getService(rpcRequest.getRpcServiceName());
        return invokeTargetMethod(rpcRequest, service);
    }

    private Object invokeTargetMethod(RpcRequest rpcRequest, Object service) {
        Object result;
        try {
            // 通过反射获取服务实例中指定方法名和参数类型的 Method 对象。
            Method method = service.getClass().getMethod(rpcRequest.getMethodName(),rpcRequest.getParamTypes());
//            使用反射调用获取到的方法,并传入对应的参数,执行方法,并将结果赋值给 result 变量。
            result = method.invoke(service,rpcRequest.getParameters());
            log.info("service:[{}] successful invoke method:[{}]", rpcRequest.getInterfaceName(), rpcRequest.getMethodName());
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            throw new RpcException(e.getMessage(), e);
        }
        return result;
    }
}

codec目录

截至到这里,从NettyRpcServerHandler引申出的所有函数都出现了,现在要回到NettyRpcServer中还没完成的解码和编码。

一共有两个文件,一个是RpcMessageDecoder、另一个是RpcMessageEncoder

RpcMessageEncoder:

这个代码的目的是对接收到的RpcMessage进行编码。

编码的格式如下所示:

 *   0     1     2     3     4        5     6     7     8         9          10      11     12  13  14   15 16
 *   +-----+-----+-----+-----+--------+----+----+----+------+-----------+-------+----- --+-----+-----+-------+
 *   |   magic   code        |version | full length         | messageType| codec|compress|    RequestId       |
 *   +-----------------------+--------+---------------------+-----------+-----------+-----------+------------+
 *   |                                                                                                       |
 *   |                                         body                                                          |
 *   |                                                                                                       |
 *   |                                        ... ...                                                        |
 *   +-------------------------------------------------------------------------------------------------------+
 * 4B  magic code(魔法数)   1B version(版本)   4B full length(消息长度)    1B messageType(消息类型)
 * 1B compress(压缩类型) 1B codec(序列化类型)    4B  requestId(请求的Id)
 * body(object类型数据)
package com.cjd.codec;

import com.cjd.compress.Compress;
import com.cjd.constants.RpcConstants;
import com.cjd.dto.RpcMessage;
import com.cjd.enums.CompressTypeEnum;
import com.cjd.enums.SerializationTypeEnum;
import com.cjd.extension.ExtensionLoader;
import com.cjd.serialize.Serializer;
import com.cjd.server.NettyRpcServer;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import lombok.extern.slf4j.Slf4j;

import java.io.Serializable;
import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
public class RpcMessageEncoder extends MessageToByteEncoder<RpcMessage> {
    /**
     * 用于实现原子性的整数操作
     * AtomicInteger 是 Java 提供的原子性整数操作类,
     * 可以保证对整数的操作是原子性的,不会受到并发访问的影响。
     * 通过 ATOMIC_INTEGER 对象,可以实现线程安全的整数操作,如递增、递减、比较并替换等操作。
     */
    private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0);

    /**
     * 将rpc对象编码为字节流
     * @param ctx
     * @param rpcMessage
     * @param out
     */
    @Override
    protected void encode(ChannelHandlerContext ctx, RpcMessage rpcMessage, ByteBuf out){
        try {
            // 魔法数
            out.writeBytes(RpcConstants.MAGIC_NUMBER);
            // 版本
            out.writeByte(RpcConstants.VERSION);
            // 留出 4 个字节的空间,用于写入完整长度的值。
            out.writerIndex(out.writerIndex() + 4);
            // 获取消息类型
            byte messageType = rpcMessage.getMessageType();
            // 将消息类型写入输出字节流。
            out.writeByte(messageType);
            // 将序列化方式写入输出字节流。
            out.writeByte(rpcMessage.getCodec());
            // 将压缩方式写入输出字节流。
            out.writeByte(CompressTypeEnum.GZIP.getCode());
            // 将自增的整数写入输出字节流。
            out.writeInt(ATOMIC_INTEGER.getAndIncrement());
            byte[] bodyBytes = null;
            // 初始化完整长度为协议头的长度。  RPC消息头的总长度,是16个字节
            int fullLength = RpcConstants.HEAD_LENGTH;
            // 如果消息类型不是心跳请求或心跳响应,则需要对消息体进行序列化和压缩。
            if (messageType != RpcConstants.HEARTBEAT_REQUEST_TYPE
                    && messageType != RpcConstants.HEARTBEAT_RESPONSE_TYPE) {
                String codecName = SerializationTypeEnum.getName(rpcMessage.getCodec());
                log.info("codec name: [{}] ", codecName);
                //  获取指定序列化方式的序列化器。
                Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension(codecName);
                // 将消息体对象进行序列化,得到字节数组。
                bodyBytes = serializer.serialize(rpcMessage.getData());
                // 获取指定压缩方式的压缩器。
                String compressName = CompressTypeEnum.getName(rpcMessage.getCompress());
                Compress compress = ExtensionLoader.getExtensionLoader(Compress.class)
                        .getExtension(compressName);
                // 压缩消息体的字节数组。
                bodyBytes = compress.compress(bodyBytes);
                // 更新完整长度,加上消息体的长度。
                fullLength += bodyBytes.length;
            }
            if (bodyBytes != null){
                // 将压缩后的消息体写入输出字节流。
                out.writeBytes(bodyBytes);
            }
            // 记录当前的写索引位置
            int writeIndex = out.writerIndex();
            // 将写索引位置移动到完整长度字段的位置。
            out.writerIndex(writeIndex - fullLength + RpcConstants.MAGIC_NUMBER.length + 1);
            //  写入完整长度字段。
            out.writeInt(fullLength);
            // 恢复写索引位置。
            out.writerIndex(writeIndex);
        } catch (Exception e) {
            log.error("Encode request error!", e);
        }
    }
}

RpcMessageDecoder:

这一步的作用主要就是解码:

package com.cjd.codec;

import com.cjd.compress.Compress;
import com.cjd.constants.RpcConstants;
import com.cjd.dto.RpcMessage;
import com.cjd.dto.RpcRequest;
import com.cjd.dto.RpcResponse;
import com.cjd.enums.CompressTypeEnum;
import com.cjd.enums.SerializationTypeEnum;
import com.cjd.extension.ExtensionLoader;
import com.cjd.serialize.Serializer;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import lombok.extern.slf4j.Slf4j;

import java.util.Arrays;

@Slf4j
// 继承了 LengthFieldBasedFrameDecoder 类,通过设置不同的参数,实现了对帧的长度字段进行解析和处理
public class RpcMessageDecoder extends LengthFieldBasedFrameDecoder {

    public RpcMessageDecoder() {
        // 调用了带参构造方法并传入默认参数。
        this(RpcConstants.MAX_FRAME_LENGTH, 5, 4, -9, 0);
    }

    /**
     * @param maxFrameLength  表示帧的最大长度 8*1024*1024=8388608
     * @param lengthFieldOffset  表示长度字段的起始偏移量 /魔法数是4B 版本是1B 所以起始偏移5B
     * @param lengthFieldLength  表示长度字段的长度  /字段长度是4B,所以是4
     * @param lengthAdjustment  表示长度字段的调整值  /需要先读取前面的9B,(fullLength-9).所以是-9
     * @param initialBytesToStrip 表示从解码帧中跳过的字节数  /检查魔法数和版本数,不跳过任何字段,所以是0
     */
    public RpcMessageDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
                             int lengthAdjustment, int initialBytesToStrip) {
        super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip);
    }

    @Override
    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        // 调用父类的 decode 方法对字节缓冲区中的数据进行解码。
        Object decoded = super.decode(ctx, in);
        if (decoded instanceof ByteBuf) {
            /**
             * 在 LengthFieldBasedFrameDecoder 中,解码方法 decode 会返回解码后的对象。
             * 如果解码成功,返回的对象是一个 ByteBuf,它包含了解码后的帧数据。
             * 但是,如果解码失败或者还没有接收到足够的数据来构成一个完整的帧,解码方法可能会返回其他类型的对象,比如 null。
             */
            ByteBuf frame = (ByteBuf) decoded;
            // 进入解码帧的逻辑。
            if (frame.readableBytes() >= RpcConstants.TOTAL_LENGTH) {
                try {
                    // 调用 decodeFrame 方法对帧进行解码。
                    return decodeFrame(frame);
                } catch (Exception e) {
                    log.error("Decode frame error!", e);
                    throw e;
                } finally {
                    // 释放帧的资源。
                    frame.release();
                }
            }

        }
        return decoded;
    }

    // 用于解码帧数据并构建 RpcMessage 对象。
    private Object decodeFrame(ByteBuf in) {
        // 检查帧数据的魔数是否正确。
        checkMagicNumber(in);
        // 检查帧数据的版本是否正确
        checkVersion(in);
        // 读取完整长度字段的值。
        int fullLength = in.readInt();
        // 读取消息类型字段的值 pong/ping/request
        byte messageType = in.readByte();
//        读取编解码器类型字段的值。
        byte codecType = in.readByte();
//        读取压缩类型字段的值
        byte compressType = in.readByte();
//        读取请求 ID 字段的值
        int requestId = in.readInt();
//        创建一个 RpcMessage 对象,并设置消息类型、编解码器类型和请求 ID
        RpcMessage rpcMessage = RpcMessage.builder()
                .codec(codecType)
                .requestId(requestId)
                .messageType(messageType).build();
        if (messageType == RpcConstants.HEARTBEAT_REQUEST_TYPE) {
            rpcMessage.setData(RpcConstants.PING);
            return rpcMessage;
        }
        if (messageType == RpcConstants.HEARTBEAT_RESPONSE_TYPE) {
            rpcMessage.setData(RpcConstants.PONG);
            return rpcMessage;
        }
//        计算消息体的长度
        int bodyLength = fullLength - RpcConstants.HEAD_LENGTH;
//        消息体长度大于 0,则读取字节数据,并根据压缩类型进入解码和反序列化逻辑
        if (bodyLength > 0) {
//            创建一个字节数组,用于存储消息体数据
            byte[] bs = new byte[bodyLength];
//             从输入字节缓冲区中读取指定长度的字节数据
            in.readBytes(bs);
            // 根据压缩类型字段获取对应的压缩算法名称
            String compressName = CompressTypeEnum.getName(compressType);
//            根据压缩算法名称获取对应的压缩器实例。
            Compress compress = ExtensionLoader.getExtensionLoader(Compress.class)
                    .getExtension(compressName);
            //解压
            bs = compress.decompress(bs);
            // 根据编解码器类型字段获取对应的编解码器名称
            String codecName = SerializationTypeEnum.getName(rpcMessage.getCodec());
            log.info("codec name: [{}] ", codecName);
            // 根据编解码器名称获取对应的序列化器实例。
            Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class)
                    .getExtension(codecName);
            // 如果消息类型是请求类型,则使用序列化器将字节数组反序列化为 RpcRequest 对象,并将其设置为消息数据
            if (messageType == RpcConstants.REQUEST_TYPE) {
                RpcRequest tmpValue = serializer.deserialize(bs, RpcRequest.class);
                rpcMessage.setData(tmpValue);
            } else {
                // 如果消息类型是响应类型,则使用序列化器将字节数组反序列化为 RpcResponse 对象,并将其设置为消息数据
                RpcResponse tmpValue = serializer.deserialize(bs, RpcResponse.class);
                rpcMessage.setData(tmpValue);
            }
        }
        return rpcMessage;
    }

    private void checkMagicNumber(ByteBuf in) {
        // read the first 4 bit, which is the magic number, and compare
        int len = RpcConstants.MAGIC_NUMBER.length;
        byte[] tmp = new byte[len];
        in.readBytes(tmp);
        for (int i = 0; i < len; i++) {
            if (tmp[i] != RpcConstants.MAGIC_NUMBER[i]) {
                throw new IllegalArgumentException("Unknown magic code: " + Arrays.toString(tmp));
            }
        }
    }

    private void checkVersion(ByteBuf in) {
        // read the version and compare
        byte version = in.readByte();
        if (version != RpcConstants.VERSION) {
            throw new RuntimeException("version isn't compatible" + version);
        }
    }
}

这里面出现的序列化和压缩方式放到后面来讲。

截至这里,关于服务端的所有代码已经结束了。

致谢

代码取自GitHub - Snailclimb/guide-rpc-framework: A custom RPC framework implemented by Netty+Kyro+Zookeeper.(一款基于 Netty+Kyro+Zookeeper 实现的自定义 RPC 框架-附详细实现过程和相关教程。)

本文从个人学习感悟出发,理清从服务端开始,一步步到RPC框架的所有脉络。希望与大家广泛交流

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值