服务发布、订阅及远程通信
项目结构
- rpc-provider,服务提供者。负责发布 RPC 服务,接收和处理 RPC 请求。
- rpc-consumer,服务消费者。使用动态代理发起 RPC 远程调用,帮助使用者来屏蔽底层网络通信的细节。
- rpc-registry,注册中心模块。提供服务注册、服务发现、负载均衡的基本功能。
- rpc-protocol,网络通信模块。包含 RPC 协议的编解码器、序列化和反序列化工具等。
- rpc-core,基础类库。提供通用的工具类以及模型定义,例如 RPC 请求和响应类、RPC 服务元数据类等。
- rpc-facade,RPC 服务接口。包含服务提供者需要对外暴露的接口,本模块主要用于模拟真实 RPC 调用的测试。
如何使用
举个例子:
// rpc-facade # HelloFacade public interface HelloFacade { String hello(String name); } // rpc-provider # HelloFacadeImpl @RpcService(serviceInterface = HelloFacade.class, serviceVersion = "1.0.0") public class HelloFacadeImpl implements HelloFacade { @Override public String hello(String name) { return "hello" + name; } } // rpc-consumer # HelloController @RestController public class HelloController { @RpcReference(serviceVersion = "1.0.0", timeout = 3000) private HelloFacade helloFacade; @RequestMapping(value = "/hello", method = RequestMethod.GET) public String sayHello() { return helloFacade.hello("mini rpc"); } }
rpc-provider 和 rpc-consumer 两个模块独立启动,模拟服务端和客户端。rpc-provider 通过 @RpcService 注解暴露 RPC 服务 HelloFacade,rpc-consumer 通过 @RpcReference 注解引用 HelloFacade 服务并发起调用,基本与我们常用的 RPC 框架使用方式保持一致。
windows环境下安装zookeeper
先准备安装包,在Apache官网下载(地址:https://zookeeper.apache.org/releases.html)。
选择任意下载地址:
下载成功以后解压到指定目录。
将conf目录下的zoo_sample.cfg文件,复制一份,重命名为zoo.cfg
修改zoo.cfg配置文件,将dataDir=/tmp/zookeeper修改成zookeeper安装目录所在的data文件夹(需要在安装目录下面新建一个空的data文件夹和log文件夹),再添加一条添加数据日志的配置,如下图
双击zkServer.cmd,启动程序,出现以下样式,则启动成功
服务发布与订阅
服务提供者发布服务
服务提供者 rpc-provider 需要完成哪些事情呢?主要分为四个核心流程:
- 服务提供者启动服务,并暴露服务端口;
- 启动时扫描需要对外发布的服务,并将服务元数据信息发布到注册中心;
- 接收 RPC 请求,解码后得到请求消息;
- 提交请求至自定义线程池进行处理,并将处理结果写回客户端。
服务提供者启动
服务提供者启动的配置方式基本是固定模式,也是从引导器 Bootstrap 开始入手。我们首先看下服务提供者的启动实现,代码如下所示:
private void startRpcServer() throws Exception { this.serverAddress = InetAddress.getLocalHost().getHostAddress(); EventLoopGroup boss = new NioEventLoopGroup(); EventLoopGroup worker = new NioEventLoopGroup(); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { } }) .childOption(ChannelOption.SO_KEEPALIVE, true); ChannelFuture channelFuture = bootstrap.bind(this.serverAddress, this.serverPort).sync(); log.info("server addr {} started on port {}", this.serverAddress, this.serverPort); channelFuture.channel().closeFuture().sync(); } finally { boss.shutdownGracefully(); worker.shutdownGracefully(); } }
服务提供者采用的是主从 Reactor 线程模型,启动过程包括配置线程池、Channel 初始化、端口绑定三个步骤,我们暂时先不关注 Channel 初始化中自定义的业务处理器 Handler 是如何设计和实现的。
参数配置
服务提供者启动需要配置一些参数,我们不应该把这些参数固定在代码里,而是以命令行参数或者配置文件的方式进行输入。我们可以使用 Spring Boot 的 @ConfigurationProperties 注解很轻松地实现配置项的加载,并且可以把相同前缀类型的配置项自动封装成实体类。接下来我们为服务提供者提供参数映射的对象:
@Data @ConfigurationProperties(prefix = "rpc") public class RpcProperties { /** * 服务暴露的端口 */ private int servicePort; /** * 注册中心的地址 */ private String registryAddr; /** * 注册中心的类型 */ private String registryType; }
我们一共提取了三个参数,分别为服务暴露的端口 servicePort、注册中心的地址 registryAddr 和注册中心的类型 registryType。@ConfigurationProperties 注解最经典的使用方式就是通过 prefix 属性指定配置参数的前缀,默认会与全局配置文件 application.properties 或者 application.yml 中的参数进行一一绑定。如果你想自定义一个配置文件,可以通过 @PropertySource 注解指定配置文件的位置。下面我们在 rpc-provider 模块的 resources 目录下创建全局配置文件 application.properties,并配置以上三个参数:
rpc.servicePort=2781 rpc.registryType=ZOOKEEPER rpc.registryAddr=127.0.0.1:2181
application.properties 配置文件中的属性必须和实体类的成员变量是一一对应的,可以采用以下常用的命名规则,例如驼峰命名 rpc.servicePort=2781;或者虚线 - 分割的方式 rpc.service-port=2781;以及大写加下划线的形式 RPC_Service_Port,建议在环境变量中使用。@ConfigurationProperties 注解还可以支持更多复杂结构的配置,并且可以 Validation 功能进行参数校验,如果有兴趣可以自行研究。
有了 RpcProperties 实体类,我们接下来应该如何使用呢?如果只配置 @ConfigurationProperties 注解,Spring 容器并不能获取配置文件的内容并映射为对象,这时 @EnableConfigurationProperties 注解就登场了。@EnableConfigurationProperties 注解的作用就是将声明 @ConfigurationProperties 注解的类注入为 Spring 容器中的 Bean。具体用法如下:
@Configuration @EnableConfigurationProperties(RpcProperties.class) public class RpcProviderAutoConfiguration { @Resource private RpcProperties rpcProperties; @Bean public RpcProvider init() throws Exception { RegistryType type = RegistryType.valueOf(rpcProperties.getRegistryType());
//构建单例注册服务 RegistryService serviceRegistry = RegistryFactory.getInstance(rpcProperties.getRegistryAddr(), type); return new RpcProvider(rpcProperties.getServicePort(), serviceRegistry); } }
我们通过 @EnableConfigurationProperties 注解使得 RpcProperties 生效,并通过 @Configuration 和 @Bean 注解自定义了 RpcProvider 的生成方式。@Configuration 主要用于定义配置类,配置类内部可以包含多个 @Bean 注解的方法,可以替换传统 XML 的定义方式。被 @Bean 注解的方法会返回一个自定义的对象,@Bean 注解会将这个对象注册为 Bean 并装配到 Spring 容器中,@Bean 比 @Component 注解的自定义功能更强。
至此,我们服务提供者的准备工作就完成了,下面添加 Spring Boot 的 main 方法,如下所示,然后尝试启动下 rpc-provider 模块。
@EnableConfigurationProperties @SpringBootApplication public class RpcProviderApplication { public static void main(String[] args) { SpringApplication.run(RpcProviderApplication.class, args); } }
发布服务
在服务提供者启动时,我们需要思考一个核心问题,服务提供者需要将服务发布到注册中心,怎么知道哪些服务需要发布呢?服务提供者需要定义发布服务类型、服务版本等属性,主流的 RPC 框架都采用 XML 文件或者注解的方式进行定义。以注解的方式暴露服务现在最为常用,省去了很多烦琐的 XML 配置过程。例如 Dubbo 框架中使用 @Service 注解替代 dubbo:service 的定义方式,服务消费者则使用 @Reference 注解替代 dubbo:reference。接下来我们看看作为服务提供者,如何通过注解暴露服务,首先给出我们自定义的 @RpcService 注解定义:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Component public @interface RpcService { /** * 服务类型 * @return */ Class<?> serviceInterface() default Object.class; /** * 服务版本 * @return */ String serviceVersion() default "1.0"; }
@RpcService 提供了两个必不可少的属性:服务类型 serviceInterface 和服务版本 serviceVersion,服务消费者必须指定完全一样的属性才能正确调用。有了 @RpcService 注解之后,我们就可以在服务实现类上使用它,@RpcService 注解本质上就是 @Component,可以将服务实现类注册成 Spring 容器所管理的 Bean,那么 serviceInterface、serviceVersion 的属性值怎么才能和 Bean 关联起来呢?这就需要我们就 Bean 的生命周期以及 Bean 的可扩展点有所了解。
Spring 的 BeanPostProcessor 接口给提供了对 Bean 进行再加工的扩展点,BeanPostProcessor 常用于处理自定义注解。自定义的 Bean 可以通过实现 BeanPostProcessor 接口,在 Bean 实例化的前后加入自定义的逻辑处理。如下所示,我们通过 RpcProvider 实现 BeanPostProcessor 接口,来实现对 声明 @RpcService 注解服务的自定义处理。
/** * Spring 的 BeanPostProcessor 接口给提供了对 Bean 进行再加工的扩展点,BeanPostProcessor 常用于处理自定义注解。 * 自定义的 Bean 可以通过实现 BeanPostProcessor 接口,在 Bean 实例化的前后加入自定义的逻辑处理。 */ @Slf4j public class RpcProvider implements InitializingBean, BeanPostProcessor { private String serverAddress; private final int serverPort; private final RegistryService serviceRegistry; private final Map<String, Object> rpcServiceMap = new HashMap<>(); public RpcProvider(int serverPort, RegistryService serviceRegistry) { this.serverPort = serverPort; this.serviceRegistry = serviceRegistry; } @Override public void afterPropertiesSet() { new Thread(() -> { try { startRpcServer(); } catch (Exception e) { log.error("start rpc server error.", e); } }).start(); } private void startRpcServer() throws Exception { this.serverAddress = InetAddress.getLocalHost().getHostAddress(); EventLoopGroup boss = new NioEventLoopGroup(); EventLoopGroup worker = new NioEventLoopGroup(); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline() .addLast(new MiniRpcEncoder()) .addLast(new MiniRpcDecoder()) .addLast(new RpcRequestHandler(rpcServiceMap)); } }) .childOption(ChannelOption.SO_KEEPALIVE, true); ChannelFuture channelFuture = bootstrap.bind(this.serverAddress, this.serverPort).sync(); log.info("server addr {} started on port {}", this.serverAddress, this.serverPort); channelFuture.channel().closeFuture().sync(); } finally { boss.shutdownGracefully(); worker.shutdownGracefully(); } } /** * 对所有初始化完成后的 Bean 进行扫描。 * 如果 Bean 包含 @RpcService 注解,那么通过注解读取服务的元数据信息并构造出 ServiceMeta 对象,接下来准备将服务的元数据信息发布至注册中心。 * 此外,RpcProvider 还维护了一个 rpcServiceMap,存放服务初始化后所对应的 Bean,rpcServiceMap 起到了缓存的角色, * 在处理 RPC 请求时可以直接通过 rpcServiceMap 拿到对应的服务进行调用。 * @param bean * @param beanName * @return * @throws BeansException */ @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { RpcService rpcService = bean.getClass().getAnnotation(RpcService.class); if (rpcService != null) { String serviceName = rpcService.serviceInterface().getName(); String serviceVersion = rpcService.serviceVersion(); try { ServiceMeta serviceMeta = new ServiceMeta(); serviceMeta.setServiceAddr(serverAddress); serviceMeta.setServicePort(serverPort); serviceMeta.setServiceName(serviceName); serviceMeta.setServiceVersion(serviceVersion); serviceRegistry.register(serviceMeta); rpcServiceMap.put(RpcServiceHelper.buildServiceKey(serviceMeta.getServiceName(), serviceMeta.getServiceVersion()), bean); } catch (Exception e) { log.error("failed to register service {}#{}", serviceName, serviceVersion, e); } } return bean; } }
RpcProvider 重写了 BeanPostProcessor 接口的 postProcessAfterInitialization 方法,对所有初始化完成后的 Bean 进行扫描。如果 Bean 包含 @RpcService 注解,那么通过注解读取服务的元数据信息并构造出 ServiceMeta 对象,接下来准备将服务的元数据信息发布至注册中心,注册中心的实现后续再细说。此外,RpcProvider 还维护了一个 rpcServiceMap,存放服务初始化后所对应的 Bean,rpcServiceMap 起到了缓存的角色,在处理 RPC 请求时可以直接通过 rpcServiceMap 拿到对应的服务进行调用。
服务消费者订阅服务
与服务提供者不同的是,服务消费者并不是一个常驻的服务,每次发起 RPC 调用时它才会去选择向哪个远端服务发送数据。所以服务消费者的实现要复杂一些,对于声明 @RpcReference 注解的成员变量,我们需要构造出一个可以真正进行 RPC 调用的 Bean,然后将它注册到 Spring 的容器中。
首先我们看下 @RpcReference 注解的定义,代码如下所示:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @Autowired public @interface RpcReference { String serviceVersion() default "1.0"; String registryType() default "ZOOKEEPER"; String registryAddress() default "127.0.0.1:2181"; long timeout() default 5000; }
@RpcReference 注解提供了服务版本 serviceVersion、注册中心类型 registryType、注册中心地址 registryAddress 和超时时间 timeout 四个属性,接下来我们需要使用这些属性构造出一个自定义的 Bean,并对该 Bean 执行的所有方法进行拦截。
Spring 的 FactoryBean 接口可以帮助我们实现自定义的 Bean,FactoryBean 是一种特种的工厂 Bean,通过 getObject() 方法返回对象,而并不是 FactoryBean 本身。
public class RpcReferenceBean implements FactoryBean<Object> { private Class<?> interfaceClass; private String serviceVersion; private String registryType; private String registryAddr; private long timeout; private Object object; @Override public Object getObject() throws Exception { return object; } @Override public Class<?> getObjectType() { return interfaceClass; } public void init() throws Exception {
// 生成动态代理对象并赋值给 object RegistryService registryService = RegistryFactory.getInstance(this.registryAddr, RegistryType.valueOf(this.registryType)); this.object = Proxy.newProxyInstance( interfaceClass.getClassLoader(), new Class<?>[]{interfaceClass}, new RpcInvokerProxy(serviceVersion, timeout, registryService)); } public void setInterfaceClass(Class<?> interfaceClass) { this.interfaceClass = interfaceClass; } public void setServiceVersion(String serviceVersion) { this.serviceVersion = serviceVersion; } public void setRegistryType(String registryType) { this.registryType = registryType; } public void setRegistryAddr(String registryAddr) { this.registryAddr = registryAddr; } public void setTimeout(long timeout) { this.timeout = timeout; } }
RpcReferenceBean 的 init() 方法需要实现动态代理对象,并通过代理对象完成 RPC 调用。对于使用者来说只是通过 @RpcReference 订阅了服务,并不感知底层调用的细节。对于如何实现 RPC 通信、服务寻址等,都是在动态代理类中完成的,动态代理的实现在后面会详细讲解。
有了 @RpcReference 注解和 RpcReferenceBean 之后,我们可以使用 Spring 的扩展点 BeanFactoryPostProcessor 对 Bean 的定义进行修改。上文中服务提供者使用的是 BeanPostProcessor,BeanFactoryPostProcessor 和 BeanPostProcessor 都是 Spring 的核心扩展点,它们之间有什么区别呢?BeanFactoryPostProcessor 是 Spring 容器加载 Bean 的定义之后以及 Bean 实例化之前执行,所以 BeanFactoryPostProcessor 可以在 Bean 实例化之前获取 Bean 的配置元数据,并允许用户对其修改。而 BeanPostProcessor 是在 Bean 初始化前后执行,它并不能修改 Bean 的配置信息。
现在我们需要对声明 @RpcReference 注解的成员变量构造出 RpcReferenceBean,所以需要实现 BeanFactoryPostProcessor 修改 Bean 的定义,具体实现如下所示。
@Component @Slf4j public class RpcConsumerPostProcessor implements ApplicationContextAware, BeanClassLoaderAware, BeanFactoryPostProcessor { private ApplicationContext context; private ClassLoader classLoader; private final Map<String, BeanDefinition> rpcRefBeanDefinitions = new LinkedHashMap<>(); @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context = applicationContext; } @Override public void setBeanClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) { BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName); String beanClassName = beanDefinition.getBeanClassName(); if (beanClassName != null) { Class<?> clazz = ClassUtils.resolveClassName(beanClassName, this.classLoader); ReflectionUtils.doWithFields(clazz, this::parseRpcReference); } } BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; this.rpcRefBeanDefinitions.forEach((beanName, beanDefinition) -> { if (context.containsBean(beanName)) { throw new IllegalArgumentException("spring context already has a bean named " + beanName); } registry.registerBeanDefinition(beanName, rpcRefBeanDefinitions.get(beanName)); log.info("registered RpcReferenceBean {} success.", beanName); }); } private void parseRpcReference(Field field) { RpcReference annotation = AnnotationUtils.getAnnotation(field, RpcReference.class); if (annotation != null) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(RpcReferenceBean.class); builder.setInitMethodName(RpcConstants.INIT_METHOD_NAME); builder.addPropertyValue("interfaceClass", field.getType()); builder.addPropertyValue("serviceVersion", annotation.serviceVersion()); builder.addPropertyValue("registryType", annotation.registryType()); builder.addPropertyValue("registryAddr", annotation.registryAddress()); builder.addPropertyValue("timeout", annotation.timeout()); BeanDefinition beanDefinition = builder.getBeanDefinition(); rpcRefBeanDefinitions.put(field.getName(), beanDefinition); } } }
RpcConsumerPostProcessor 类中重写了 BeanFactoryPostProcessor 的 postProcessBeanFactory 方法,从 beanFactory 中获取所有 Bean 的定义信息,然后分别对每个 Bean 的所有 field 进行检测。如果 field 被声明了 @RpcReference 注解,通过 BeanDefinitionBuilder 构造 RpcReferenceBean 的定义,并为 RpcReferenceBean 的成员变量赋值,包括服务类型 interfaceClass、服务版本 serviceVersion、注册中心类型 registryType、注册中心地址 registryAddr 以及超时时间 timeout。构造完 RpcReferenceBean 的定义之后,会将RpcReferenceBean 的 BeanDefinition 重新注册到 Spring 容器中。
服务提供者和服务消费者都是使用springboot,自带tomcat,若没有配置端口号,默认使用 8080。所以需要配置下服务消费者的端口号,在 rpc-consumer 模块的 resources 目录下创建全局配置文件 application.properties,并配置以下参数:
server.port = 8089
至此,我们已经将服务提供者服务消费者的基本框架搭建出来了,并且着重介绍了服务提供者使用 @RpcService 注解是如何发布服务的,服务消费者相应需要一个能够注入服务接口的注解 @RpcReference,被 @RpcReference 修饰的成员变量都会被构造成 RpcReferenceBean,并为它生成动态代理类,后面我们再继续深入介绍。
RPC 通信方案设计
搭建好了服务提供者和服务消费者的基本框架以后,就可以建立两个模块之间的通信机制了。通过向 ChannelPipeline 添加自定义的业务处理器,来完成 RPC 框架的远程通信机制。需要实现的主要功能如下:
- 服务消费者实现协议编码,向服务提供者发送调用数据。
- 服务提供者收到数据后解码,然后向服务消费者发送响应数据,暂时忽略 RPC 请求是如何被调用的。
- 服务消费者收到响应数据后成功返回。
看下 RPC 请求调用的过程,如下图所示。
RPC 请求的过程对于服务消费者来说是出站操作,对于服务提供者来说是入站操作。数据发送前,服务消费者将 RPC 请求信息封装成 MiniRpcProtocol 对象,然后通过编码器 MiniRpcEncoder 进行二进制编码,最后直接向发送至远端即可。服务提供者收到请求数据后,将二进制数据交给解码器 MiniRpcDecoder,解码后再次生成 MiniRpcProtocol 对象,然后传递给 RpcRequestHandler 执行真正的 RPC 请求调用。
RpcRequestHandler 处理成功后向服务消费者返回响应结果过程,如下图所示:
与 RPC 请求过程相反,是由服务提供者将响应结果封装成 MiniRpcProtocol 对象,然后通过 MiniRpcEncoder 编码发送给服务消费者。服务消费者对响应结果进行解码,因为 RPC 请求是高并发的,所以需要 RpcRequestHandler 根据响应结果找到对应的请求,最后将响应结果返回。
综合 RPC 请求调用和结果响应的处理过程来看,编码器 MiniRpcEncoder、解码器 MiniRpcDecoder 以及通信协议对象 MiniRpcProtocol 都可以设计成复用的,最终服务消费者和服务提供者的 ChannelPipeline 结构如下图所示。
自定义 RPC 通信协议
协议是服务消费者和服务提供者之间通信的基础,主流的 RPC 框架都会自定义通信协议,相比于 HTTP、HTTPS、JSON 等通用的协议,自定义协议可以实现更好的性能、扩展性以及安全性。结合 RPC 请求调用与结果响应的场景,我们设计一个简易版的 RPC 自定义协议,如下所示:
+---------------------------------------------------------------+ | 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte | +---------------------------------------------------------------+ | 状态 1byte | 消息 ID 8byte | 数据长度 4byte | +---------------------------------------------------------------+ | 数据内容 (长度不定) | +---------------------------------------------------------------+
我们把协议分为协议头 Header 和协议体 Body 两个部分。协议头 Header 包含魔数、协议版本号、序列化算法、报文类型、状态、消息 ID、数据长度,协议体 Body 只包含数据内容部分,数据内容的长度是不固定的。RPC 请求和响应都可以使用该协议进行通信,对应协议实体类的定义如下所示:
@Data public class MiniRpcProtocol<T> implements Serializable { private MsgHeader header;// 协议头 private T body;//协议体 }
@Data
public class MsgHeader implements Serializable {
/*
+---------------------------------------------------------------+
| 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte |
+---------------------------------------------------------------+
| 状态 1byte | 消息 ID 8byte | 数据长度 4byte |
+---------------------------------------------------------------+
*/
private short magic; // 魔数
private byte version; // 协议版本号
private byte serialization; // 序列化算法
private byte msgType; // 报文类型
private byte status; // 状态
private long requestId; // 消息 ID
private int msgLen; // 数据长度
}
在 RPC 请求调用的场景下,MiniRpcProtocol 中泛型 T 对应的 MiniRpcRequest 类型,MiniRpcRequest 主要包含 RPC 远程调用需要的必要参数,定义如下所示。
@Data public class MiniRpcRequest implements Serializable { private String serviceVersion; // 服务版本 private String className; // 服务接口名 private String methodName; // 服务方法名 private Object[] params; // 方法参数列表 private Class<?>[] parameterTypes; // 方法参数类型列表 }
在 RPC 结果响应的场景下,MiniRpcProtocol 中泛型 T 对应的 MiniRpcResponse 类型,MiniRpcResponse 实体类的定义如下所示。此外,响应结果是否成功可以使用 MsgHeader 中的 status 字段表示,0 表示成功,非 0 表示失败。MiniRpcResponse 中 data 表示成功状态下返回的 RPC 请求结果,message 表示 RPC 请求调用失败的错误信息。
@Data public class MiniRpcResponse implements Serializable { private Object data; // 请求结果 private String message; // 错误信息 }
设计完 RPC 自定义协议之后,解决 MiniRpcRequest 和 MiniRpcResponse 如何进行编码的问题。
序列化选型
MiniRpcRequest 和 MiniRpcResponse 实体类表示的协议体内容都是不确定具体长度的,所以我们一般会选用通用且高效的序列化算法将其转换成二进制数据,这样可以有效减少网络传输的带宽,提升 RPC 框架的整体性能。目前比较常用的序列化算法包括 Json、Kryo、Hessian、Protobuf 等,这些第三方序列化算法都比 Java 原生的序列化操作都更加高效。
首先我们定义了一个通用的序列化接口 RpcSerialization,所有序列化算法扩展都必须实现该接口,RpcSerialization 接口分别提供了序列化 serialize() 和反序列化 deserialize() 方法,如下所示:
public interface RpcSerialization { <T> byte[] serialize(T obj) throws IOException; <T> T deserialize(byte[] data, Class<T> clz) throws IOException; }
接下来我们为 RpcSerialization 提供了 HessianSerialization 和 JsonSerialization 两种类型的实现类。以 HessianSerialization 为例,实现逻辑如下:
@Component @Slf4j public class HessianSerialization implements RpcSerialization { @Override public <T> byte[] serialize(T object) { if (object == null) { throw new NullPointerException(); } byte[] results; HessianSerializerOutput hessianOutput; try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { hessianOutput = new HessianSerializerOutput(os); hessianOutput.writeObject(object); hessianOutput.flush(); results = os.toByteArray(); } catch (Exception e) { throw new SerializationException(e); } return results; } @SuppressWarnings("unchecked") @Override public <T> T deserialize(byte[] bytes, Class<T> clz) { if (bytes == null) { throw new NullPointerException(); } T result; try (ByteArrayInputStream is = new ByteArrayInputStream(bytes)) { HessianSerializerInput hessianInput = new HessianSerializerInput(is); result = (T) hessianInput.readObject(clz); } catch (Exception e) { throw new SerializationException(e); } return result; } }
为了能够支持不同序列化算法,我们采用工厂模式来实现不同序列化算法之间的切换,使用相同的序列化接口指向不同的序列化算法。对于使用者来说只需要知道序列化算法的类型即可,不用关心底层序列化是如何实现的。具体实现如下:
public class SerializationFactory { public static RpcSerialization getRpcSerialization(byte serializationType) { SerializationTypeEnum typeEnum = SerializationTypeEnum.findByType(serializationType); switch (typeEnum) { case HESSIAN: return new HessianSerialization(); case JSON: return new JsonSerialization(); default: throw new IllegalArgumentException("serialization type is illegal, " + serializationType); } } }
实现自定义的处理器
协议编码实现
Netty 提供了两个最为常用的编解码抽象基类 MessageToByteEncoder 和 ByteToMessageDecoder,帮助我们很方便地扩展实现自定义协议。
我们接下来要完成的编码器 MiniRpcEncoder 需要继承 MessageToByteEncoder,并重写 encode() 方法,具体实现如下所示:
public class MiniRpcEncoder extends MessageToByteEncoder<MiniRpcProtocol<Object>> { /* +---------------------------------------------------------------+ | 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte | +---------------------------------------------------------------+ | 状态 1byte | 消息 ID 8byte | 数据长度 4byte | +---------------------------------------------------------------+ | 数据内容 (长度不定) | +---------------------------------------------------------------+ */ @Override protected void encode(ChannelHandlerContext ctx, MiniRpcProtocol<Object> msg, ByteBuf byteBuf) throws Exception { MsgHeader header = msg.getHeader(); byteBuf.writeShort(header.getMagic()); byteBuf.writeByte(header.getVersion()); byteBuf.writeByte(header.getSerialization()); byteBuf.writeByte(header.getMsgType()); byteBuf.writeByte(header.getStatus()); byteBuf.writeLong(header.getRequestId()); RpcSerialization rpcSerialization = SerializationFactory.getRpcSerialization(header.getSerialization()); byte[] data = rpcSerialization.serialize(msg.getBody()); byteBuf.writeInt(data.length); byteBuf.writeBytes(data); } }
编码逻辑比较简单,在服务消费者或者服务提供者调用 writeAndFlush() 将数据写给对方前,都已经封装成 MiniRpcRequest 或者 MiniRpcResponse,所以可以采用 MiniRpcProtocol<Object>作为 MiniRpcEncoder 编码器能够支持的编码类型。
协议解码实现
解码器 MiniRpcDecoder 需要继承 ByteToMessageDecoder,并重写 decode() 方法,具体实现如下所示:
public class MiniRpcDecoder extends ByteToMessageDecoder { /* +---------------------------------------------------------------+ | 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte | +---------------------------------------------------------------+ | 状态 1byte | 消息 ID 8byte | 数据长度 4byte | +---------------------------------------------------------------+ | 数据内容 (长度不定) | +---------------------------------------------------------------+ */ @Override public final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { if (in.readableBytes() < ProtocolConstants.HEADER_TOTAL_LEN) { return; } in.markReaderIndex(); short magic = in.readShort(); if (magic != ProtocolConstants.MAGIC) { throw new IllegalArgumentException("magic number is illegal, " + magic); } byte version = in.readByte(); byte serializeType = in.readByte(); byte msgType = in.readByte(); byte status = in.readByte(); long requestId = in.readLong(); int dataLength = in.readInt(); if (in.readableBytes() < dataLength) { in.resetReaderIndex(); return; } byte[] data = new byte[dataLength]; in.readBytes(data); MsgType msgTypeEnum = MsgType.findByType(msgType); if (msgTypeEnum == null) { return; } MsgHeader header = new MsgHeader(); header.setMagic(magic); header.setVersion(version); header.setSerialization(serializeType); header.setStatus(status); header.setRequestId(requestId); header.setMsgType(msgType); header.setMsgLen(dataLength); RpcSerialization rpcSerialization = SerializationFactory.getRpcSerialization(serializeType); switch (msgTypeEnum) { case REQUEST: MiniRpcRequest request = rpcSerialization.deserialize(data, MiniRpcRequest.class); if (request != null) { MiniRpcProtocol<MiniRpcRequest> protocol = new MiniRpcProtocol<>(); protocol.setHeader(header); protocol.setBody(request); out.add(protocol); } break; case RESPONSE: MiniRpcResponse response = rpcSerialization.deserialize(data, MiniRpcResponse.class); if (response != null) { MiniRpcProtocol<MiniRpcResponse> protocol = new MiniRpcProtocol<>(); protocol.setHeader(header); protocol.setBody(response); out.add(protocol); } break; case HEARTBEAT: // TODO break; } } }
解码器 MiniRpcDecoder 相比于编码器 MiniRpcEncoder 要复杂很多,MiniRpcDecoder 的目标是将字节流数据解码为消息对象,并传递给下一个 Inbound 处理器。整个 MiniRpcDecoder 解码过程有几个要点要特别注意:
- 只有当 ByteBuf 中内容大于协议头 Header 的固定的 18 字节时,才开始读取数据。
- 即使已经可以完整读取出协议头 Header,但是协议体 Body 有可能还未就绪。所以在刚开始读取数据时,需要使用 markReaderIndex() 方法标记读指针位置,当 ByteBuf 中可读字节长度小于协议体 Body 的长度时,再使用 resetReaderIndex() 还原读指针位置,说明现在 ByteBuf 中可读字节还不够一个完整的数据包。
- 根据不同的报文类型 MsgType,需要反序列化出不同的协议体对象。在 RPC 请求调用的场景下,服务提供者需要将协议体内容反序列化成 MiniRpcRequest 对象;在 RPC 结果响应的场景下,服务消费者需要将协议体内容反序列化成 MiniRpcResponse 对象。
请求处理与响应
在 RPC 请求调用的场景下,服务提供者的 MiniRpcDecoder 编码器将二进制数据解码成 MiniRpcProtocol<MiniRpcRequest> 对象后,再传递给 RpcRequestHandler 执行 RPC 请求调用。RpcRequestHandler 也是一个 Inbound 处理器,它并不需要承担解码工作,所以 RpcRequestHandler 直接继承 SimpleChannelInboundHandler 即可,然后重写 channelRead0() 方法,具体实现如下:
@Slf4j public class RpcRequestHandler extends SimpleChannelInboundHandler<MiniRpcProtocol<MiniRpcRequest>> { private final Map<String, Object> rpcServiceMap; public RpcRequestHandler(Map<String, Object> rpcServiceMap) { this.rpcServiceMap = rpcServiceMap; } @Override protected void channelRead0(ChannelHandlerContext ctx, MiniRpcProtocol<MiniRpcRequest> protocol) { RpcRequestProcessor.submitRequest(() -> { MiniRpcProtocol<MiniRpcResponse> resProtocol = new MiniRpcProtocol<>(); MiniRpcResponse response = new MiniRpcResponse(); MsgHeader header = protocol.getHeader(); header.setMsgType((byte) MsgType.RESPONSE.getType()); try { Object result = handle(protocol.getBody()); response.setData(result); header.setStatus((byte) MsgStatus.SUCCESS.getCode()); resProtocol.setHeader(header); resProtocol.setBody(response); } catch (Throwable throwable) { header.setStatus((byte) MsgStatus.FAIL.getCode()); response.setMessage(throwable.toString()); log.error("process request {} error", header.getRequestId(), throwable); } ctx.writeAndFlush(resProtocol); }); } private Object handle(MiniRpcRequest request) throws Throwable { String serviceKey = RpcServiceHelper.buildServiceKey(request.getClassName(), request.getServiceVersion()); Object serviceBean = rpcServiceMap.get(serviceKey); if (serviceBean == null) { throw new RuntimeException(String.format("service not exist: %s:%s", request.getClassName(), request.getMethodName())); } Class<?> serviceClass = serviceBean.getClass(); String methodName = request.getMethodName(); Class<?>[] parameterTypes = request.getParameterTypes(); Object[] parameters = request.getParams(); FastClass fastClass = FastClass.create(serviceClass); int methodIndex = fastClass.getIndex(methodName, parameterTypes); return fastClass.invoke(methodIndex, serviceBean, parameters); } }
因为 RPC 请求调用是比较耗时的,所以比较推荐的做法是将 RPC 请求提交到自定义的业务线程池中执行。其中 handle() 方法是真正执行 RPC 调用的地方,你可以先留一个空的实现,在之后动态代理的课程中我们再完成它。根据 handle() 的执行情况,MiniRpcProtocol<MiniRpcResponse> 最终会被设置成功或者失败的状态,以及相应的请求结果或者错误信息,最终通过 writeAndFlush() 方法将数据写回服务消费者。
上文中我们已经分析了服务消费者入站操作,首先要经过 MiniRpcDecoder 解码器,根据报文类型 msgType 解码出 MiniRpcProtocol<MiniRpcResponse>响应结果,然后传递给 RpcResponseHandler 处理器,RpcResponseHandler 负责响应不同线程的请求结果,具体实现如下:
public class RpcResponseHandler extends SimpleChannelInboundHandler<MiniRpcProtocol<MiniRpcResponse>> { @Override protected void channelRead0(ChannelHandlerContext ctx, MiniRpcProtocol<MiniRpcResponse> msg) { long requestId = msg.getHeader().getRequestId(); MiniRpcFuture<MiniRpcResponse> future = MiniRpcRequestHolder.REQUEST_MAP.remove(requestId); future.getPromise().setSuccess(msg.getBody()); } }
public class MiniRpcRequestHolder {
public final static AtomicLong REQUEST_ID_GEN = new AtomicLong(0);
public static final Map<Long, MiniRpcFuture<MiniRpcResponse>> REQUEST_MAP = new ConcurrentHashMap<>();
}
@Data
public class MiniRpcFuture<T> {
private Promise<T> promise;
private long timeout;
public MiniRpcFuture(Promise<T> promise, long timeout) {
this.promise = promise;
this.timeout = timeout;
}
}
服务消费者在发起调用时,维护了请求 requestId 和 MiniRpcFuture<MiniRpcResponse>的映射关系,RpcResponseHandler 会根据请求的 requestId 找到对应发起调用的 MiniRpcFuture,然后为 MiniRpcFuture 设置响应结果。
我们采用 Netty 提供的 Promise 工具来实现 RPC 请求的同步等待,Promise 基于 JDK 的 Future 扩展了更多新的特性,帮助我们更好地以同步的方式进行异步编程。Promise 模式本质是一种异步编程模型,我们可以先拿到一个查看任务执行结果的凭证,不必等待任务执行完毕,当我们需要获取任务执行结果时,再使用凭证提供的相关接口进行获取。
至此,RPC 框架的通信模块我们已经实现完了。