一、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模块:负责生成代理,调用实际接口,并处理响应等。
整体结构如下
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端会比较简单。理由如下:
服务端只需暴露接口,处理接受请求处理响应基本上就可以了。但是作为客户端来说,需要处理的就比较多了,维护服务端提暴露的服务列表(本地缓存)、负载均衡(简单的来说就是服务发现)、生成代理类、失败重试等等一系列,所以我们先开发服务端。
在开发之前我们需要罗列出服务端要做的事情:
- 扫描需要暴露的接口
- 将暴露的接口保存到注册中心
- 处理网络连接,收到请求然后处理响应。
简单的来说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);});}
}
代码解释:上述代码是为了将暴露的接口保存到注册中心,步骤如下
- 首先在Spring容器中查询出含有YPrcService注解的的类
- 创建根节点(根据配置)
- 拿到Service后,根据注解上配置的“接口属性”在ZK中创建节点
- 拿到当前的IP和端口号,在点不创建的节点下 创建临时节点(为什么是临时节点,上面注释中有)
- 完成注册
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框架,开发了服务端的一部分内容。接下里的我们不断完善这个框架。希望对你有所帮助,未完待续。。。。