服务端
服务端调用逻辑:
服务端: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);
}
}
}
这里面出现的序列化和压缩方式放到后面来讲。
截至这里,关于服务端的所有代码已经结束了。
致谢
本文从个人学习感悟出发,理清从服务端开始,一步步到RPC框架的所有脉络。希望与大家广泛交流