Simple RPC - 06 从零开始设计一个服务端(上)_注册中心的实现

news/2024/10/19 6:19:24/

文章目录

在这里插入图片描述

Pre

Simple RPC - 01 框架原理及总体架构初探

Simple RPC - 02 通用高性能序列化和反序列化设计与实现

Simple RPC - 03 借助Netty实现异步网络通信

Simple RPC - 04 从零开始设计一个客户端(上)

Simple RPC - 05 从零开始设计一个客户端(下)_ 依赖倒置和SPI


核心内容

  1. 服务端结构概述注册中心和RPC服务的结构及作用。
  2. 注册中心实现:通过单机版的注册中心实现共享元数据,分析其接口设计和SPI机制。
  3. RPC服务实现:理解服务端处理RPC请求的核心逻辑,包括如何注册服务和处理请求。
  4. 请求分发机制:深入了解RequestInvocation和RpcRequestHandler类中的请求分发机制。
  5. 代码分析与总结:通过代码实例进一步理解设计思想,并总结整体架构和设计原则。

服务端结构概述

在RPC框架中,服务端可以分为两个主要部分:注册中心RPC服务

  • 注册中心:负责管理服务元数据,并提供服务发现的功能。
  • RPC服务:负责处理客户端发来的RPC请求,并调用相应的业务服务。

简单来说:注册中心的作用是帮助客户端来寻址,找到对应 RPC 服务的物理地址;RPC 服务用于接收客户端桩的请求,调用业务服务的方法,并返回结果。


注册中心的实现

1. 注册中心的架构

通常,一个完整的注册中心包括客户端服务端两部分:

  • 客户端:向调用方提供 API,负责与注册中心服务端的通信。

  • 服务端:实际管理和记录每个 RPC 服务的注册信息,并将这些信息存储在元数据中。当客户端需要查找服务时,服务端会返回对应的服务地址。

在本例中,出于简化考虑,我们实现了一个单机版注册中心。这个注册中心只有客户端部分,多个客户端通过读写同一个本地元数据文件实现服务信息的共享。

注册中心只能在单机环境下运行,不支持跨服务器调用。

2. 面向接口编程的设计

尽管当前实现是单机版的注册中心,但通过“面向接口编程”的设计模式,我们可以在不修改已有代码的情况下,通过 SPI 插件机制,扩展出一个支持跨服务器调用的注册中心(例如,基于 HTTP 协议的实现)。


3. 注册中心的接口设计

在 RPC 框架的接入点接口 RpcAccessPoint 中,增加了一个用于获取注册中心实例的方法:

public interface RpcAccessPoint extends Closeable {/*** 获取注册中心的引用* @param nameServiceUri 注册中心 URI* @return 注册中心引用*/NameService getNameService(URI nameServiceUri);
}
  • 该方法接受一个注册中心的 URI 作为参数,并返回一个 NameService 接口的实例。这个 NameService 接口表示注册中心的客户端,可以用来和注册中心服务端通信。

NameService 接口中定义了与注册中心通信的核心方法:

public interface NameService {/*** 返回所有支持的协议*/Collection<String> supportedSchemes();/*** 连接注册中心* @param nameServiceUri 注册中心地址*/void connect(URI nameServiceUri);
}
  • supportedSchemes 方法返回注册中心支持的协议(例如 filehttp)。
  • connect 方法根据 URI 建立与注册中心的连接。

完整代码如下

/*** 注册中心接口定义* 该接口用于服务的注册和发现,支持多种通信协议** @author artisan*/
public interface NameService {/*** 获取所有支持的协议** @return 支持的协议集合*/Collection<String> supportedSchemes();/*** 连接注册中心** @param nameServiceUri 注册中心的URI地址*/void connect(URI nameServiceUri);/*** 注册服务** @param serviceName 服务名称* @param uri 服务的URI地址* @throws IOException 如果连接或注册失败,则抛出此异常*/void registerService(String serviceName, URI uri) throws IOException;/*** 查询服务地址** @param serviceName 服务名称* @return 服务的URI地址* @throws IOException 如果查找失败,则抛出此异常*/URI lookupService(String serviceName) throws IOException;
}

4. SPI机制的应用

通过 SPI 机制,RpcAccessPoint 可以根据 URI 中指定的协议,动态加载不同的 NameService 实现类。例如,在单机版注册中心中,NameService 的实现类是 LocalFileNameService,其具体功能是读写本地文件,存储和查找服务信息。

public class LocalFileNameService implements NameService {@Overridepublic Collection<String> supportedSchemes() {return Collections.singleton("file");}@Overridepublic void connect(URI nameServiceUri) {// 连接到本地文件,初始化文件读写工具}// 其他方法实现...
}

通过这种方式,新的注册中心实现可以通过 SPI 动态添加到系统中。例如,要实现一个基于 HTTP 的注册中心,只需开发一个新的 NameService 实现类,并将其添加到系统的 CLASSPATH 中即可。


LocalFileNameService代码如下


/*** LocalFileNameService 类实现了 NameService 接口,提供了一种基于文件系统来管理服务名称和URI的实现方式* 它使用 "file" 协议来操作本地文件,并将服务信息存储在文件中* @author artisan*/
public class LocalFileNameService implements NameService {private static final Logger logger = LoggerFactory.getLogger(LocalFileNameService.class);/*** 支持的协议集合,本实现仅支持 "file" 协议*/private static final Collection<String> schemes = Collections.singleton("file");/*** 用于存储服务信息的文件对象*/private File file;/*** 返回此服务支持的协议集合** @return 支持的协议集合*/@Overridepublic Collection<String> supportedSchemes() {return schemes;}/*** 连接到指定的名称服务URI,如果支持该URI的协议,则将URI解析为本地文件* 此方法首先检查给定的URI是否使用受支持的协议如果协议受支持,则将URI转换为本地文件路径* 如果不支持该协议,则抛出运行时异常** @param nameServiceUri 名称服务的URI,用于连接和解析* @throws RuntimeException 如果URI的协议不受支持,则抛出此异常*/@Overridepublic void connect(URI nameServiceUri) {// 检查URI的协议是否在支持的协议列表中if (schemes.contains(nameServiceUri.getScheme())) {// 如果协议受支持,则将URI转换为本地文件路径file = new File(nameServiceUri);} else {// 如果协议不受支持,则抛出异常throw new RuntimeException("Unsupported scheme!");}}/*** 注册服务,将服务名称和服务URI写入到文件中* 此方法是同步的,以确保并发访问时的数据一致性** @param serviceName 服务名称* @param uri 服务的URI* * @throws IOException 如果发生I/O错误*/@Overridepublic synchronized void registerService(String serviceName, URI uri) throws IOException {// 记录服务注册的日志信息logger.info("Register service: {}, uri: {}.", serviceName, uri);// 使用RandomAccessFile和FileChannel来读写文件,并确保资源在使用后能够正确关闭try (RandomAccessFile raf = new RandomAccessFile(file, "rw");FileChannel fileChannel = raf.getChannel()) {// 获取文件锁,以确保并发访问时的数据一致性FileLock lock = fileChannel.lock();try {// 获取文件长度,用于后续判断文件是否为空int fileLength = (int) raf.length();Metadata metadata;byte[] bytes;// 如果文件长度大于0,说明文件非空,读取并解析文件内容if (fileLength > 0) {bytes = new byte[(int) raf.length()];ByteBuffer buffer = ByteBuffer.wrap(bytes);// 循环读取文件内容到ByteBuffer中while (buffer.hasRemaining()) {fileChannel.read(buffer);}// 解析字节码为Metadata对象metadata = SerializeSupport.parse(bytes);} else {// 如果文件为空,创建一个新的Metadata对象metadata = new Metadata();}// 根据服务名获取或创建一个空的URI列表List<URI> uris = metadata.computeIfAbsent(serviceName, k -> new ArrayList<>());// 如果列表中不存在该URI,则添加进去if (!uris.contains(uri)) {uris.add(uri);}// 记录更新后的Metadata信息logger.info(metadata.toString());// 将Metadata对象序列化为字节码bytes = SerializeSupport.serialize(metadata);// 清空文件,为写入新的字节码做准备fileChannel.truncate(bytes.length);// 将文件指针移到文件开头,准备写入fileChannel.position(0L);// 将字节码写入文件fileChannel.write(ByteBuffer.wrap(bytes));// 强制将写入操作刷入磁盘fileChannel.force(true);} finally {// 释放文件锁lock.release();}}}/*** 根据服务名称查找服务的URI* 如果文件中存在对应的服务URI,则随机返回一个** @param serviceName 服务名称* @return 服务的URI,如果找不到则返回null* @throws IOException 如果发生I/O错误*/@Overridepublic URI lookupService(String serviceName) throws IOException {Metadata metadata;// 使用try-with-resources语句确保文件资源正确关闭try (RandomAccessFile raf = new RandomAccessFile(file, "rw");FileChannel fileChannel = raf.getChannel()) {// 获取文件锁以确保数据的一致性FileLock lock = fileChannel.lock();try {// 读取文件内容到字节数组byte[] bytes = new byte[(int) raf.length()];ByteBuffer buffer = ByteBuffer.wrap(bytes);// 循环读取直到文件末尾while (buffer.hasRemaining()) {fileChannel.read(buffer);}// 如果文件非空,则反序列化为Metadata对象,否则创建新的空Metadata对象metadata = bytes.length == 0 ? new Metadata() : SerializeSupport.parse(bytes);// 记录日志logger.info(metadata.toString());} finally {// 释放文件锁lock.release();}}// 从Metadata中获取服务的所有URIList<URI> uris = metadata.get(serviceName);// 如果没有找到对应的URI列表,返回nullif (null == uris || uris.isEmpty()) {return null;} else {// 随机选择一个URI返回return uris.get(ThreadLocalRandom.current().nextInt(uris.size()));}}
}

小结

  • 面向接口编程:设计时面向接口编程,使得系统具有良好的扩展性,可以通过增加 SPI 插件方式扩展新的功能。
  • 单机版注册中心:当前实现的是一个单机版的注册中心,通过本地文件共享元数据,不支持跨服务器调用。
  • SPI机制:通过 SPI 机制,可以动态加载不同的 NameService 实现,支持多种协议的注册中心

这种设计模式确保了系统的灵活性可扩展性,为后续的功能扩展提供了便利。

在这里插入图片描述


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

相关文章

Redis 的 List 结构非常适合用于实现消息队列php

1. Redis List 结构消息队列简介 Redis 的 List 结构非常适合用于实现消息队列。你可以通过 LPUSH 或 RPUSH 命令将消息推入队列&#xff0c;通过 BLPOP 或 BRPOP 命令从队列中弹出消息。BLPOP 和 BRPOP 命令支持阻塞操作&#xff0c;适合在消费者端等待消息的到来。 2. 实现…

Docker Compose安装和部署Airflow

要在Docker中安装和部署Airflow&#xff0c;您可以按照以下步骤进行。Airflow是一个流行的工作流管理工具&#xff0c;使用Docker可以轻松设置和运行Airflow环境。 1. 安装Docker和Docker Compose 首先&#xff0c;确保您的系统上已经安装了Docker和Docker Compose。如果没有…

杜占朋人物风采

杜占朋&#xff0c;衡水名校校长&#xff0c;一位荣获全国杰出青年称号的杰出教育家&#xff0c;同时也是全国范围内备受尊崇的红色基因传承者。他以其卓越的学术成就、丰富的实践经验以及不懈的教育创新精神&#xff0c;成为了当代教育领域的璀璨明星。他身兼数职&#xff0c;…

走向绿色:能源新选择,未来更美好

当前&#xff0c;全球范围内可再生能源正经历着从辅助能源向核心能源的深刻转型&#xff0c;绿色能源日益渗透至居住、出行、日常应用等多个领域&#xff0c;深刻影响着我们的生活方式&#xff0c;使我们能够更加充分地体验清洁能源所带来的优质生活。 一、绿色能源与“住” …

第二百零三节 Java正则表达式教程 - Java正则表达式匹配

Java正则表达式教程 - Java正则表达式匹配 Matcher 类对字符序列执行匹配通过解释在 Pattern 对象中定义的编译模式。 Pattern 类的 matcher()方法创建一个实例的 Matcher 类。 import java.util.regex.Matcher; import java.util.regex.Pattern;public class Main {public s…

30道python自动化测试面试题与答案汇总!

Python是不可或缺的语言,它的优美与简洁令人无法自拔,下面这篇文章主要给大家介绍了关于30道python自动化测试面试题与答案汇总的相关资料,需要的朋友可以参考下 1、什么项目适合做自动化测试&#xff1f; 关键字&#xff1a;不变的、重复的、规范的 1&#xff09;任务测试明…

软件设计师全套备考系列文章6 -- 线性表、栈和队列、串、数组、矩阵、广义表

软考-- 软件设计师&#xff08;6&#xff09;-- 线性表、栈和队列、串、数组、矩阵、广义表 文章目录 软考-- 软件设计师&#xff08;6&#xff09;-- 线性表、栈和队列、串、数组、矩阵、广义表前言一、线性表二、栈和队列三、串、数组、矩阵、广义表 前言 考试时间&#xff…

二叉树 - 二叉树的层序遍历

二叉树的层序遍历 102. 二叉树的层序遍历 /*** Definition for a binary tree node.* function TreeNode(val, left, right) {* this.val (valundefined ? 0 : val)* this.left (leftundefined ? null : left)* this.right (rightundefined ? null : right)…