手动实现一个RPC框架(三):Netty+ZooKeeper+Java
手动实现一个RPC框架,需要用到哪些技术,这些技术又分别起到了什么作用呢。
手动实现一个RPC框架系列文章
在前面第两篇章的学习内容,我们已经了解到RPC是一个什么东东,以及如何简单实现一个远程传输的功能,另外还介绍了Dubbo和Feign的区别。详情可以去下面的链接中进行查看。
手动实现一个RPC框架 (一):RPC的介绍_种一棵橙子树的博客-CSDN博客
手动实现一个RPC框架(二):Dubbo与Feign的区别_种一棵橙子树的博客-CSDN博客
本篇文章,我们继续实现我们自己的RPC框架,我是根据GitHub上Java Guide哥的项目进行实现然后加入写下自己的理解和知识点总结的,各位想要看见实现的结果,或者是想完整的了解整个项目,可以点击下面的链接跳转。
目录
一、RPC框架需要有哪些东西?
首先我们来看一下这张图
一个最简单的RPC框架,至少得拥有服务注册中心,远程服务请求的客户端,以及提供远程服务的服务端。这也是我在第一篇文章中实现的远程调用的架构。
然而,Dubbo作为RPC框架的架构是这样的
看着有点懵,实际上我也很懵,我们继续往下看。
一个完整的RPC框架应该有的东西,至于这些部分的基本功能和用处,可以参考我第一篇文章的末尾写的。手动实现一个RPC框架 (一):RPC的介绍_种一棵橙子树的博客-CSDN博客
那接下来我们就一个一个介绍这里面使用的东西吧
注册中心
注册中心,有学习过SpringCloud,接触过微服务开发,分布式环境搭建的肯定对这个东西很熟悉吧,注册中心起到的作用就是,我们把实现远程调用功能的服务端注册到注册中心,而我们访问远程调用的客户端,要从注册中心中发现对应的服务,然后他才能实现远程调用。
注册中心也有很多种,Eureka,Nacos,Zookeeper等等。这里我选择用Zookeeper,虽然我也仅仅是了解,平常用的话,Nacos比较多,但是由于Guide哥使用的是Zookeeper,我也就照着用啦,在完成后,我可能会再用Nacos来实现一遍。
Zookeeper为我们提供了高可用,高性能,稳定的分布式数据一致性解决方案,通常被用于实现数据发布/订阅,负载均衡,命名服务,分布式协调及通知,集群管理和 Lead 主机的选取,分布式锁和分布式队列等功能。并且,Zookeeper将数据保存在内存中,性能很高,但是存储的数据量不多(树形结构,基于内存)
我们通过注册中心负责服务的注册与发现,服务端启动的时候将服务名称以及对应的地址(ip+port)注册到注册中心,消费端(客户端)根据服务名称找到对应的服务地址,有了服务地址后,客户端就可以通过网络请求服务端了。
来看下这张图
那么总结一下整个流程
1.服务容器启动,加载并且运行服务提供者
2.服务提供者启动的时候,向注册中心注册自己的服务(带上ip地址+端口号+服务名)
3.服务消费者在启动的时候,向注册中心订阅自己所需的服务(根据服务名)
4.注册中心将服务提供者的地址列表发送给消费者,然后将基于长连接的推送变更数据给消费者。
5.服务消费者,从提供的地址列表中,采用负载均衡算法,选出一台提供者进行调用。若调用失败,选另一台。
6.服务消费者和提供者,在内存中计算调用次数和调用时间,定时发送一次统计数据到监控中心。
网络传输
既然我们要使用远程调用,之前也提到过了,就是要在本机调用其他服务器的服务。那我们肯定是要通过网络请求来实现的,在第一篇章的内容有简单提到,为了确保我们调用的远程服务的正确性,发送请求的时候要明确接口名,方法名,参数类型,参数名等等。
至于网络传输的实现形式,可以使用最基本的Socket,但是Socket是阻塞IO,性能较低,而且功能单一。
我们可以使用基于同步非阻塞的IO模型NIO的网络编程框架Netty。
Netty是一个基于NIO的 client-servier (客户-服务端)框架,使用它可以快速简单的开发网络应用程序。
它极大的简化TCO和UDP套接字(Socket)服务器等网络编程的流程,并且性能和安全性甚至更好
支持多种协议,如FTP,SMTP,HTTP以及各种二进制和文本传统协议。
序列化和反序列化
我们都知道,在网络传输的过程中,序列化操作是很重要的。因为网络传输的数据必须是二进制的因此我们Java程序编写的对象无法在网络中进行传输,我们需要将其序列化为二进制数据,在传输完成后,在进行反序列化操作,重新解析为Java对象。
除此之外,不仅网路传输的时候需要用到序列号和反序列化,将对象存储到文件,数据库等场景都需要用到序列化和反序列化。过程如下图
图片来源于Java Guide公众号
JDK自带的序列化方式,只需要实现Serializable接口就可以了,但是这种方式不支持跨语言调用,而且性能比较低。
现在常用的序列化方式有 hessian,kyro,protostuff等等,在之后会进行讲解。
动态代理
首先,代理模式就是我们给某一个对象去提供一个代理对象,然后我们通过代理对象代替真实对象去完成一些操作。JDK默认的动态代理方式为proxy。
那么为什么要在RPC框架中使用动态代理呢,我们首先回顾一个概念,RPC远程调用的主要目的就是让我们可以像调用本地方法一样简单的去调用远程方法,不需要去关心远程方法是怎么实现的
那么我们怎么才能屏蔽掉远程方法调用的底层细节呢,答案就是通过动态代理的形式来实现,当我们在调用远程方法的时候,实际上通过代理对象来传输网络请求。
负载均衡
负载均衡是微服务和分布式环境中常见的一种服务处理策略,一般我们为了应对服务访问量大的场景,会通过配置多个服务器来共同提供某种服务,那么在调用服务的时候如何选择该去那个服务器中进行呢,这就需要我们的负载均衡策略来进行处理。我们肯定不能让一台服务器一直干活,一台服务器一直闲着,所以,设置合适的负载均衡策略,可以显著的帮我们提高服务的性能和质量。
传输协议
我们还需要设计一个私有的RPC协议,这个协议是客户端(服务消费者)和服务端(服务提供者)沟通的媒介。
通过设计协议,我们可以定义需要传输什么类型的数据,并且还会规定每一种类型的数据应该占用多少字节,这样我们在收到二进制数据之后,就可以正确的解析出我们需要的数据对象。有点类似密文传输的密钥概念。
一个RPC协议通常应该包含以下几个内容 参考:Netty——自定义协议通信 - 曹伟雄 - 博客园 (cnblogs.com)
魔数
魔数是通信双方协商的一个暗号,通常采用固定的几个字节表示。魔数的作用是用于服务端在接收数据时先解析出前几个固定字节做正确性对比。
如果和协议中的魔数不匹配,则认为是非法数据,可以直接关闭连接或采取其他措施增强系统安全性。
魔数的思想在很多场景中都有体现,如 Java Class 文件开头就存储了魔数 OxCAFEBABE,在 JVM 加载 Class 文件时首先就会验证魔数对的正确性。
协议版本号
为了应对业务需求的变化,可能需要对自定义协议的结构或字段进行改动。不同版本的协议对应的解析方法也是不同的。所以在生产级项目中强烈建议预留协议版本这个字段。
序列化算法
序列化算法字段表示发送方将对象转换成二进制流,以及接收方将接收的二进制流转换成对象的方法,如 JSON、 Hessian、Java 自带序列化等。
报文类型
报文类型用于描述业务场景中存在的不同报文类型。如 RPC 框架中有请求、响应、心跳类型。IM 通讯场景中有登陆、创建群聊、发送消息、接收消息、退出群聊等类型。
长度域字段
长度域字段代表请求数据的长度,可以定义整个报文的长度,也可以是请求数据部分的长度。
请求数据
请求数据通常为的业务对象信息序列化后的二进制流。是整个报文的主体。
状态
状态字段用于标识请求是否正常,一般由被调用方设置。例如一次 RPC 调用失败,状态字段可被服务提供方设置为异常状态。
校验字段
校验字段存放某种校验算法计算报文校验码,校验码用于验证报文的正确性。
保留字段
保留字段是可选项,为了应对协议升级的可能性,可以预留若干字节的保留字段,以备不时之需。
技术概况
在本篇文章的内容中,我们将接下来需要设计的自定义RPC框架需要使用到的相关技术进行了简单的罗列和讲解,目前大体结构是 Java+Netty+Zookeper。下面分别描述具体使用了哪些相关知识。
Java
1.动态代理机制
2.序列化机制
3.线程池
4.CompletableFuture类
Netty
1.使用Netty进行网络传输
2.ByteBuf
3.Netty粘包拆包
4.Netty长连接和心跳机制。
Zookeeper
服务注册中心,注册服务和发现服务
总结
以上文章,对RPC框架的技术进行了概括和讲解,本文的实现是基于GitHub社区作者Java Guide的作品来实现的,下面为作品连接,各位观众可以自行了解阅读。Snailclimb/guide-rpc-framework: A custom RPC framework implemented by Netty+Kyro+Zookeeper.(一款基于 Netty+Kyro+Zookeeper 实现的自定义 RPC 框架-附详细实现过程和相关教程。) (github.com)
更多推荐
所有评论(0)