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

news2024/9/23 21:21:02

文章目录

  • Pre
  • 核心内容
  • 服务端结构概述
  • 注册中心的实现
    • 1. 注册中心的架构
    • 2. 面向接口编程的设计
    • 3. 注册中心的接口设计
    • 4. SPI机制的应用
  • 5. 小结

在这里插入图片描述

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 {
    @Override
    public Collection<String> supportedSchemes() {
        return Collections.singleton("file");
    }

    @Override
    public 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 支持的协议集合
     */
    @Override
    public Collection<String> supportedSchemes() {
        return schemes;
    }

    /**
     * 连接到指定的名称服务URI,如果支持该URI的协议,则将URI解析为本地文件
     * 此方法首先检查给定的URI是否使用受支持的协议如果协议受支持,则将URI转换为本地文件路径
     * 如果不支持该协议,则抛出运行时异常
     *
     * @param nameServiceUri 名称服务的URI,用于连接和解析
     * @throws RuntimeException 如果URI的协议不受支持,则抛出此异常
     */
    @Override
    public 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错误
     */
    @Override
    public 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错误
     */
    @Override
    public 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中获取服务的所有URI
        List<URI> uris = metadata.get(serviceName);
        // 如果没有找到对应的URI列表,返回null
        if (null == uris || uris.isEmpty()) {
            return null;
        } else {
            // 随机选择一个URI返回
            return uris.get(ThreadLocalRandom.current().nextInt(uris.size()));
        }
    }
}


5. 小结

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

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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2050566.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【与C++的邂逅】--- 类和对象(上)

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; 与C的邂逅 本篇博客将讲解C中的类和对象&#xff0c;C是面向对象的语言&#xff0c;面向对象三大特性是封装,继承,多态。学习类和对象&#xff0c;我们可…

Adobe Dimension DN v4.0.2 解锁版下载和安装教程 (专业的三维3D建模工具)

前言 Adobe Dimension&#xff08;简称DN&#xff09;是一款3D设计软件&#xff0c;三维合成和渲染工具&#xff0c;2D平面的二维转为3D立体的三维合成工具&#xff0c;用于3Dmax\C4D\MAYA等三维软件生成的效果图&#xff0c;在3D场景中排列对象、图形和光照。3D应用程序使用的…

Nginx实验

编译安装 Nginx 准备rhel9环境 下载安装包nginx-1.24.0&#xff08;xftp&#xff09;/复制下载链接 &#xff08;nginx.org——>download&#xff09; 解压 [rootnginx nginx-1.24.0]# tar zxf nginx-1.24.0.tar.gz [rootnginx nginx-1.24.0]#tar zxf nginx-1.24.0.tar.…

yolov8安装教程

一、资源下载 1.下载YOLOv8代码 github:YOLOv8-github gitee:YOLOv8-gitee&#xff08;推荐使用国内的gitee&#xff09; 2.conda、cuda 如果没有安装conda&#xff0c;按照流程安装好conda&#xff0c;还要下载好符合自己电脑版本的CUDA 后续会用。 二、创建conda虚拟环…

C语言典型例题43

《C程序设计教程&#xff08;第四版&#xff09;——谭浩强》 习题3.3 有一个函数&#xff1a;y{x,x<1;2x-1,1≤x≤10;3x-11,x≥10。写程序&#xff0c;输入x&#xff0c;输出y。 代码&#xff1a; //《C程序设计教程&#xff08;第四版&#xff09;——谭浩强》 //习题3.3…

OD C卷 - 传递悄悄话

传递悄悄话 &#xff08;100&#xff09; 给定一个二叉树&#xff0c;节点采用顺序存储&#xff0c;如 i0 表示根节点&#xff0c;2i 1 表示左子树根&#xff0c;2i 2 表示右子树根;每个节点站一个人&#xff0c;节点数值表示由父节点到该节点传递消息需要的时间&#xff1b…

周末休整

我写的东西&#xff0c;不爱看的人可以不看&#xff0c;我是给喜欢我的人写的&#xff0c;不喜欢我的人&#xff0c;我也讨厌她。 今天故意写点教人学坏的东西&#xff0c;因为以前写了很多正能量的东西&#xff0c;虽然阅读量还可以&#xff0c;但当见面聊天之后&#xff0c;…

【CSS】CSS新单位vmin和vmax

通过vmin单位可以自动取视口宽度和高度中较小的那个值&#xff0c;vmax同理。 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1…

百度智能云通用文字识别(标准版)- java.lang.NoSuchFieldError: Companion

需求环境 ORC识别图片信息 参考百度示例 百度智能云API文档通用文字识别 官方示例 package baidu.com;import okhttp3.*; import org.json.JSONObject;import java.io.*;/*** 需要添加依赖* <!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp -->…

8.16-ansible的应用

ansible ansible是基于模块工作的&#xff0c;本身没有批量部署的能力。真正具有批量部署的是ansible所运行的模块&#xff0c;ansible只是提供一种框架。 格式 ansible 主机ip|域名|组名|别名 -m ping|copy|... 参数 1.ping模块 m0 # 查看有没有安装epel ​ [rootm0 ~]#…

在vue3中配置scss

要在 Vue 3 中使用 SCSS&#xff08;Sass 的一个子集&#xff09;&#xff0c;你需要安装相应的依赖&#xff0c;并对项目进行一些配置。下面是详细的步骤&#xff1a; 步骤 1: 安装依赖 首先&#xff0c;你需要安装 sass 和 vue-loader&#xff08;如果你使用的是 Vue CLI&a…

vue 2.x + @vue/cli 4.x开发阶段使用Vite

项目用的是vue 2.x vue/cli 4.x(webpack 4.x)。冷启动项目时耗时较长&#xff0c;达到了2分钟以上。热更新时间也达到了10s以上。严重影响了开发体验和效率。 解决问题的过程中遵循2个原则&#xff1a; 不对会影响原构建流程的代码进行改动&#xff0c;向原Webpack构建兼容。…

加密创投周期进化论:未来或黯淡,但流动性是那道光

加密创投领域的风起云涌&#xff0c;而在2022年后&#xff0c;加密市场逐渐进入“行业阵痛期”&#xff0c;面对熊市的寒冬&#xff0c;整个行业的流动性急剧减少&#xff0c;市场的发展被迫寄希望于“板块轮动”的交替炒作。尽管如此&#xff0c;比特币生态和L2生态的兴起&…

树莓派开发笔记03-树莓派的GPIO口输入检测

github主页&#xff1a;https://github.com/snqx-lqh gitee主页&#xff1a;https://gitee.com/snqx-lqh 本项目github地址&#xff1a;https://github.com/snqx-lqh/RaspberryPiLearningNotes 本项目gitee地址&#xff1a;https://gitee.com/snqx-lqh/RaspberryPiLearningNote…

LLAMA 3.1 论文的见解

这有什么大不了的&#xff1f; LLAMA 3.1 的发布标志着 AI 开发的一个重要里程碑。开源模型首次接近领先的闭源模型的性能水平。这一转变预示着未来开源模型同样有效&#xff0c;任何人都可以灵活地修改和调整它们。马克扎克伯格将此与 Linux 的开源性质进行了比较&#xff0c;…

Mysql原理与调优-如何进行sql优化

1.绪论 本文主要讲解我们如何优化一个sql。优化的过程主要分为3个步骤&#xff0c;找到哪些sql需要被优化&#xff0c;这就需要用到慢sql日志。然后发现慢SQL为什么慢&#xff0c;即当前sql是如何执行的&#xff0c;这就需要用到执行计划。最后才是对sql进行优化&#xff0c;对…

作业帮 TiDB 7.5.x 使用经验

作者&#xff1a; 是我的海 原文来源&#xff1a; https://tidb.net/blog/5f9784d3 近期在使用 TiDB 时遇到的一些小问题的梳理总结&#xff0c;大部分版本都在6.5.6和7.5.2 1、limit 导致的扫描量过大的优化 研发定时任务每天需要扫描大量数据&#xff0c;到时机器网卡被…

26.10 Django Ajax异步提交

1. 表单提交 1.1 表单的作用 表单是Web开发中常见的数据收集方式, 它允许用户通过表单输入数据, 并通过提交操作将这些数据发送到服务器进行处理.表单提交方式主要分为两大类: 传统的同步提交(也称为标准提交)和异步提交(主要通过Ajax实现). 它们在工作方式, 用户体验和数据传…

语音助手Verbi:科技创新的未来

今天&#xff0c;我要向大家介绍一个名为Verbi的语音助手项目。这是一个结合了多种先进技术的模块化语音助手应用程序&#xff0c;能够实现语音到文本、文本生成和文本到语音的全流程处理。通过这个项目&#xff0c;我们可以体验到尖端科技如何改变我们的日常生活。 Verbi的诞…

PHP安全开发

安全开发 PHP 基础 增&#xff1a;insert into 表名(列名 1, 列名 2) value(‘列 1 值 1’, ‘列 2 值 2’); 删&#xff1a;delete from 表名 where 列名 ‘条件’; 改&#xff1a;update 表名 set 列名 数据 where 列名 ‘条件’; 查&#xff1a;select * from 表名 wher…