当前位置: 首页 > news >正文

根据源码,模拟实现 RabbitMQ - 网络通讯设计,实现客户端Connection、Channel(完结)

目录

一、客户端代码实现

1.1、需求分析

1.2、具体实现

1)实现 ConnectionFactory

2)实现 Connection

3)实现 Channel

二、编写 Demo 

2.1、实例 

2.1、实例演示


一、客户端代码实现


1.1、需求分析

RabbitMQ 的客户端设定:一个客户端可以有多个模块,每个模块都可以和 broker server 之间建立 “逻辑上的连接” (channel),这几个模块的channel 彼此之间是互相不影响的,同时这几个 channel 又复用的同一个 TCP 连接,省去了频繁 建立/销毁 TCP 连接的开销(三次握手、四次挥手......).

这里,我们也按照这样的逻辑实现 消息队列 的客户端,主要涉及到以下三个核心类:

  1. ConnectionFactory:连接工厂,这个类持有服务器的地址,主要功能就是创建 Connection 对象.
  2. Connection:表示一个 TCP连接,持有 Socket 对象,用来 写入请求/读取响应,管理多个Channel 对象.
  3. Channel:表示一个逻辑上的连接,需要提供一系列的方法,去和服务器提供的核心 API 对应(客户端提供的这些方法的内部,就是写入了一个特定的请求,然后等待服务器响应).

1.2、具体实现

1)实现 ConnectionFactory

主要用来创建 Connection 对象.

public class ConnectionFactory {//broker server 的 ip 地址private String host;//broker server 的端口号private int port;//    //访问 broker server 的哪个虚拟主机
//    //这里暂时先不涉及
//    private String virtualHostName;
//    private String username;
//    private String password;public Connection newConnection() throws IOException {Connection connection = new Connection(host, port);return connection;}public String getHost() {return host;}public void setHost(String host) {this.host = host;}public int getPort() {return port;}public void setPort(int port) {this.port = port;}
}

2)实现 Connection

属性如下

    private Socket socket;//一个 socket 连接需要管理多个 channelprivate ConcurrentHashMap<String, Channel> channelMap = new ConcurrentHashMap<>();private InputStream inputStream;private OutputStream outputStream;// DataXXX 主要用来 读取/写入 特定格式数据(例如 readInt())private DataInputStream dataInputStream;private DataOutputStream dataOutputStream;//用来处理 0xc 的回调,这里开销可能会很大,不希望把 Connection 阻塞住,因此使用 线程池 来处理private ExecutorService callbackPool;

构造如下

这里不光需要初始化属性,还需要创建一个扫描线程,由这个线程负责不停的从 socket 中读取响应数据,把这个响应数据再交给对应的 channel 负责处理

    public Connection(String host, int port) throws IOException {socket = new Socket(host, port);inputStream = socket.getInputStream();outputStream = socket.getOutputStream();dataInputStream = new DataInputStream(inputStream);dataOutputStream = new DataOutputStream(outputStream);callbackPool = Executors.newFixedThreadPool(4);//创建一个扫描线程,由这个线程负责不停的从 socket 中读取响应数据,把这个响应数据再交给对应的 channel 负责处理Thread t = new Thread(() -> {try {while(!socket.isClosed()) {Response response = readResponse();dispatchResponse(response);}} catch (SocketException e) {//连接正常断开的,此时这个异常可以忽略System.out.println("[Connection] 连接正常断开!");} catch(IOException | ClassNotFoundException | MqException e) {System.out.println("[Connection] 连接异常断开!");e.printStackTrace();}});t.start();}

释放 Connection 相关资源

    public void close() {try {callbackPool.shutdown();channelMap.clear();inputStream.close();outputStream.close();socket.close();} catch (IOException e) {e.printStackTrace();}}

使用这个方法来区别,当前的响应是一个针对控制请求的响应,还是服务器推送过来的消息.

如果是服务器推送过来的消息,就响应表明是 0xc,也就是一个回调,通过线程池来进行处理;

如果只是一个普通的响应,就把这个结果放到 channel 的 哈希表中(随后 channel 会唤醒所有阻塞等待响应的线程,去 map 中拿数据).

    public void dispatchResponse(Response response) throws IOException, ClassNotFoundException, MqException {if(response.getType() == 0xc) {//服务器推送过来的消息数据SubScribeReturns subScribeReturns = (SubScribeReturns) BinaryTool.fromBytes(response.getPayload());//根据 channelId 找到对应的 channel 对象Channel channel = channelMap.get(subScribeReturns.getChannelId());if(channel == null) {throw new MqException("[Connection] 该消息对应的 channel 再客户端中不存在!channelId=" + channel.getChannelId());}//执行该 channel 对象内部的回调(这里的开销未知,有可能很大,同时不希望把这里阻塞住,所以使用线程池来执行)callbackPool.submit(() -> {try {channel.getConsumer().handlerDelivery(subScribeReturns.getConsumerTag(), subScribeReturns.getBasicProperties(),subScribeReturns.getBody());} catch(MqException | IOException e) {e.printStackTrace();}});} else {//当前响应是针对刚才的控制请求的响应BasicReturns basicReturns = (BasicReturns) BinaryTool.fromBytes(response.getPayload());//把这个结果放到 channel 的 哈希表中Channel channel = channelMap.get(basicReturns.getChannelId());if(channel == null) {throw new MqException("[Connection] 该消息对应的 channel 在客户端中不存在!channelId=" + channel.getChannelId());}channel.putReturns(basicReturns);}}

发送请求和读取响应

    /*** 发送请求* @param request* @throws IOException*/public void writeRequest(Request request) throws IOException {dataOutputStream.writeInt(request.getType());dataOutputStream.writeInt(request.getLength());dataOutputStream.write(request.getPayload());dataOutputStream.flush();System.out.println("[Connection] 发送请求!type=" + request.getType() + ", length=" + request.getLength());}/*** 读取响应*/public Response readResponse() throws IOException {Response response = new Response();response.setType(dataInputStream.readInt());response.setLength(dataInputStream.readInt());byte[] payload = new byte[response.getLength()];int n = dataInputStream.read(payload);if(n != response.getLength()) {throw new IOException("读取的响应格式不完整! n=" + n + ", responseLen=" + response.getLength());}response.setPayload(payload);System.out.println("[Connection] 收到响应!type=" + response.getType() + ", length=" + response.getLength());return response;}

在 Connection 中提供创建 Channel 的方法

    public Channel createChannel() throws IOException {String channelId = "C-" + UUID.randomUUID().toString();Channel channel = new Channel(channelId, this);//放到 Connection 管理的 channel 的 Map 集合中channelMap.put(channelId, channel);//同时也需要把 “创建channel” 这个消息告诉服务器boolean ok = channel.createChannel();if(!ok) {//如果创建失败,就说明这次创建 channel 操作不顺利//把刚才加入 hash 表的键值对再删了channelMap.remove(channelId);return null;}return channel;}

Ps:代码中使用了很多次 UUID ,这里我们和之前一样,使用加前缀的方式来进行区分.

3)实现 Channel

属性和构造如下

    private String channelId;// 当前这个 channel 是属于哪一个连接private Connection connection;//用来存储后续客户端收到的服务器响应,已经辨别是哪个响应(要对的上号) key 是 ridprivate ConcurrentHashMap<String, BasicReturns> basicReturnsMap = new ConcurrentHashMap<>();//如果当前 Channel 订阅了某个队列,就需要记录对应的回调是什么,当该队列消息返回回来的时候,调用回调//此处约定一个 Channel 只能有一个回调private Consumer consumer;public Channel(String channelId, Connection connection) {this.channelId = channelId;this.connection = connection;}public String getChannelId() {return channelId;}public void setChannelId(String channelId) {this.channelId = channelId;}public Connection getConnection() {return connection;}public void setConnection(Connection connection) {this.connection = connection;}public ConcurrentHashMap<String, BasicReturns> getBasicReturnsMap() {return basicReturnsMap;}public void setBasicReturnsMap(ConcurrentHashMap<String, BasicReturns> basicReturnsMap) {this.basicReturnsMap = basicReturnsMap;}public Consumer getConsumer() {return consumer;}public void setConsumer(Consumer consumer) {this.consumer = consumer;

实现 0x1 创建 channel

主要就是构造构造出 request,然后发送请求到 BrokerServer 服务器,阻塞等待服务器响应.

    /*** 0x1* 和服务器进行交互,告诉服务器,此处客户端已经创建了新的 channel 了* @return*/public boolean createChannel() throws IOException {//构造 payloadBasicArguments arguments = new BasicArguments();arguments.setChannelId(channelId);arguments.setRid(generateRid());byte[] payload = BinaryTool.toBytes(arguments);//发送请求Request request = new Request();request.setType(0x1);request.setLength(payload.length);request.setPayload(payload);connection.writeRequest(request);//等待服务器响应BasicReturns basicReturns = waitResult(arguments.getRid());return basicReturns.isOk();}/*** 生成 rid* @return*/public String generateRid() {return "R-" + UUID.randomUUID().toString();}/*** 阻塞等待服务器响应* @param rid* @return*/private BasicReturns waitResult(String rid) {BasicReturns basicReturns = null;while((basicReturns = basicReturnsMap.get(rid)) == null) {//查询结果为空,就说明咱们去菜鸟驿站要取的包裹还没到//此时就需要阻塞等待synchronized (this) {try {wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}basicReturnsMap.remove(rid);return basicReturns;}/*** 由 Connection 中的方法调用,区分为普通响应之后触发* 将响应放回到 channel 管理的 map 中,并唤醒所有线程* @param basicReturns*/public void putReturns(BasicReturns basicReturns) {basicReturnsMap.put(basicReturns.getRid(), basicReturns);synchronized (this) {//当前也不知道有多少线程再等待上述的这个响应//因此就把所有等待的线程唤醒notifyAll();}}

Ps:其他的 请求操作也和 0x1 的方式几乎一样,这里不一一展示了,主要说一下 0xa

0xa 消费者订阅队列消息,这里要先设置好回调到属性中,方便 Connection 通过这个属性来 处理回调

值得注意的一点, 我们约定 channelId 就是 consumerTag

    public boolean basicConsume(String queueName, boolean autoAck, Consumer consumer) throws IOException, MqException {//先设置回调if(this.consumer != null) {throw new MqException("该 channel 已经设置过消费消息回调了,不能重复!");}this.consumer = consumer;BasicConsumeArguments basicConsumeArguments = new BasicConsumeArguments();basicConsumeArguments.setRid(generateRid());basicConsumeArguments.setChannelId(channelId);basicConsumeArguments.setConsumerTag(channelId); // 注意:此处的 consumerTag 使用 channelId 来表示basicConsumeArguments.setQueueName(queueName);basicConsumeArguments.setAutoAck(autoAck);byte[] payload = BinaryTool.toBytes(basicConsumeArguments);Request request = new Request();request.setType(0xa);request.setLength(payload.length);request.setPayload(payload);connection.writeRequest(request);BasicReturns basicReturns = waitResult(basicConsumeArguments.getRid());return basicReturns.isOk();}

二、编写 Demo 


2.1、实例 

到了这里基本就实现完成了一个 跨主机/服务器 之间的生产者消费者模型了(功能上可以满足日常开发对消息队列的使用),但是还具有很强的扩展性,可以继续参考 RabbitMQ,如果有想法的,或者是遇到不会的问题,可以私信我~

以下我来我来编写一个 demo,模拟 跨主机/服务器 之间的生产者消费者模型(这里为了方便,就在本机演示).

首先再 spring boot 项目的启动类中 创建 BrokerServer ,绑定端口号,然后启动

@SpringBootApplication
public class RabbitmqProjectApplication {public static ConfigurableApplicationContext context;public static void main(String[] args) throws IOException {context = SpringApplication.run(RabbitmqProjectApplication.class, args);BrokerServer brokerServer = new BrokerServer(9090);brokerServer.start();}}

编写消费者

public class DemoConsumer {public static void main(String[] args) throws IOException, MqException, InterruptedException {//建立连接ConnectionFactory factory = new ConnectionFactory();factory.setHost("127.0.0.1");factory.setPort(9090);Connection connection = factory.newConnection();Channel channel = connection.createChannel();//创建交换机和队列(这里和生产者创建交换机和队列不冲突,谁先启动,就按照谁的创建,即使已经存在交换机和队列,再创建也不会有什么副作用)channel.exchangeDeclare("demoExchange", ExchangeType.DIRECT, true, false, null);channel.queueDeclare("demoQueue", true, false, false, null);//消费者消费消息channel.basicConsume("demoQueue", true, new Consumer() {@Overridepublic void handlerDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) throws MqException, IOException {System.out.println("开销消费");System.out.println("consumerTag=" + consumerTag);System.out.println("body=" + new String(body));System.out.println("消费完毕");}});//由于消费者不知道生产者要生产多少,就在这里通过循环模拟一直等待while(true) {Thread.sleep(500);}}}

编写生产者

public class DemoProducer {public static void main(String[] args) throws IOException, InterruptedException {//建立连接ConnectionFactory factory = new ConnectionFactory();factory.setHost("127.0.0.1");factory.setPort(9090);Connection connection = factory.newConnection();Channel channel = connection.createChannel();//创建交换机和队列(这里和消费者创建交换机和队列不冲突,谁先启动,就按照谁的创建,即使已经存在交换机和队列,再创建也不会有什么副作用)channel.exchangeDeclare("demoExchange", ExchangeType.DIRECT, true, false, null);channel.queueDeclare("demoQueue", true, false, false, null);//生产消息byte[] body1 = "Im cyk1 !".getBytes();channel.basicPublish("demoExchange", "demoQueue", null, body1);Thread.sleep(500);//关闭连接channel.close();connection.close();}}

2.1、实例演示

启动 spring boot 项目(启动 BrokerServer)

运行消费者(消费者和生产者谁先后运行都可以)

 

运行生产者

 

 

相关文章:

根据源码,模拟实现 RabbitMQ - 网络通讯设计,实现客户端Connection、Channel(完结)

目录 一、客户端代码实现 1.1、需求分析 1.2、具体实现 1&#xff09;实现 ConnectionFactory 2&#xff09;实现 Connection 3&#xff09;实现 Channel 二、编写 Demo 2.1、实例 2.1、实例演示 一、客户端代码实现 1.1、需求分析 RabbitMQ 的客户端设定&#xff…...

The Cube++ Illumination Estimation Dataset 文章总结

The Cube++ Illumination Estimation Dataset 颜色恒常性数据集Cube++ Type: Academic Journal Author: Ershov Link: https://ieeexplore.ieee.org/document/9296220 Select: ⭐️⭐️⭐️⭐️ Status: Done 备注: Cube++数据集 Journal: ACCESS Year: 2020 code: https://g…...

“烧钱”的大模型,如何迈过存储这道坎?

几乎每一个行业都在讨论大模型&#xff0c;每一个行业巨头都在训练大模型&#xff0c;人工智能已然进入了大模型主导的时代。 想要占领大模型应用的高地&#xff0c;数据和算力可以说是不可或缺的基石。和算力相关的讨论已经有很多&#xff0c;以至于英伟达的市值在2023年翻了…...

UNIX网络编程卷一 学习笔记 第二十九章 数据链路访问

目前大多操作系统都为程序提供访问数据链路层的功能&#xff0c;此功能可提供以下能力&#xff1a; 1.能监视由数据链路层接收的分组&#xff0c;使得tcpdump之类的程序能运行&#xff0c;而无需专门的硬件设备来监视分组。如果结合使用网络接口进入混杂模式&#xff08;promis…...

WebGIS的一些学习笔记

一、简述计算机网络的Internet 概念、网络类型分类、基本特征和功用是什么 计算机网络的Internet 概念 计算机网络是地理上分散的多台独立自主的计算机遵循约定的通讯协议&#xff0c;通过软、硬件互连以实现交互通信、资源共享、信息交换、协同工作以及在线处理等功能的系统…...

java Spring Boot将不同配置拆分入不同文件管理

关于java多环境开发 最后还有一个小点 我们一般会将不同的配置 放在不同的配置文件中 好处肯定就在于 想换的时候非常方便 那么 我们直接看代码 我们将项目中的 application.yml 更改代码如下 spring:profiles:active: dev这里 意思是 我们选择了dev 环境 然后创建一个文件 …...

Docker(三) 创建Docker镜像

一、在Docker中拉取最基本的Ubuntu系统镜像 搜索Ubuntu镜像 Explore Dockers Container Image Repository | Docker Hub 下载镜像 docker pull ubuntu:22.04 二、在镜像中添加自己的内容 使用ubuntu镜像创建容器 docker run -it ubuntu:20.04 /bin/bash 在容器中创建了一个文…...

Linux操作系统--shell编程(正则表达式)

1..正则表达式概述 正则表达式使用单个字符串来描述、匹配一系列符合某个语法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些符合某个模式的文本。在 Linux 中,grep,sed,awk 等文本处理工具都支持通过正则表达式进行模式匹配。 2.常规的匹配操作 3.…...

k8s的service mesh功能有那些

Kubernetes&#xff08;K8s&#xff09;的服务网格&#xff08;Service Mesh&#xff09;是一种用于管理微服务架构中服务通信、安全性、可观察性等方面的工具集合。服务网格通过将网络和安全功能从应用程序代码中分离出来&#xff0c;帮助简化了微服务的部署和管理。以下是一些…...

【数据库技术】NineData数据复制,加速实时数仓构建

8 月 30 日&#xff0c;由 NineData 和 SelectDB 共同举办的主题为“实时数据驱动&#xff0c;引领企业智能化数据管理”的线上联合发布会&#xff0c;圆满成功举办&#xff01;双方聚焦于实时数据仓库技术和数据开发能力&#xff0c;展示如何通过强大的生态开发兼容性&#xf…...

Kotlin入门1. 语法基础

Kotlin入门1. 语法基础 一、简介二、在Idea创建一个示例项目三、基本语法1. 第一个程序2. 基本数据类型(1) 数字(2) 类型转换(3) 数学运算位运算 &#xff08;4&#xff09;可空类型 3. 函数4. 字符串(1) 字符串拼接(2) 字符串查找(3) 字符串替换(4) 字符串分割 5. null 安全的…...

MVCC简介、工作流程、优缺点

目录 简介 相关概念 工作流程 MVCC优缺点 简介 MVCC&#xff08;Multi-Version Concurrency Control&#xff09;即多版本并发控制&#xff0c;是通过维护数据的历史版本&#xff0c;从而解决并发访问情况下的读一致性问题 相关概念 读锁&#xff1a; 也叫共享锁、S锁。若…...

pandas由入门到精通-pandas的数据结构

pandas数据分析-pandas的数据结构 pandas 数据结构Series1. 创建Series数组2. 性质3. 索引4. 运算DataFrame1. 创建Df数组2. 性质3.索引4. 对列进行增删改Index Objects本文介绍pandas中一些常用的属性方法的概述,给读者提供快速学习的架构和思路。表格中提供的一些参数方法没…...

jenkins+ssh+Putty构建windows的IIS服务发布

使用JenkinssshPutty发布windows IIS服务 下面是使用Jenkins、SSH和PuTTY实现Windows IIS服务发布的步骤&#xff1a; 在构建服务器上安装Windows SSH服务、PuTTY和7-Zip软件。在部署服务器上也安装Windows SSH服务和7-Zip软件。在构建服务器上使用批处理命令执行编译&#x…...

服务器和普通电脑有何区别?43.248.189.x

简单来讲&#xff0c;服务器和电脑的功能是一样的&#xff0c;我们也可以把服务器称之为电脑&#xff08;PC机&#xff09;&#xff0c;只是服务器对稳定性与安全性以及处理器数据能力有更高要求&#xff0c;比如我们每天浏览一个网站&#xff0c;发现这个网站每天24小时都能访…...

Zookeeper的使用

一、Zookeeper简介 分布式协调框架&#xff0c;小型的树形结构数据共享储存系统。 zookeeper的应用场景 集群管理 注册中心 配置中心 发布者将数据发布到ZooKeeper一系列节点上面&#xff0c;订阅者进行数据订阅&#xff0c;当数据有变化时&#xff0c;可及时得到数据的变…...

【实用 Python 库】使用 XPath 与 lxml 模块在 Python 中高效解析 XML 与 HTML

在今天的信息时代&#xff0c;数据无处不在&#xff0c;从网络爬虫到数据挖掘&#xff0c;从文本处理到数据分析&#xff0c;我们时常需要从结构化文档中提取有用的信息。XPath 是一门强大的查询语言&#xff0c;它可以在 XML 与 HTML 等文档中定位特定的元素与数据。而在 Pyth…...

数据库的基本概念

数据库 数据库由表集合组成&#xff0c;它是以一定的组织方式存储的相互有关的数据集合。 表&#xff1a;记录&#xff1a;行&#xff0c;字段&#xff08;属性&#xff09;&#xff1a;列&#xff0c;以行列的形式就组成了表&#xff08;数据存储在表中&#xff09;。 关系数…...

无涯教程-Android - 环境设置

您可以从Oracle的Java网站下载最新版本的Java JDK-Java SE下载&#xff0c;您将在下载的文件中找到有关安装JDK的说明,按照给定的说明安装和配置安装程序。最后,将PATH和JAVA_HOME环境变量设置为引用包含 java 和 javac 的目录,通常分别是java_install_dir/bin和java_install_d…...

将 ChatGPT 与 ReactJS 集成以实现更智能的对话界面

在本博客中,我们将探讨如何使用 Kommunicate 平台将 ChatGPT 与 ReactJS 集成,从而更轻松地在网站上部署和管理聊天机器人。 随着技术世界的不断发展,聊天机器人已成为许多企业不可或缺的一部分,提供高效、个性化的客户交互。在众多可用的人工智能聊天机器人解决方案中,C…...

关于xml中返回string类型代码中用list接收的问题,扫描

1.结论,xml中返回为string的话,在list中只会取出来第一个元素 //根据value查询GetMapping("getTest")public List<HashMap> getTest() {List<HashMap> list dictService.getTest();return list;} <select id"getTest" resultType"jav…...

【前端demo】CSS border-radius可视化 原生实现

文章目录 效果原理代码 前端demo系列目录&#xff1a;https://blog.csdn.net/karshey/article/details/132585901 效果 参考&#xff1a; Fancy Border Radius Generator (9elements.github.io) https://border-radius.com/ CSS border-radius 新玩法(含可视化生成工具) - …...

Qt Creator使用Clang Format方法

Qt Creator使用Clang Format 习惯性的想格式化代码&#xff0c;发现Qt Creator默认居然是没有代码格式化的&#xff0c;只有一个缩进。 Qt Creater中有个插件&#xff1a;beautifier&#xff0c;在"帮助->关于"插件中&#xff0c;开启“Beautifier”即可&#xf…...

智慧矿山2.0:煤矿智能化综合管理AI大数据监管平台建设方案设计

一、行业背景 能源与煤矿是我国国民经济的重要物质生产部门和支柱产业之一&#xff0c;同时也是一个安全事故多发的高危行业&#xff0c;施工阶段的现场管理对工程成本、进度、质量及安全等至关重要。煤矿智能化既是未来趋势&#xff0c;更是产业发展需求&#xff0c;建设智慧…...

Linux——(第一章)Linux的入门

VMwear workstations下载及安装 Ubuntu server 18.04安装 VScode下载与安装 使用VS Code连接远程服务器 MobaXterm的下载安装及远程连接 Filezila的下载、安装与使用&#xff08;向服务器传输文件&#xff09; 目录 1.概述 2.Linux和Windows的区别 3.VM的安装与使用 1.概述 …...

十六、策略模式

一、什么是策略模式 策略&#xff08;Strategy&#xff09;模式的定义&#xff1a;该模式定义了一系列算法&#xff0c;并将每个算法封装起来&#xff0c;使它们可以相互替换&#xff0c;且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式&#xff0c;它通过对算法…...

Python装饰器

什么是python装饰器&#xff1f; 顾名思义&#xff0c;从字面意思就可以理解&#xff0c;它是用来"装饰"Python的工具&#xff0c;使得代码更具有Python简洁的风格。换句话说&#xff0c;它是一种函数的函数&#xff0c;因为装饰器传入的参数就是一个函数&#xff0…...

【Spring】使用自定义注解方式实现AOP鉴权

AOP&#xff0c;是一种面向切面编程&#xff0c;可以通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。 在软件开发中&#xff0c;鉴权&#xff08;Authentication&#xff09;是一项非常重要的安全措施&#xff0c;用于验证用户身份和权限。在应用程序中&…...

Go几种读取配置文件的方式

比较有名的方案有 使用viper管理配置[1] 支持多种配置文件格式&#xff0c;包括 JSON,TOML,YAML,HECL,envfile&#xff0c;甚至还包括Java properties 支持为配置项设置默认值 可以通过命令行参数覆盖指定的配置项 支持参数别名 viper[2]按照这个优先级&#xff08;从高到低&am…...

每日一题(反转链表)

每日一题&#xff08;反转链表&#xff09; 206. 反转链表 - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a; 可以定义一个新的newhead结构体指针。再定义cur指针和next指针互相配合&#xff0c;将原链表中的节点从头到尾依次头插到newhead链表中&#xff0c;同时更…...