手写简单的RPC框架(一)

news/2024/11/22 21:31:34/

一、RPC简介

1、什么是RPC

RPC(Remote Procedure Call)远程过程调用协议,一种通过网络从远程计算机上请求服务,而不需要了解底层网络技术的协议。RPC它假定某些协议的存在,例如TPC/UDP等,为通信程序之间携带信息数据。在OSI网络七层模型中,RPC跨越了传输层和应用层,RPC使得开发,包括网络分布式多程序在内的应用程序更加容易。
过程是什么? 过程就是业务处理、计算任务,更直白的说,就是程序,就是想调用本地方法一样调用远程的过程。

2、为什么会出现RPC

RPC的概念与技术早在1981年由Nelson提出。1984年,Birrell和Nelson把其用于支持异构型分布式系统间的通讯。Birrell的RPC 模型引入存根进程( stub) 作为远程的本地代理,调用RPC运行时库来传输网络中的调用。Stub和RPC runtime屏蔽了网络调用所涉及的许多细节,特别是,参数的编码/译码及网络通讯是由stub和RPC runtime完成的,因此这一模式被各类RPC所采用。由于分布式系统的异构性及分布式计算模式与计算任务的多样性,RPC作为网络通讯与委托计算的实现机制,在方法、协议、语义、实现上不断发展,种类繁多,其中SUN公司和开放软件基金会在其分布式产品中所建立和实用的RPC较为典型。
在SUN公司的网络文件系统NFS及开放网络计算环境ONC中,RPC是基本实现技术。OSF酝酿和发展的另一个重要的分布式计算软件环境DCE也是基于RPC的。在这两个系统中,RPC既是其自身的实现机制,又是提供给用户设计分布式应用程序的高级工具。由于对分布式计算的广泛需求,ONC和DCE成为Client/Server模式分布式计算环境的主流产品,而RPC也成为实现分布式计算的事实标准之一。
摘抄自:百度文库https://baike.baidu.com/item/%E8%BF%9C%E7%A8%8B%E8%BF%87%E7%A8%8B%E8%B0%83%E7%94%A8/7854346?fromtitle=RPC&fromid=609861&fr=aladdin

同时微服务的出现也进一步促进了RPC的发展,我们知道在微服务当道的今天。众多个微服务之间需要合作才能完成业务,例如“订单服务”需要调用“用户服务”的某个接口,这个场景就非常适合RPC(当然了用Http请求也是可以的)

二、RPC需要解决的问题

1、 通信协议?

所谓的协议,可以认为是一种约定,即服务端和客户端定义好数据是如何解析的。由于在网络传输的过程都是比特流(01010101),所以双方需要约定好如何解读这些数据。

2、服务提供方和调用方如何进行通信

通常RPC框架,双方的通信是基于TCP协议的。

3、调用方如何知道服务提供方

方法有很多,
1、比如最简单的服务的提供方将服务信息写入数据库,调用每次去查询。当然这个方案显然是不可能的,抛开性能问题不谈,绝大多数场景这两者并不能使用同一个数据库。
2、利用一些中间件,比如ZK就非常适合。在ZK中存储服务端暴露的信息,同时客户端可以通过添加监听器来感知服务端信息的变化。

4、如何高效的序列化和反序列化

这里引用一下其他文章:https://zhuanlan.zhihu.com/p/367295821

三、手写一个简单的PRC框架

在这里插入图片描述

上图是一个简单的RPC调用架构图,当然了实际上会更复杂。结合小结说的RPC要解决的问题,我们罗列一些一个PRC框架所需要的技术。
1、我们希望服务端(生产者)的信息不要写死,客户端(消费者)可以从某个地方动态的获取到服务端的消息,同时服务端如果宕机了,客户端可以感知到,从而不再去调用宕机的服务端接口。当然可以实现这种功能的技术有很多,不过首先想到的就是Zookeeper。所以我们第一个技术选型将Zookeeper作为注册中心。
2、既然PRC是远程调用,那么肯定离不开网络。比如我们可以用Http去实现我们的远程调用,不过相对来说性能会差一些。所以考虑到性能方面,我们可以自己写一个网络模块。提到网络通信,我们很自然的想到了Netty,Netty作为一个使用简单的NIO的高性能框架,可以快速编写服务端程序。
3、我们知道网络通信的过程中,我们的数据都是二进制,0101010的形式,但是在咱们业务上都是以具体的实体类来使用。所以我们需要有一些列的编解码器,根据一定的协议(规范)来解析网络的数据流,当我们根据协议拿到了数据流后,在业务上我们是不能直接使用的所以需要进行返序列化。提到返序列化,第一个想到的就是JDK自带的序列化,JDK自带的序列化有一定的局限性:1、效率相对较低;2、不支持跨平台。所以不使用JDK自带的序列化工具,这里我们使用protobuf 这个框架来实现序列化和反序列化。
4、由于当下多是Spring或者Springboot的项目,所以我们也使用Sping作为项目容器。

小结:

至此手写一个简单的RPC框架所需要的技术点已经够了,话不多说让我们开始coding吧。

四、搭建项目框架

1、项目分层

在PRC中有服务端(生产者)和客户端(消费者)这两种角色,所以基于这个考虑,我们把项目进行拆分。总共分为3个模块:

  • Server模块:主要负责将暴露的接口信息上报到注册中心中供消费者调用。
  • Core模块:PRC核心功能、包括网络IO、编解码器、缓存等等一些列功能。
  • Client模块:负责生成代理,调用实际接口,并处理响应等。

整体结构如下
image.png

2、POM文件

TIPS:一开始依赖并非完整的,随着项目的开发逐步完善。
1、父工程

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.cmxy</groupId><artifactId>yrpc</artifactId><version>1.0-SNAPSHOT</version><modules><module>yrpc-client</module><module>yrpc-core</module><module>yrpc-server</module></modules><packaging>pom</packaging><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version><spring.version>5.1.4.RELEASE</spring.version><cglib.version>3.1</cglib.version><netty.version>4.1.42.Final</netty.version><zkclient.version>0.1</zkclient.version><objenesis.version>2.6</objenesis.version><protostuff.version>1.6.0</protostuff.version><slf4j.log4j.version>1.7.25</slf4j.log4j.version><guava.version>19.0</guava.version><reflections.version>0.9.10</reflections.version><beanutils.version>1.9.3</beanutils.version><commons.lang3.version>3.6</commons.lang3.version><commons.collections.version>3.2.2</commons.collections.version></properties><dependencyManagement><dependencies><!-- zookeeper客户端组件依赖 --><dependency><groupId>com.github.sgroschupf</groupId><artifactId>zkclient</artifactId><version>${zkclient.version}</version></dependency><!-- Netty 组件依赖 --><dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>${netty.version}</version></dependency><!-- 实例化组件依赖 --><dependency><groupId>org.objenesis</groupId><artifactId>objenesis</artifactId><version>${objenesis.version}</version></dependency><!-- protostuff 核心依赖 --><!--基于google protobuf的工具类 protostuff--><dependency><groupId>io.protostuff</groupId><artifactId>protostuff-core</artifactId><version>${protostuff.version}</version></dependency><dependency><groupId>io.protostuff</groupId><artifactId>protostuff-runtime</artifactId><version>${protostuff.version}</version></dependency><!-- spring 上下文组件依赖 --><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>${spring.version}</version></dependency><!-- 日志组件依赖 --><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>${slf4j.log4j.version}</version></dependency><!-- Google Guava 核心扩展库--><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>${guava.version}</version></dependency><!-- Apache 集合 扩展依赖 --><dependency><groupId>commons-collections</groupId><artifactId>commons-collections</artifactId><version>${commons.collections.version}</version></dependency><!-- Apache lang 包扩展依赖 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>${commons.lang3.version}</version></dependency><!-- Apache BeanUtils 辅助工具依赖 --><dependency><groupId>commons-beanutils</groupId><artifactId>commons-beanutils</artifactId><version>${beanutils.version}</version></dependency><!-- cglib动态代理依赖--><dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>${cglib.version}</version></dependency><!-- Java元数据分析反射依赖--><dependency><groupId>org.reflections</groupId><artifactId>reflections</artifactId><version>${reflections.version}</version></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.16.22</version><optional>true</optional></dependency></dependencies><build><pluginManagement><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.3</version><configuration><source>${java.version}</source><target>${java.version}</target><encoding>UTF-8</encoding></configuration></plugin></plugins></pluginManagement><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.3</version><configuration><source>${java.version}</source><target>${java.version}</target><encoding>UTF-8</encoding></configuration></plugin></plugins></build>
</project>

2、Core模块

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>yrpc</artifactId><groupId>com.cmxy</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>yrpc-core</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!-- spring 上下文组件依赖 --><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId></dependency><!-- Netty 通讯依赖--><dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId></dependency><!-- zookeeper客户端依赖 --><dependency><groupId>com.github.sgroschupf</groupId><artifactId>zkclient</artifactId></dependency><!--基于google protobuf的工具类 protostuff--><dependency><groupId>io.protostuff</groupId><artifactId>protostuff-core</artifactId></dependency><dependency><groupId>io.protostuff</groupId><artifactId>protostuff-runtime</artifactId></dependency><!-- Apache 集合 扩展依赖 --><dependency><groupId>commons-collections</groupId><artifactId>commons-collections</artifactId></dependency><!-- Apache lang 包扩展依赖 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><!-- Apache BeanUtils 辅助工具依赖 --><dependency><groupId>commons-beanutils</groupId><artifactId>commons-beanutils</artifactId></dependency><!-- Google Guava 核心扩展库--><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId></dependency><!-- 日志组件依赖 --><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId></dependency></dependencies></project>

3、Server和Client模块只需要引入Core模块即可(目前是这样)

3、开发Server模块

首先我们要明确各个模块的作用,然后从由简到难的开发。相比之下Server端会比较简单。理由如下:
服务端只需暴露接口,处理接受请求处理响应基本上就可以了。但是作为客户端来说,需要处理的就比较多了,维护服务端提暴露的服务列表(本地缓存)、负载均衡(简单的来说就是服务发现)、生成代理类、失败重试等等一系列,所以我们先开发服务端。
在开发之前我们需要罗列出服务端要做的事情:

  1. 扫描需要暴露的接口
  2. 将暴露的接口保存到注册中心
  3. 处理网络连接,收到请求然后处理响应。

简单的来说RPC 服务端最基本的功能就是这几个,接下来我们逐一实现。

通常情况下我们会自定义一个注解,有该注解的接口我们认为是需要提供给外部使用的。所以我们在Core模块中定义一个最简单的注解,名字就叫YRpcService

package com.cmxy.rpc.annotation;import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;/*** @Author hardy(叶阳华)* @Description* @Date 2023/5/24 10:40*/@Component
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface YRpcService {/*** 等同于@Component的value* @return*/@AliasFor(annotation = Component.class)String value() default "";/*** 服务接口Class* @return*/Class<?> interfaceClass() default void.class;/*** 服务接口名称* @return*/String interfaceName() default "";/*** 服务版本号* @return*/String version() default "";/*** 服务分组* @return*/String group() default "";}
由于当下基本上都是Spring环境,所以我们也利用Spring的特性。将该注解也认定是Spring的一个Component。接下来我们编写一个服务端的初始化类(Server模块下)

在这里插入图片描述

package com.cmxy.rpc.server.registry.zk;import com.cmxy.rpc.annotation.YRpcService;
import com.cmxy.rpc.server.config.zk.RpcServerConfiguration;
import com.cmxy.rpc.server.registry.Registry;
import com.cmxy.rpc.util.IpUtil;
import com.cmxy.rpc.util.SpringApplicationUtil;
import java.util.Map;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;/*** @Author hardy(叶阳华)* @Description* @Date 2023/5/24 10:50*/
@Slf4j
public class ZkRegister implements Registry {@Resourceprivate ServerZKit serverZKit;@Resourceprivate RpcServerConfiguration rpcServerConfiguration;@Resourceprivate SpringApplicationUtil springApplicationUtil;/*** 基于ZK实现的服务注册: 1、扫描所有需要暴露的接口:即携带了YRpcService的接口 2、将接口信息注册到ZK中*/@Overridepublic void register() {//1、扫描出携带了YRpcService注解的类final Map<String, Object> serviceMap = SpringApplicationUtil.getBeanListByAnnotationClass(YRpcService.class);if (serviceMap.isEmpty()) {log.info("暂无需要暴露的接口,结束注册");return;}//创建根目录serverZKit.createRootNode();//2、将接口信息写入ZK:path:ServiceBean的名称 data:IP+端口号//注意这里创建的是临时节点:以确保当前节点不可用的时候ZK上自动删除当前的节点信息serviceMap.forEach((beanName, serviceBean) -> {//获取当前Bean上的注解,通过注解final YRpcService yRpcService = serviceBean.getClass().getAnnotation(YRpcService.class);//获取接口final Class<?> serviceClass = yRpcService.interfaceClass();//创建服务层节点:例如 com.example.service.impl.testImplString serviceName = serviceClass.getName();serverZKit.createPersistentNode(serviceName);//获取服务器IPString ip = IpUtil.getRealIp();//获取端口号:注意这里是RPC端端口号,不是服务端的端口号(因为通信是RPC框架)Integer port = rpcServerConfiguration.getRpcPort();String path = serviceClass.getName();//创建临时节点serverZKit.createEphemeralNode(serviceName + "/" + ip + ":" + port);log.info("服务:{} 注册成功,ip:{} 端口:{}", serviceName, ip, port);});}
}

代码解释:上述代码是为了将暴露的接口保存到注册中心,步骤如下

  1. 首先在Spring容器中查询出含有YPrcService注解的的类
  2. 创建根节点(根据配置)
  3. 拿到Service后,根据注解上配置的“接口属性”在ZK中创建节点
  4. 拿到当前的IP和端口号,在点不创建的节点下 创建临时节点(为什么是临时节点,上面注释中有)
  5. 完成注册

4、工具类代码

1、ServerZKit

package com.cmxy.rpc.server.registry.zk;import com.cmxy.rpc.server.config.zk.RpcServerConfiguration;
import org.I0Itec.zkclient.ZkClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;/*** Zookeeper连接操作接口*/
@Component
public class ServerZKit {@Autowiredprivate ZkClient zkClient;@Autowiredprivate RpcServerConfiguration rpcServerConfiguration;/**** 根节点创建*/public void createRootNode() {boolean exists = zkClient.exists(rpcServerConfiguration.getZkRoot());if (!exists) {zkClient.createPersistent(rpcServerConfiguration.getZkRoot());}}/**** 创建其他节点* @param path*/public void createPersistentNode(String path) {String pathName = rpcServerConfiguration.getZkRoot() + "/" + path;boolean exists = zkClient.exists(pathName);if (!exists) {zkClient.createPersistent(pathName);}}/**** 创建临时节点* @param path*/public void createEphemeralNode(String path) {String pathName = rpcServerConfiguration.getZkRoot() + "/" + path;boolean exists = zkClient.exists(pathName);if (!exists) {zkClient.createEphemeral(pathName);}}
}

2、SpringFactory工具

package com.cmxy.rpc.util;import java.lang.annotation.Annotation;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;/*** @Author hardy(叶阳华)* @Description* @Date 2023/5/24 10:42*/
@Component
public class SpringApplicationUtil implements ApplicationContextAware {private static ApplicationContext applicationContext;@Overridepublic void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {SpringApplicationUtil.applicationContext = applicationContext;}public static Object getBean(String className) {return applicationContext.getBean(className);}public static <T> T getBean(Class<T> clazz) {return applicationContext.getBean(clazz);}/**** 获取有指定注解的对象* @param annotationClass* @return*/public static Map<String, Object> getBeanListByAnnotationClass(Class<? extends Annotation> annotationClass) {return applicationContext.getBeansWithAnnotation(annotationClass);}}

3、配置类

package com.cmxy.rpc.server.config.zk;import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;@Data
@Component
public class RpcServerConfiguration {/*** ZK根节点名称*/@Value("${rpc.server.zk.root}")private String zkRoot;/*** ZK地址信息*/@Value("${rpc.server.zk.addr}")private String zkAddr;/*** RPC通讯端口*/@Value("${rpc.network.port}")private int rpcPort;/*** Spring Boot 服务端口*/@Value("${server.port}")private int serverPort;/*** ZK连接超时时间配置*/@Value("${rpc.server.zk.timeout:10000}")private int connectTimeout;
}

五、小结

本文我们讲述了什么是RPC,以及RPC所解决的问题、需要的技术点。最后我们准备做一个简单的RPC框架,开发了服务端的一部分内容。接下里的我们不断完善这个框架。希望对你有所帮助,未完待续。。。。


http://www.ppmy.cn/news/90618.html

相关文章

linux系统采集方法

Linux系统是一个非常流行的操作系统&#xff0c;广泛应用于服务器和嵌入式设备。在Linux环境中&#xff0c;我们可以使用各种工具来收集信息、监控系统和获得实时性能数据。本文将介绍一些用于采集系统信息的常用工具。 一、系统命令 top&#xff1a;用来查看系统各项资源使用…

Python实现最小公约数和最小公倍数

1. 题目 最大公约数计算。从键盘接收两个整数&#xff0c;编写程序求出这两个整数的最大公约数和最小公倍数。&#xff08;提示&#xff1a;用辗转相除法求最大公约数&#xff09; 2. 知识点讲解 2.2 最大公约数是什么&#xff1f; 最大公约数&#xff0c;英文为 Greatest …

基于Java的论坛管理系统的设计与实现

摘 要 Java论坛管理系统主要是实现在网上进行交流和学习Java语言,本论坛提供用户注册、发帖和回帖等论坛的基本功能,划分J2SE、J2EE、J2ME和开发工具的使用等版块,使用合理的管理方法管理论坛,严格保证帖子质量,为Java爱好者提供一个学习Java的好地方。本系统采用SQL2000…

Druid连接池技术实践

什么是Druid连接池&#xff1f; Druid连接池是阿里巴巴开源的数据库连接池项目。 Druid连接池为监控而生&#xff0c;内置强大的监控功能&#xff0c;监控特性不影响性能。功能强大&#xff0c;能防SQL注入&#xff0c;内置Loging能诊断Hack应用行为。 哦&#xff0c;首先Dru…

Java选择题集合(三)(更新中)

说明&#xff1a;这里是怀化学院java考试平台题库里的题&#xff0c;为了方便将题整理出来&#xff0c;但题库还在不断更新中&#xff0c;故此集合整理的题最新截止至发布时间&#xff0c;我将正确答案用红色粗体标注&#xff0c;但这里大概有226道选择题&#xff0c;我将题集分…

迅为RK3568开发板Android 双屏/三屏同显

iTOP-RK3568 开发板支持以下屏幕 迅为 LVDS 7 寸屏幕 迅为 LVDS 10.1 寸 1024*600 屏幕 迅为 LVDS 10.1 寸 1280*800 屏幕 迅为 MIPI 7 寸屏幕 HDMI 屏幕&#xff08;通过 HDMI 线连接&#xff09; HDMI 屏幕&#xff08;通过 VGA 线连接&#xff09; 然后修改 Android1…

隐形黑客潜入美国和关岛关键基础设施而未被发现

微软和“五眼联盟”国家周三表示&#xff0c;一个隐秘的组织成功地在美国和关岛的关键基础设施组织中建立了一个持久的立足点&#xff0c;而没有被发现。 这家科技巨头的威胁情报团队正在以伏特台风(Volt Typhoon)的名义跟踪这些活动&#xff0c;包括入侵后的凭证访问和网络系…

2023年6月合肥/厦门/长春/深圳DAMA-CDGP数据治理专家认证报名

目前6月18日CDGA&CDGP考试目前开放的城市有&#xff1a;北京、上海、广州(满)、深圳、长沙、呼和浩特、杭州&#xff08;满&#xff09;、南京、济南&#xff08;满&#xff09;、成都、西安、武汉&#xff08;满&#xff09;、天津。 新增了武汉、天津这2个城市。另外合肥…