Netty进阶实现自定义Rpc

news2024/9/20 16:47:32

项目地址:xz125/Rpc-msf (github.com)

1 项目架构:

RPC 框架包含三个最重要的组件,分别是客户端、服务端和注册中心。在一次 RPC 调用流程中,这三个组件是这样交互的:

  • 服务端(provider)在启动后会将它提供的服务列表地址信息发布到注册中心。

  • 客户端(client)会通过本地代理模块 Proxy 发现注册中心的服务信息,客户端从服务列表中选取其中一个的服务地址,并将数据通过网络发送给服务端。

  • 服务端接收到数据后进行解码,得到请求信息;

  • 服务端根据解码后的请求信息调用对应的服务,然后将调用结果返回给客户端。

2 项目依赖

2.1 使用maven聚合工程

  • rpc 父工程

  • consumer,服务消费者,是rpc的子工程,依赖于rpc-client-spring-boot-starter。

  • provider,服务提供者,是rpc的子工程,依赖于rpc-server-spring-boot-starter。

  • provider-api,服务提供者暴露的服务API,是rpc的子工程。

  • rpc-client-spring-boot-starter,rpc客户端starter,封装客户端发起的请求过程(动态代理、网络通信)。

  • rpc-core,RPC核心依赖,负载均衡策略、消息协议、协议编解码、序列化、请求响应实体、服务注册发现。

  • rpc-server-spring-boot-starter,rpc服务端starter,负责发布 RPC 服务,接收和处理 RPC 请求(动态代理、网络通信),反射调用服务端。

项目依赖图

3 项目解析

3.1发布服务和消费服务

对于发布的服务需要使用 @RpcService 注解标识,复合注解,基于 @Service

// !!!注解定义
@Target(ElementType.TYPE)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Service
public @interface RpcService {
    /**
     *  暴露服务接口类型
     * @return
     */
    Class<?> interfaceType() default Object.class;

    /**
     *  服务版本
     * @return
     */
    String version() default "1.0";
}

// !!!实现类定义
@Slf4j
@RpcService(interfaceType = HelloWorldService.class, version = "1.0")
public class HelloWorldServiceImpl implements HelloWorldService {
    @Override
    public String sayHello(String name) {
        log.info("您好:" + name + ", rpc 调用成功");
        return String.format("您好:%s, rpc 调用成功", name);
    }
}

消费服务需要使用 @RpcAutowired 注解标识,复合注解,基于 @Autowired

// 注解定义
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Autowired
public @interface RpcAutowired {

    String version() default "1.0";
}
 
// controller 实现。
@Controller
public class HelloWorldController {

    @RpcAutowired
    private HelloWorldService helloWorldService;

    @GetMapping("/hello/world")
    public ResponseEntity<String> pullServiceInfo(@RequestParam("name") String name){
        return  ResponseEntity.ok(helloWorldService.sayHello(name));
    }
}
3.2 动态代理

基于jdk接口的动态代理,客户端不能切换(rpc-client-spring-boot-starter模块 proxy 包)

原理是服务消费者启动的时候有个 RpcClientProcessor bean 的后置处理器,会扫描ioc容器中的bean,如果这个bean有属性被@RpcAutowired修饰,就给属性动态赋代理对象。

3.3 服务注册发现

本项目使用ZK做的,实现在 `rpc-core` 模块 com.msf.core.discovery 包下面是服务发现, com.msf.core.register 包下面是服务注册。 服务提供者启动后,RpcServerProvider 会获取到被 @RpcService 修饰的bean,将服务元数据注册到zk上。

3.4 负载均衡策略

负载均衡定义在rpc-core中,目前支持轮询(FullRoundBalance)和随机(RandomBalance),默认使用随机策略。由rpc-client-spring-boot-starter指定,可以通过配置文件进行修改。

@Primary
 @Bean(name = "loadBalance")
 @ConditionalOnMissingBean
 @ConditionalOnProperty(prefix = "rpc.client", name = "balance", havingValue = "randomBalance", matchIfMissing = true)
 public LoadBalance randomBalance() {
     return new RandomBalance();
 }

 @Bean(name = "loadBalance")
 @ConditionalOnMissingBean
 @ConditionalOnProperty(prefix = "rpc.client", name = "balance", havingValue = "fullRoundBalance")
 public LoadBalance loadBalance() {
     return new FullRoundBalance();
 } 

可以在消费者中配置 rpc.client.balance=fullRoundBalance 替换,也可以自己定义,通过实现接口 LoadBalance,并将创建的类加入IOC容器即可。

3.5自定义消息协议、编解码。

所谓协议,就是通信双方事先商量好规则,服务端知道发送过来的数据将如何解析。

+---------------------------------------------------------------+

| 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte|

+---------------------------------------------------------------+

| 状态 1byte | 消息 ID 8byte | 数据长度 4byte |

+---------------------------------------------------------------+

| 数据内容 (长度不定) |

+---------------------------------------------------------------+

  • 魔数:魔数是通信双方协商的一个暗号,通常采用固定的几个字节表示。魔数的作用是防止任何人随便向服务器的端口上发送数据。 例如 java Class 文件开头就存储了魔数 0xCAFEBABE,在加载 Class 文件时首先会验证魔数的正确性

  • 协议版本号:随着业务需求的变化,协议可能需要对结构或字段进行改动,不同版本的协议对应的解析方法也是不同的。

  • 序列化算法:序列化算法字段表示数据发送方应该采用何种方法将请求的对象转化为二进制,以及如何再将二进制转化为对象,如 JSON、Hessian、Java 自带序列化等。

  • 报文类型: 在不同的业务场景中,报文可能存在不同的类型。RPC 框架中有请求、响应、心跳等类型的报文。(可以是请求报文,或者是响应报文等等)

  • 状态: 状态字段用于标识请求是否正常(SUCCESS、FAIL)。

  • 消息ID: 请求唯一ID,通过这个请求ID将响应关联起来,也可以通过请求ID做链路追踪。

  • 数据长度: 标明数据的长度,用于判断是否是一个完整的数据包

  • 数据内容: 请求体内容

3.6 编解码

编解码实现在 rpc-core 模块,在包 com.msf.core.codec下。

3.6.1 如何实现编解码?

编码利用 netty 的 MessageToByteEncoder 类实现。实现 encode 方法,MessageToByteEncoder 继承 ChannelOutboundHandlerAdapter 。编码就是将请求数据写入到 ByteBuf 中。

解码是利用 netty 的 ByteToMessageDecoder 类实现。 实现 decode 方法,ByteToMessageDecoder 继承 ChannelInboundHandlerAdapter。解码就是将 ByteBuf 中数据解析出请求的数据。解码要注意 TCP 粘包和拆包问题。

本项目如何解决的?

使用的是 消息长度 + 消息内容 的形式。在解码器 RpcDecoder 中读取固定长度数据。

3.7 序列化与反序列化

序列化和反序列化在 rpc-core 模块 com.msf.core.serialization 包下,提供了 HessianSerialization 和 JsonSerialization 序列化。默认使用 HessianSerialization 序列化。用户也可以根据项目中实现接口进行自定义。

public static SerializationTypeEnum parseByName(String typeName) {
        for (SerializationTypeEnum typeEnum : SerializationTypeEnum.values()) {
            if (typeEnum.name().equalsIgnoreCase(typeName)) {
                return typeEnum;
            }
        }
        return HESSIAN;
    }

    public static SerializationTypeEnum parseByType(byte type) {
        for (SerializationTypeEnum typeEnum : SerializationTypeEnum.values()) {
            if (typeEnum.getType() == type) {
                return typeEnum;
            }
        }
        return HESSIAN;
    }
3.8 网络传输

使用netty代码,值得注意的是 handler 的顺序不能弄错,编码是出站操作(可以放在入站后面),解码和收到响应都是入站操作,解码要在前面

bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline()
            // 编码 是出站操作 将消息编写二进制
            .addLast(new RpcEncoder<>())
            // 解码 是入站操作 将二进制解码成消息
            .addLast(new RpcDecoder())
            // 接收响应 入站操作
            .addLast(handler);
    }
});

4 启动流程

服务提供者启动

  • 服务提供者 provider 会依赖 rpc-server-spring-boot-starter

  • ProviderApplication 启动,根据springboot 自动装配机制,RpcServerAutoConfiguration 自动配置生效

  • RpcServerProvider 是一个bean后置处理器,会发布服务(会对加入@RpcService注解的bean进行增强),将服务元数据注册到ZK上。

  • RpcServerProvider.run 方法会开启一个 netty 服务

服务消费者启动

  • 服务消费者 consumer 会依赖 rpc-client-spring-boot-starter

  • ConsumerApplication 启动,根据springboot 自动装配机制,RpcClientAutoConfiguration 自动配置生效

  • 将服务发现、负载均衡、代理等bean加入IOC容器

  • 后置处理器 RpcClientProcessor 会扫描 bean ,将被 @RpcAutowired 修饰的属性动态赋值为代理对象

调用过程

  1. 服务消费者 发起请求 http://localhost:9090/hello/world?name=hello

  1. 服务消费者 调用 helloWordService.sayHello() 方法,会被代理到执行 ClientStubInvocationHandler.invoke() 方法

  1. 服务消费者 通过ZK服务发现获取服务元数据,找不到报错404

  1. 服务消费者 自定义协议,封装请求头和请求体

  1. 服务消费者 通过自定义编码器 RpcEncoder 将消息编码

  1. 服务消费者 通过 服务发现获取到服务提供者的ip和端口, 通过Netty网络传输层发起调用

  1. 服务消费者 通过 RpcFuture 进入返回结果(超时)等待

  1. 服务提供者 收到消费者请求

  1. 服务提供者 将消息通过自定义解码器 RpcDecoder 解码

  1. 服务提供者 解码之后的数据发送到 RpcRequestHandler 中进行处理,通过反射调用执行服务端本地方法并获取结果

  1. 服务提供者 将执行的结果通过 编码器 RpcEncoder 将消息编码。(由于请求和响应的协议是一样,所以编码器和解码器可以用一套)

  1. 服务消费者 将消息通过自定义解码器 RpcDecoder 解码

  1. 服务消费者 通过RpcResponseHandler将消息写入 请求和响应池中,并设置 RpcFuture 的响应结果

  1. 服务消费者 获取到结果

4.1环境搭建
  • 操作系统:Windows

  • 集成开发工具:IntelliJ IDEA

  • 项目技术栈:SpringBoot 2.5.2 + JDK 1.8 + Netty 4.1.42.Final

  • 项目依赖管理工具:Maven 4.0.0

  • 注册中心:Zookeeeper 3.7.0

4.2项目测试
  • 启动 Zookeeper 服务器:bin/zkServer.cmd

  • 启动 provider 模块 ProviderApplication,需要修改zookeeper的ip地址(RpcServerProperties)

  • 启动 consumer 模块 ConsumerApplication,需要修改zookeeper的ip地址(RpcClientProperties)

  • 测试:浏览器输入 http://localhost:9090/hello/world?name=hello,成功返回 您好:hello, rpc 调用成功

5 结果展示

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/360001.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RocketMQ 第一章

RocketMQ 第一章 1、什么是MQ Message Queue&#xff08;消息队列&#xff09;&#xff0c;从字⾯上理解&#xff1a;⾸先它是⼀个队列。FIFO 先进先出的数据结构 —— 队列。消息队列就是所谓的存放消息的队列。 消息队列解决的不是存放消息的队列的⽬的&#xff0c;而是解…

AcWing1015.摘花生

AcWing 1015. 摘花生Hello Kitty想摘点花生送给她喜欢的米老鼠。她来到一片有网格状道路的矩形花生地(如下图)&#xff0c;从西北角进去&#xff0c;东南角出来。地里每个道路的交叉点上都有种着一株花生苗&#xff0c;上面有若干颗花生&#xff0c;经过一株花生苗就能摘走该它…

《FPGA学习》->蜂鸣器播放

&#x1f34e;与其担心未来&#xff0c;不如现在好好努力。在这条路上&#xff0c;只有奋斗才能给你安全感。你若努力&#xff0c;全世界都会为你让路。蜂鸣器的发声原理由振动装置和谐振装置组成&#xff0c;而蜂鸣器又分为无源他激型与有源自激型。本实验采用无源蜂鸣器&…

嵌入物理(PINN)还是基于物理(AD)?

文章目录1. 传统"反演问题"1.1 反演问题是什么1.2 常见反演问题1.3 传统反演问题的困境2. 深度学习优势3. AD inversion 例子3.1 ADsurf3.2 ADseismic关于PINN的内容大家可以直接google PINN (Physical-informed neural network),其主要的目的是用一个神经网络拟合物…

K8S 部署 Jenkins

本文使用 bitnami 镜像部署 Jenkins 官方文档&#xff1a;https://github.com/bitnami/charts/tree/main/bitnami/jenkins 添加 bitnami 仓库 helm repo add bitnami https://charts.bitnami.com/bitnami自定义 values.yaml storageClass&#xff1a;集群的存储类&#xff…

(考研湖科大教书匠计算机网络)第五章传输层-第八节1:TCP连接管理理论部分(三次握手与四次挥手)

获取pdf&#xff1a;密码7281专栏目录首页&#xff1a;【专栏必读】考研湖科大教书匠计算机网络笔记导航此部分内容借鉴博主【小林coding】 &#xff0c;其对计算机网络内容的图解可以说是深入浅出&#xff0c;尤其是三次握手和四次挥手这一部分&#xff0c;堪称全网最佳。所这…

OpenEuler安装软件方法

在树莓派上烧录好OpenEuler后上面是什么软件都没有的&#xff0c;像一些gcc的环境都需要自己进行配置。官方提供的安装命令是yum&#xff0c;但是执行yum是找不到命令的&#xff1a;   这个其实是因为OpenEuler中默认的安装软件使用了dnf而不是yum&#xff0c;所以软件的安装…

智能小车红外跟随原理

红外跟随电路红外跟随电路由电位器R17&#xff0c;R28&#xff1b;发光二极管D8&#xff0c;D9&#xff1b;红外发射管 D2&#xff0c;D4和红外接收管D3&#xff0c;D5和芯片LM324等组成,LM234用于信号的比较&#xff0c;并产生比较结果输出给单片机进行处理。智能小车红外跟随…

OpenGL学习日志之纹理

引言 为了使我们渲染的模型拥有更多细节&#xff0c;我们可以添加足够多的顶点&#xff0c;然后给每一个顶点都添加顶点颜色。但是这样就会产生很多额外的开销&#xff0c;因此就出现了纹理映射技术&#xff0c;我们通过纹理采样为物体的表面添加更多的细节。 纹理定义 通俗…

超25亿全球月活,字节依然没有流量

&#xff08;图片来源于网络&#xff0c;侵删&#xff09; 文|螳螂观察 作者| 搁浅虎鲸 注意看&#xff0c;这个男人叫梁汝波&#xff0c;是字节跳动的联合创始人&#xff0c;也是接棒张一鸣的新任CEO。 在字节跳动十周年之际&#xff0c;他发表了激情昂扬的演讲。“激发创…

【Datawhale图机器学习】图嵌入表示学习

图嵌入表示学习 学习视频&#xff1a;https://www.bilibili.com/video/BV1AP4y1r7Pz/ 如何把节点映射成D维向量&#xff1f; 人工特征工程&#xff1a;节点重要度、集群系数、Graphlet图表示学习&#xff1a;通过随机游走构造自监督学习任务。DeepWalk、Node2Vec矩阵分解深度…

win10字体模糊怎么办?看下面4种宝藏解决方法

最近很多用户反映电脑安装了Win10系统后出现字体发虚&#xff0c;模糊不清的问题&#xff0c;这看起来让人非常难受。win10字体模糊怎么办&#xff1f;来看下面4种宝藏解决方法&#xff01;下面的方法适用于各类台式电脑以及笔记本电脑哦&#xff01; 操作环境&#xff1a; 演示…

ESP开发环境搭建

一、windows中搭建 esp-idf tool(可选),下载连接如下:https://dl.espressif.com/dl/esp-idf/?idf4.4 下载安装tools后进入vscode进行插件安装&#xff08;未离线下载idf工具也可以通过第二步通过插件下载安装&#xff09; 1. vscode安装编译环境 ESP-IDF 需要安装一些必备工…

高并发系统设计之负载均衡

本文已收录至Github&#xff0c;推荐阅读 &#x1f449; Java随想录 文章目录DNS负载均衡Nginx负载均衡负载均衡算法负载均衡配置超时配置被动健康检查与主动健康检查LVS/F5Nginx当我们的应用单实例不能支撑用户请求时&#xff0c;此时就需要扩容&#xff0c;从一台服务器扩容到…

【matplotlib】可视化解决方案——如何设置轴标签的透明度和大小

概述 Axes 标签对于读者理解图表非常重要&#xff0c;它描述了图表中展现的数据内容。通过向 axes 对象添加标签&#xff0c;可以有效理解图表所表达的内容。首先来了解一下 matplotlib 是如何组织图表的。最上层是一个 Figure 实例&#xff0c;包含绘图中所有可见和不可见的内…

北斗导航 | 2023 PTTI会议论文 2023 ITM会议论文 2022 ION GNSS+ 会议论文下载:ION 美国导航学会

===================================================== github:https://github.com/MichaelBeechan CSDN:https://blog.csdn.net/u011344545 ===================================================== 2023 PTTI会议论文 2023 ITM会议论文 2022 ION GNSS+ 论文下载百度云链…

Teradata当年数据仓库的“一哥”为何突然退出中国市场:苦撑了3年,员工早有预料

2月15日&#xff0c;Teradata天睿公司官宣即将撤离中国市场。 又是一个艰难的决定&#xff0c;听着似乎很熟悉。Teradata为什么突然宣布结束在中国的直营&#xff1f;其实&#xff0c;回顾Teradata在中国市场的发展状况&#xff0c;一点也不突然。 多年前&#xff0c;我曾经与…

Excel表格自动转Json数据

Excel表格转JSON格式在实际工作中&#xff0c;我们常常使用Excel记录各种数据&#xff0c;但在各种应用系统传输数据却使用JSON格式&#xff0c;这就需要把Excel转为JSON。如果能把数据转换传输过程自动化就更完美了。Excel转JsonXX公司生产日报表为例&#xff0c;生产工人用Ex…

JSR303基本使用以及整合springboot统一异常处理

目录 一、前言 什么是JSR303 二、JSR303基本使用&#xff08;普通使用&#xff09; 1&#xff09;、引入jar包 2&#xff09;、实体类对需要校验的数据进行校验 3)、对前端传递过来的参数进行限制 三、JSR303基本使用&#xff08;分组校验&#xff09; 1)、创建分组 2)…

leaflet 根据两个坐标值,设置arc弧线和Marker(079)

第069个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中根据提供的两个点,绘制出marker,同时将两点间绘制出一条弧线。 直接复制下面的 vue+openlayers源代码,操作2分钟即可运行实现效果. 文章目录 示例效果配置方式示例源代码(共88行)安装插件相关API参考…