一、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 {
@Resource
private ServerZKit serverZKit;
@Resource
private RpcServerConfiguration rpcServerConfiguration;
@Resource
private SpringApplicationUtil springApplicationUtil;
/**
* 基于ZK实现的服务注册: 1、扫描所有需要暴露的接口:即携带了YRpcService的接口 2、将接口信息注册到ZK中
*/
@Override
public 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.testImpl
String serviceName = serviceClass.getName();
serverZKit.createPersistentNode(serviceName);
//获取服务器IP
String 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 {
@Autowired
private ZkClient zkClient;
@Autowired
private 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;
@Override
public 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框架,开发了服务端的一部分内容。接下里的我们不断完善这个框架。希望对你有所帮助,未完待续。。。。