从零手搓一个【消息队列】创建核心类, 数据库设计与实现

文章目录

  • 一、创建核心类
    • 1, 交换机
    • 2, 交换机类型
    • 3, 队列
    • 4, 绑定
    • 5, 交换机转发 & 绑定规则
    • 6, 消息
    • 7, 消息属性
  • 二、数据库设计
    • 1, 使用 SQLite
    • 2, 使用 MyBatis
      • 2.1, 创建 Interface
      • 2.2, 创建 xml 文件
  • 三、硬盘管理 -- 数据库
    • 1, 创建 DataBaseManager 类
    • 2, init() 初始化数据库
    • 3, insertDefaultData() 插入默认数据
    • 4, createTable() 创建数据表
    • 5, isDBExists() 数据库是否存在
    • 6, deleteTables() 删除数据表
    • 7, 封装数据库的增删查操作
  • 四、小结


创建 Spring Boot 项目, Spring Boot 2 系列版本, Java 8 , 引入 MyBatis, Lombok 依赖

提示:是正在努力进步的小菜鸟一只,如有大佬发现文章欠佳之处欢迎批评指点~ 废话不多说,直接上干货!

整体目录结构 :
在这里插入图片描述

本文主要实现 server 包


一、创建核心类

上篇文章 分析了项目需求, 介绍了项目中重要的核心概念和核心 API, 以及重要板块

一个消息队列中需要的交换机, 队列, 绑定, 消息等核心概念, 以面向对象的思想, 在server.core 包下创建出来对应的类
在这里插入图片描述


1, 交换机

@Data
public class Exchange {// 身份标识(唯一, RabbitMQ 就是以 name 作为身份标识的)private String name;// 三种交换机类型private ExchangeTypeEnum type = ExchangeTypeEnum.DIRECT;// 是否需要持久化存储private boolean durable = false;// 是否(交换机没人使用时)自动删除 ------------------>先不实现private boolean autoDelete = false;// 创建交换机时, 指定的参数选项 ------------------>先不实现// 数据库中存储 String 类型, 需要序列化private Map<String, Object> arguments = new HashMap<>();/*** 实现序列化, 修改 getter()和 setter(), 供数据库使用*/public String getArguments(){ObjectMapper objectMapper = new ObjectMapper();// 序列化 往数据库里写try {return objectMapper.writeValueAsString(arguments);} catch (JsonProcessingException e) {e.printStackTrace();}return "{}";}public void setArguments(String arguments) {ObjectMapper objectMapper = new ObjectMapper();// 反序列化 从数据库里读try {this.arguments = objectMapper.readValue(arguments, new TypeReference<HashMap<String, Object>>() {});} catch (JsonProcessingException e) {e.printStackTrace();}}/*** 便于 测试/ 代码内部调用 时使用*/public Object getArguments(String key) {return arguments.get(key);}public void setArguments(String key, Object value) {arguments.put(key, value);}public void setArguments(Map<String, Object> arguments) {this.arguments = arguments;}
}

本项目未实现 autoDelete 和 arguments


2, 交换机类型

这是一个枚举类, 包含直接交换机, 扇出交换机, 主题交换机

public enum ExchangeTypeEnum {DIRECT(0),FANOUT(1),TOPIC(2);private final int type;ExchangeTypeEnum(int type) {this.type = type;}public int getType() {return type;}
}

3, 队列

类名不设为 Queue, 防止和标准库中的 Queue 冲突

@Data
public class MessageQueue {// 唯一标识private String name;// 是否需要持久化存储private boolean durable = false;// 是否为独有(如果是独有, 只能被一个消费者使用) ------------------>先不实现private boolean exclusive = false;// 是否(队列没人使用时)自动删除 ------------------>先不实现private boolean autoDelete = false;// 创建队列时, 指定的参数选项 ------------------>先不实现// 数据库中存储 String 类型, 需要序列化private Map<String, Object> arguments = new HashMap<>();/*** 实现序列化, 修改 getter()和 setter(), 供数据库使用*/public String getArguments(){ObjectMapper objectMapper = new ObjectMapper();// 序列化 往数据库里写try {return objectMapper.writeValueAsString(arguments);} catch (JsonProcessingException e) {e.printStackTrace();}return "{}";}public void setArguments(String arguments) {ObjectMapper objectMapper = new ObjectMapper();// 反序列化 从数据库里读try {this.arguments = objectMapper.readValue(arguments, new TypeReference<HashMap<String, Object>>() {});} catch (JsonProcessingException e) {e.printStackTrace();}}/*** 便于 测试/ 代码内部调用 时使用*/public Object getArguments(String key) {return arguments.get(key);}public void setArguments(String key, Object value) {arguments.put(key, value);}public void setArguments(Map<String, Object> arguments) {this.arguments = arguments;}
}

暂不实现 exclusive, autoDelete, arguments


4, 绑定

@Data
public class Binding {// 绑定的消息队列标识private String queueName;// 绑定的交换机标识private String exchangeName;// 绑定的 keyprivate String bindingKey;
}

bindingKey 是在创建交换机和队列的绑定时指定的, 生产者发布消息时, 需额外指定一个 routingKey
如果是直接交换机, routingKey 作为队列的唯一标识
如果是扇出交换机, routingKey 为 null, 无需使用
如果是主题交换机, routingKey 需和 bindingKey 匹配


5, 交换机转发 & 绑定规则

在此先不展示, 在后续文章中对应的部分再展示 防止思路混淆


6, 消息

@Data
public class Message implements Serializable {// 属性private BasicProperties basicProperties = new BasicProperties();// 正文private byte[] body;// 消息存储在文件中的偏移量(字节, 约定 "[,)" 区间 )private transient long offsetBegin = 0;private transient long offsetEnd = 0;// 是否合法(逻辑删除的标记, 0x1 有效, 0x0 无效)private byte isValid = 0x1;// 提供工厂方法, 封装 Message 类的创建过程public static Message createMessage(String routingKey, BasicProperties basicProperties, byte[] body) {Message message = new Message();if (basicProperties != null) {message.setBasicProperties(basicProperties);}message.setMessageId("M$" + UUID.randomUUID().toString().replace("-", ""));message.setRoutingKey(routingKey);message.setBody(body);return message;}// 下面这些方法是为了封装 basicProperties 中的 getter()和 setter()public String getMessageId() {return basicProperties.getMessageId();}public void setMessageId(String id) {basicProperties.setMessageId(id);}public String getRoutingKey() {return basicProperties.getRoutingKey();}public void setRoutingKey(String key) {basicProperties.setRoutingKey(key);}public int getDeliverMode() {return basicProperties.getDeliverMode();}public void setDeliverMode(int value) {basicProperties.setDeliverMode(value);}
}
  • Message 需要实现 Serializable 接⼝. 后续需要把 Message 写⼊⽂件以及进⾏⽹络传输.
  • basicProperties 是消息的属性信息. body 是消息体.
  • offsetBeg 和 offsetEnd 表⽰消息在消息⽂件中所在的起始位置和结束位置. 这⼀块具体的设计后⾯再详细介绍. 使⽤ transient 关键字避免属性被序列化.
  • isValid ⽤来表⽰消息在⽂件中是否有效. 这⼀块具体的设计后⾯再详细介绍.
  • createMessage() 相当于⼀个⼯⼚⽅法, ⽤来创建⼀个 Message 实例. messageId 通过UUID 的⽅式⽣成.

文件中的数据不相当于一个顺序表, 如果要真正删除一条消息, 是不是需要把后面的数据整体往前挪动? 这无疑是个低效操作, 因此, 对于 “删除消息” 这种高频操作, 逻辑删除显然是更优解, 但也不能让消息无限制的堆在文件中, 所以后面会参考 JVM 的 GC , 自主实现清理文件的功能


7, 消息属性

这个类作为Message 类的 引用类型的成员属性, 也需要实现 Serializable 接⼝, 否则 message 对象不能被序列化

@Data
public class BasicProperties implements Serializable {// 消息的唯一标识(UUID)private String messageId;// 和 bindingKey 匹配(如果交换机为 DIRECT, 该值就是队列名, 如果交换机为 FANOUT, 该值为 null )private String routingKey;// 是否要消息持久化( RabbitMQ 就是使用 1 表示不持久化, 2 表示持久化)private int deliverMode = 2;// ... 其他属性暂不考虑
}

二、数据库设计


1, 使用 SQLite

对于 Exchange, MSGQueue, Binding, 我们使⽤数据库进⾏持久化保存.

此处我们使⽤的数据库是 SQLite, 是⼀个更轻量的数据库

SQLite 只是⼀个动态库(当然, 官⽅也提供了可执⾏程序 exe), 我们在 Java 中直接引⼊ SQLite 依赖, 即可直接使⽤, 不必安装其他的软件.

MySQL 是一个客户端服务器结构的程序, SQLite 相当于直接操作本地的硬盘文件

  • 在pom.xml文件中的 “dependencies” 标签中拷贝 :
		<dependency><groupId>org.xerial</groupId><artifactId>sqlite-jdbc</artifactId><version>3.41.0.1</version></dependency>
  • 在 resource 目录下创建 application.yml 文件配置SQLite数据源, 拷贝:
spring:datasource:url: jdbc:sqlite:./data/meta.db # 注意这个路径username:# 不需要password:# 不需要driver-class-name: org.sqlite.JDBCmybatis:mapper-locations: classpath:mapper/**Mapper.xml

数据库文件的位置就是 ./data/meta.db, 数据库的数据就在这里


2, 使用 MyBatis

实现 mapper 包
在这里插入图片描述


2.1, 创建 Interface

在 server.mapper 包下定义一个 MetaMapper 接口, 需要提供 交换机, 队列, 绑定 的建表, 插入, 删除 , 查询的 API (抽象方法)

不需要使用sql 语句建库, 创建出 ./data/meta.db 这个文件就相当于建库了, 后面再写创建文件的操作

@Mapper
public interface MetaMapper {/*** 建表*/void createExchangeTable();void createQueueTable();void createBindingTable();/*** exchange 表*/void insertExchange(Exchange exchange);void deleteExchange(String exchangeName);List<Exchange> selectAllExchanges();/*** queue 表*/void insertQueue(MessageQueue queue);void deleteQueue(String queueName);List<MessageQueue> selectAllQueues();/*** binding 表*/void insertBinding(Binding binding);void deleteBinding(Binding binding);List<Binding> selectAllBindings();
}

2.2, 创建 xml 文件

在 resource 目录下, 新建一个 mapper 包, 创建 MetaMapper.xml 文件, 在这个文件中编写 sql 语句, 实现上述在 MetaMapper 接口中的抽象方法

在 xml 文件中拷贝:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.server.mapper.MetaMapper"></mapper>

其中 namespace 这个字段的值要对应刚才定义的 MetaMapper 接口 的路径


  • 建表(使用 update 标签即可)
    <update id="createExchangeTable">create table if not exists exchange (name varchar(50) primary key,type int,durable boolean,autoDelete boolean,arguments varchar(1024));</update><update id="createQueueTable">create table if not exists queue (name varchar(50) primary key,durable boolean,exclusive boolean,autoDelete boolean,arguments varchar(1024));</update><update id="createBindingTable">create table if not exists binding (exchangeName varchar(50),queueName varchar(50),bindingKey varchar(256));</update>

  • exchange 表的增删查的 sql
    <insert id="insertExchange" parameterType="com.example.demo.server.core.Exchange">insert into exchange values(#{name}, #{type}, #{durable}, #{autoDelete}, #{arguments});</insert><select id="selectAllExchanges" resultType="com.example.demo.server.core.Exchange">select * from exchange;</select><delete id="deleteExchange" parameterType="java.lang.String">delete from exchange where name = #{exchangeName};</delete>

  • queue 表的增删查的 sql
<insert id="insertQueue" parameterType="com.example.demo.server.core.MessageQueue">insert into queue values(#{name}, #{durable}, #{exclusive}, #{autoDelete}, #{arguments});</insert><select id="selectAllQueues" resultType="com.example.demo.server.core.MessageQueue">select * from queue;</select><delete id="deleteQueue" parameterType="java.lang.String">delete from queue where name = #{queueName};</delete>

  • queue 表的增删查的 sql
	<insert id="insertBinding" parameterType="com.example.demo.server.core.Binding">insert into binding values(#{exchangeName}, #{queueName}, #{bindingKey});</insert><select id="selectAllBindings" resultType="com.example.demo.server.core.Binding">select * from binding;</select><delete id="deleteBinding" parameterType="com.example.demo.server.core.Binding">delete from binding where exchangeName = #{exchangeName} and queueName = #{queueName};</delete>

三、硬盘管理 – 数据库

实现 datacenter 包中的 DataBaseManager 类
在这里插入图片描述

datacenter 这个包中整合硬盘上的数据管理 + 内存上的数据管理
硬盘上的数据管理又整合了 数据库中的数据管理 + 文件中的数据管理


1, 创建 DataBaseManager 类

成员属性需要 MetaMapper 的对象, 用来封装刚才编写的数据库的 API

但并不使用 SpringBoot 的依赖注入 (@AutoWired), 而是使用以来查找的方式获取到 metaMapper

public class DataBaseManager {private MetaMapper metaMapper;}

在启动类中初始化容器

@SpringBootApplication
public class DemoApplication {public static ConfigurableApplicationContext context;public static void main(String[] args) throws IOException {context = SpringApplication.run(DemoApplication.class, args);}
}

2, init() 初始化数据库

对于 DataBaseManager 类的初始化工作, 不仅仅是对成员属性的初始化, 而是需要一些额外的业务逻辑, 这种情况就不使用构造方法了, 而是单独定义一个方法

初始化工作: metaMapper + 建库建表 + 插入默认数据

如果是第一次启动服务器, 没有数据库则建库建表
如果是重启服务器, 已有数据库则不做处理

MyBatis 在第一次创建数据表的时候就会创建出 ./data/meta.db 这个文件, 但前提是要有 ./data 这个目录, 所以要先手动创建

    public void init() {this.metaMapper = DemoApplication.context.getBean(MetaMapper.class);if(!isDBExists()) {// 创建目录File file = new File("./data");file.mkdirs();// 创建数据表createTable();// 插入数据insertDefaultData();}}

3, insertDefaultData() 插入默认数据

创建一个默认的交换机(直接交换机)

    public void insertDefaultData() {Exchange exchange = new Exchange();exchange.setName("");exchange.setType(ExchangeTypeEnum.DIRECT);exchange.setDurable(false);exchange.setAutoDelete(false);metaMapper.insertExchange(exchange);}

4, createTable() 创建数据表

    public void createTable() {metaMapper.createExchangeTable();metaMapper.createQueueTable();metaMapper.createBindingTable();}

5, isDBExists() 数据库是否存在

SQLite 是一个轻量级数据库, 操作 SQLite 相当于操作本地的硬盘文件, 所以检查数据库是否存在就是检查数据库文件是否存在

    public boolean isDBExists() {File file = new File("./data/meta.db");return file.exists();}

6, deleteTables() 删除数据表

同上, 删除数据库就是删除文件, 先删文件再删目录

    public void deleteTables(){File file = new File("./data/meta.db");file.delete();File dir = new File("./data");dir.delete();}

7, 封装数据库的增删查操作

public void insertExchange(Exchange exchange) {metaMapper.insertExchange(exchange);}public List<Exchange> selectAllExchanges() {return metaMapper.selectAllExchanges();}public void deleteExchange(String exchangeName) {metaMapper.deleteExchange(exchangeName);}public void insertQueue(MessageQueue queue) {metaMapper.insertQueue(queue);}public List<MessageQueue> selectAllQueues() {return metaMapper.selectAllQueues();}public void deleteQueue(String queueName) {metaMapper.deleteQueue(queueName);}public void insertBinding(Binding binding) {metaMapper.insertBinding(binding);}public List<Binding> selectAllBindings() {return metaMapper.selectAllBindings();}public void deleteBinding(Binding binding) {metaMapper.deleteBinding(binding);}

四、小结

本文主要实现了两点 :

  • 1, 根据面向对象思想, 创建出了交换机, 队列, 绑定, 消息, 等核心概念的类
  • 2, 持久化存储 --> 硬盘管理 --> 数据库
    • 2.1, 数据库设计, 使用 SQLite, 并结合 MyBatis 编写了交换机, 队列, 绑定的建表, 增, 删, 查的 sql
    • 2.2, 使用 DataBaseManager 这个类管理数据库中的数据, 因为仅仅有sql语句不足以支撑所有的业务逻辑, 还需要对数据库的初始化, 判断存在, 删除等做进一步的封装

篇幅有限, 目前为止, 持久化存储 --> 硬盘管理 --> 文件 这个板块还没实现

下篇会实现消息在文件上的存储( 文件管理 : MessageFileManager 类)


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

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

相关文章

算法题系列10·最长公共前缀

目录 题目描述 思路 实现 题目描述 编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀&#xff0c;返回空字符串 ""。示例 1&#xff1a; 输入&#xff1a;strs ["flower","flow","flight"] 输出&#xff1a;&qu…

LabVIEW开发教学实验室自动化INL和DNL测试系统

LabVIEW开发教学实验室自动化INL和DNL测试系统 如今&#xff0c;几乎所有的测量仪器都是基于微处理器的设备。模拟输入量在进行数字处理之前被转换为数字量。对于参加电气和电子测量课程的学生来说&#xff0c;了解ADC以及如何欣赏其性能至关重要。ADC的不确定性可以根据其传输…

蓝桥杯每日一题2023.10.5

3420. 括号序列 - AcWing题库 题目描述 题目分析 对于这一我们需要有前缀知识完全背包 完全背包的朴素写法&#xff1a; #include<bits/stdc.h> using namespace std; const int N 1010; int n, m, v[N], w[N], f[N][N]; int main() {cin >> n >> m;fo…

PyTorch实例:简单线性回归的训练和反向传播解析

文章目录 &#x1f966;引言&#x1f966;什么是反向传播&#xff1f;&#x1f966;反向传播的实现&#xff08;代码&#xff09;&#x1f966;反向传播在深度学习中的应用&#x1f966;链式求导法则&#x1f966;总结 &#x1f966;引言 在神经网络中&#xff0c;反向传播算法…

华为OD七日集训第6期 十一特辑 - 按算法分类,由易到难,循序渐进,玩转OD

目录 专栏导读华为OD机试算法题太多了&#xff0c;知识点繁杂&#xff0c;如何刷题更有效率呢&#xff1f; 一、逻辑分析二、数据结构1、线性表① 数组② 双指针 2、map与list3、优先队列4、滑动窗口5、二叉树6、并查集7、栈 三、算法1、基础算法① 贪心算法② 二分查找③ 分治…

OpenCV 15(SIFT/SURF算法)

一、SIFT Harris和Shi-Tomasi角点检测算法&#xff0c;这两种算法具有旋转不变性&#xff0c;但不具有尺度不变性&#xff0c;以下图为例&#xff0c;在左侧小图中可以检测到角点&#xff0c;但是图像被放大后&#xff0c;在使用同样的窗口&#xff0c;就检测不到角点了。 尺度…

JavaScript操作CSS样式

上节课我们基本完成了游戏的主体&#xff0c;这节课我们来学习如果使用JavaScript去操作CSS样式 ● 例如&#xff0c;我们现在想当玩家输入对的数字之后&#xff0c;我们讲背景改为绿色&#xff0c;并且把number的框宽度变大 const secretnumber Math.trunc(Math.random() * …

经典算法-----汉诺塔问题

前言 今天我们学习一个老经典的问题-----汉诺塔问题&#xff0c;可能在学习编程之前我们就听说过这个问题&#xff0c;那这里我们如何去通过编程的方式去解决这么一个问题呢&#xff1f;下面接着看。 汉诺塔问题 问题描述 这里是引用汉诺塔问题源自印度一个古老的传说&#x…

目前制造企业生产计划现状是什么?有没有自动化排产系统?

大家都知道&#xff0c;人的指挥中心是大脑&#xff0c;大脑对我们的发出各种各样的指令&#xff0c;告诉我们&#xff1a;“手”做什么事情&#xff0c;“眼睛”看什么地方&#xff0c;“耳朵”听什么声音&#xff0c;然后再将摸到的、看到的、听到的信息传递给大脑&#xff0…

制作 3 档可调灯程序编写

PWM 0~255 可以将数据映射到0 75 150 225 尽可能均匀电压间隔

maven下载、本地仓库设置与idea内置maven设置

一、下载安装maven maven下载官网&#xff1a;https://maven.apache.org/download.cgi 下载到本地后解压 二、配置环境变量 我的电脑-属性-高级系统设置-环境变量/系统变量 新建MAVEN_HOME 变量值为自己的maven包所在的位置 编辑path 添加 %MAVEN_HOME%\bin 三、测试 Win…

一个案例熟悉使用pytorch

文章目录 1. 完整模型的训练套路1.2 导入必要的包1.3 准备数据集1.3.1 使用公开数据集&#xff1a;1.3.2 获取训练集、测试集长度&#xff1a;1.3.3 利用 DataLoader来加载数据集 1.4 搭建神经网络1.4.1 测试搭建的模型1.4.2 创建用于训练的模型 1.5 定义损失函数和优化器1.6 使…

redis持久化与调优

一 、Redis 高可用&#xff1a; 在web服务器中&#xff0c;高可用是指服务器可以正常访问的时间&#xff0c;衡量的标准是在多长时间内可以提供正常服务&#xff08;99.9%、99.99%、99.999%等等&#xff09;。但是在Redis语境中&#xff0c;高可用的含义似乎要宽泛一些&#x…

POJ 2886 Who Gets the Most Candies? 树状数组+二分

一、题目大意 我们有N个孩子&#xff0c;每个人带着一张卡片&#xff0c;一起顺时针围成一个圈来玩游戏&#xff0c;第一回合时&#xff0c;第k个孩子被淘汰&#xff0c;然后他说出他卡片上的数字A&#xff0c;如果A是一个正数&#xff0c;那么下一个回合他左边的第A个孩子被淘…

通过usb串口发送接收数据

USB通信使用系统api&#xff0c;USB转串口通信使用第三方库usb-serial-for-android&#xff0c; 串口通信使用Google官方库android-serialport-api。x 引入包后在本地下载的位置&#xff1a;C:\Users\Administrator\.gradle\caches\modules-2\files-2.1 在 Android 中&#x…

【python海洋专题十一】colormap调色

【python海洋专题十一】colormap调色 上期内容 本期内容 图像的函数包调用&#xff01; Part01. 自带颜色条Colormap 调用方法&#xff1a; cmap3plt.get_cmap(ocean)查询方法&#xff01; Part02. seaborn函数包 01&#xff1a;sns.cubehelix_palette cmap5 sns.cu…

string类的模拟实现(万字讲解超详细)

目录 前言 1.命名空间的使用 2.string的成员变量 3.构造函数 4.析构函数 5.拷贝构造 5.1 swap交换函数的实现 6.赋值运算符重载 7.迭代器部分 8.数据容量控制 8.1 size和capacity 8.2 empty 9.数据修改部分 9.1 push_back 9.2 append添加字符串 9.3 运算符重载…

OpenCV利用Camshift实现目标追踪

目录 原理 做法 代码实现 结果展示 原理 做法 代码实现 import numpy as np import cv2 as cv# 读取视频 cap cv.VideoCapture(video.mp4)# 检查视频是否成功打开 if not cap.isOpened():print("Error: Cannot open video file.")exit()# 获取第一帧图像&#x…

SpringCloud Alibaba - Sentinel 微服务保护解决雪崩问题、Hystrix 区别、安装及使用

目录 一、Sentinel 1.1、背景&#xff1a;雪崩问题 1.2、雪崩问题的解决办法 1.2.1、超时处理 缺陷&#xff1a;为什么这里只是 “缓解” 雪崩问题&#xff0c;而不是百分之百解决了雪问题呢&#xff1f; 1.2.2、舱壁模式 缺陷&#xff1a;资源浪费 1.2.3、熔断降级 1.…

OK3568 forlinx系统编译过程及问题汇总

1. 共享文件夹无法加载&#xff1b;通过网上把文件夹加载后&#xff0c;拷贝文件很慢&#xff0c;任务管理器查看发现硬盘读写速率很低。解决办法&#xff1a;重新安装vmware tools。 2. 拷贝Linux源码到虚拟机&#xff0c;解压。 3. 虚拟机基本库安装 forlinxubuntu:~$ sudo…