项目实战 — 消息队列(8){网络通信设计①}

目录

一、自定义应用层协议

🍅 1、格式定义

🍅 2、准备工作

🎄定义请求和响应 

 🎄 定义BasicArguments

🎄 定义BasicReturns

🍅 2、创建参数类

        🎄 交换机

        🎄 队列

        🎄 绑定

        🎄发布消息

        🎄 订阅消息

        🎄确认应答

        🎄 消息推送

二、服务器设计

 🍅 1、编写实例变量和构造方法

🍅 2、编写启动类和关闭类

🍅 3、编写处理连接的方法:processConnection()

 🍅 4、编写读取请求readRequest()和写回响应writeResponse方法

🍅 5、实现根据请求计算响应:process()方法编写


一、自定义应用层协议

🍅 1、格式定义

本消息队列,是需要通过网络进行通信的。这里主要基于TCP协议,自定义应用层协议。

由于当前交互的Message数据,是二进制数据,由于HTTP和JSON都是文本协议,所以这里就不适用了。使用自定义的应用层协议。

约定自定义应用层协议格式:

        以下是请求和响应的组成部分:

 type:

描述当前请求和响应式做什么的,描述当前请求/响应是在调用哪个API(VirtualHost中的核心API)

        以下是type标识请求相应不同的功能,取值如下:

        其中Channel代表的是Connection(TCP的连接)内部的”逻辑上"的连接。此时一个           Connection中可能会含有多个Channel。存在的意义是为了让TCP连接

VirtualHost中的十多个方法:
0x1创建channel
0x2关闭channel
0x3创建exchange
0x4销毁exchange
0x5创建queue
0x6销毁queue
0x7创建binding
0x8销毁binding
0x9发送message
0xa订阅message
0xb返回ack
0xc服务器给客户端推送的消息(被订阅的消息)(响应独有)

length:描述了payload的长度

payload: 会根据当前是请求还是响应,以及当前的type有不同的取值。

比如当前是0x3(创建交换机),

/*
* 表示一个网络通信中的请求对象,按照自定义协议的格式来展开
* */
@Data
public class Request {private int type;private int length;private byte[] payload;
}

当前是一个请求,那么pyload中的内容是exchangeDeclare的参数的序列化的结果;

如果当前是一个响应,那么payload里面的内容就是exchangeDeclare的返回结果的序列化内容。

那么接下来就进行代码设计

以下都是再commen包中创建。

🍅 2、准备工作

🎄定义请求和响应 

/*
* 表示一个网络通信中的请求对象,按照自定义协议的格式来展开
* */
@Data
public class Request {private int type;private int length;private byte[] payload;
}
/*
* 表示一个网络通信中的响应对象,也是根据自定义应用层协议来的
* */
@Data
public class Response {private int type;private int length;private byte[] payload;
}

 🎄 定义BasicArguments

使用这个类表示方法的公共参数/辅助的字段 ,后续的每个方法会有一些不同的参数,不同的参数再使用不同的子类来表示。

rid代表请求的id,和响应的id一样,他们是一对

channel表示的是“逻辑连接”,表示客户端各种模块复用一个TCP连接,

channelId就代表这些连接。

@Data
public class BasicArguments implements Serializable {
//     表示一次请求/响应的身份标识,可以把请求和响应对上protected String rid;
//    客户端的身份标识protected String channelId;
}

🎄 定义BasicReturns

使用这个类标识各个远程调用的方法的返回值的公共信息

/*
* 标识各个远程调用的方法的返回值的公共信息
* */
@Data
public class BasicReturns implements Serializable {
//    用来标识唯一的请求和响应protected String rid;protected String channelId;
//    用来表示当前远程调用方法的返回值protected boolean ok;
}

🍅 2、创建参数类

根据前面VirtualHost中的十多个方法,每个方法创建一个类,标识该方法中的相关参数。

那么这个参数到底是如何进行传递的?

如下图,以交换机的参数进行举例。

关于我们远程调用的过程:当发起请求时,就把这些参数通过请求传过去,然后调用VirtualHost中的API(就是VirtualHost中的那些创建删除方法),调用完以后再返回响应。

以下是有关交换机的请求报文:

以下是创建交换机的响应报文:没有请求报文复杂是因为,响应只需要返回请求是否执行远程调用是否成功即可。 

以下就创建这些参数类: 

        🎄 交换机

 创建交换机:

@Data
public class ExchangeDeclareArguments extends BasicArguments implements Serializable {private String ExchangeName;private ExchangeType exchangeType;private boolean durable;
}

删除交换机:

@Data
public class ExchangeDeleteArguments extends BasicArguments implements Serializable {private String exchangeName;
}

        🎄 队列

创建队列:

@Data
public class QueueDeclareArguments extends BasicArguments implements Serializable {private String QueueName;private boolean durable;
}

删除队列:

@Data
public class QueueDeleteArguments extends BasicArguments implements Serializable {private String queueName;
}

        🎄 绑定

创建绑定:

@Data
public class QueueBindArguments extends BasicArguments implements Serializable {private String exchangeName;private String queueName;private String bindingKey;
}

删除绑定:

@Data
public class QueueUnbindArguments extends BasicArguments implements Serializable {private String queueName;private String exchangeName;
}

        🎄发布消息

@Data
public class BasicPublishArguments extends BasicArguments implements Serializable {private String exchangeName;private String routingKey;private BasicProperties basicProperties;private byte[] body;
}

        🎄 订阅消息

这个方法参数,还包含一个Consumer consumer。

这是一个回调函数,这个回调函数是不能作为参数进行传输的,因为这个回调函数,是客户端这边的。

比如,这里请求调用一个”订阅队列“的远程方法,

客户端这边:服务器收到了请求,执行了basicConsume方法,并且返回了响应。订阅以后,客户端的消费者就会在后面收到消息,而这个回调函数是在消费者收到消息以后,才会进行逻辑处理,而不是再发送请求时进行传递的。

服务器这边:执行的是一个固定的回调函数:把消息返回给客户端。

@Data
public class BasicConsumeArguments extends BasicArguments implements Serializable {private String consumerTag;private String queueName;private boolean autoAck;
}

        🎄确认应答

@Data
public class BasicAckArguments extends BasicArguments implements Serializable {private String queueName;private String messageId;
}

        🎄 消息推送

前面的都是客户端给服务器发送消息,这里是服务器给消费者推送消息。所以要继承BasicReturns。

@Data
public class SubScribeReturns extends BasicReturns implements Serializable {private String consumerTag;private BasicProperties basicProperties;private byte[] body;
}

二、服务器设计

在 mqServer包中创建一个BrokerServer类。

 🍅 1、编写实例变量和构造方法

 private ServerSocket serverSocket = null;private VirtualHost virtualHost = new VirtualHost("default");//    使用这个哈希表,表示当前所有会话(那些客户端在和这个服务器进行通信)
//    此处的key是channelId,value是对应的 socket对象private ConcurrentHashMap<String , Socket> sessions = new ConcurrentHashMap<String ,Socket>();//    引入线程池,处理多个客户端的请求private ExecutorService executorService = null;//    引入boolean变量控制服务器是否运行private volatile boolean runnable = true;public BrokerServer(int port) throws IOException {
//        端口号serverSocket = new ServerSocket(port);}

🍅 2、编写启动类和关闭类

 这里利用了线程池,不断的处理连接

public void start() throws IOException {System.out.println("[BrokerServer] 启动!");executorService = Executors.newCachedThreadPool();try {while (runnable) {Socket clientSocket = serverSocket.accept();// 把处理连接的逻辑丢给这个线程池.executorService.submit(() -> {processConnection(clientSocket);});}} catch (SocketException e) {System.out.println("[BrokerServer] 服务器停止运行!");// e.printStackTrace();}}public void stop() throws IOException {runnable = false;
//        停止线程池executorService.shutdownNow();serverSocket.close();}private void processConnection(Socket clientSocket) {//TODO
}

🍅 3、编写处理连接的方法:processConnection()

处理一个客户端的连接,主要有以下几步:

        (1)读取请求并且解析

        (2)根据请求计算响应

        (3)把相应协写回给客户端

//    通过该方法,处理一个客户端的连接
//    在一个连接中,可能会涉及到多个连接和请求private void processConnection(Socket clientSocket) throws IOException {
//        获取到流对象,读取应用层协议try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()){
//                按照特定格式来读取并且解析(转换),此时就需要用到DataInputStream和DataOutputStreamtry (DataInputStream dataInputStream = new DataInputStream(inputStream);DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {while (true) {
//                  1、读取请求并且解析Request request = readRequest(dataInputStream);
//                  2、根据请求计算响应Response response = process(request, clientSocket);
//                  3、把响应写回给客户端writeResponse(dataOutputStream,response);}}}catch (EOFException|SocketException e) {
//                DataInputStream如果读到EOF(文件末尾),会抛出一个EOFException异常
//                视为正常的异常,用或者异常来结束循环System.out.println("[BrokerServer] connection 关闭! 客户端的地址: " + clientSocket.getInetAddress().toString()+ ":" + clientSocket.getPort());} catch (IOException | ClassNotFoundException | MqException e) {System.out.println("[BrokerServer] connection 出现异常!");e.printStackTrace();} finally {try {clientSocket.close();
//          一个TCP连接中,可能含有多个channel,需要把当前socket对应的channel也顺便清理掉clearClosedSession(clientSocket);}catch (IOException e) {e.printStackTrace();}}}

 🍅 4、编写读取请求readRequest()和写回响应writeResponse方法

这里就是根据前面设定的报文格式来编写的读取请求和写回响应的方法,这里的payload的具体内容在这里不作解析,在后面的process方法中进行解析

//    读取请求并且解析private Request readRequest(DataInputStream dataInputStream) throws IOException {Request request = new Request();//        读取出请求中4个字节的typerequest.setType(dataInputStream.readInt());
//        读出4个字节的lengthrequest.setLength(dataInputStream.readInt());byte[] payload = new byte[request.getLength()];int n = dataInputStream.read(payload);if (n != request.getLength()){throw new IOException("读取请求格式出错");}request.setPayload(payload);return request;}//    把响应写回给客户端private void writeResponse(DataOutputStream dataOutputStream, Response response) throws IOException {dataOutputStream.writeInt(response.getType());dataOutputStream.writeInt(response.getLength());dataOutputStream.write(response.getPayload());
//        刷新缓冲区dataOutputStream.flush();}

🍅 5、实现根据请求计算响应:process()方法编写

这里就要针对具体的payload进行编写了。

当前请求中的payload里面的内容,是根据type来的,如下

VirtualHost中的十多个方法:
0x1创建channel
0x2关闭channel
0x3创建exchange
0x4销毁exchange
0x5创建queue
0x6销毁queue
0x7创建binding
0x8销毁binding
0x9发送message
0xa订阅message
0xb返回ack
0xc服务器给客户端推送的消息(被订阅的消息)(响应独有)

如果是0x3,就是创建交换机对应的参数...... 

主要分为以下几步:

        1、把request中的payload作出一个初步的解析

        2、根据type的值,进一步区分请求要做什么

        3、构造响应

private Response process(Request request, Socket clientSocket) throws IOException, ClassNotFoundException, MqException {// 1. 把 request 中的 payload 做一个初步的解析.BasicArguments basicArguments = (BasicArguments) BinaryTool.fromBytes(request.getPayload());System.out.println("[Request] rid=" + basicArguments.getRid() + ", channelId=" + basicArguments.getChannelId()+ ", type=" + request.getType() + ", length=" + request.getLength());// 2. 根据 type 的值, 来进一步区分接下来这次请求要干啥.boolean ok = true;if (request.getType() == 0x1) {// 创建 channelsessions.put(basicArguments.getChannelId(), clientSocket);System.out.println("[BrokerServer] 创建 channel 完成! channelId=" + basicArguments.getChannelId());} else if (request.getType() == 0x2) {// 销毁 channelsessions.remove(basicArguments.getChannelId());System.out.println("[BrokerServer] 销毁 channel 完成! channelId=" + basicArguments.getChannelId());} else if (request.getType() == 0x3) {// 创建交换机. 此时 payload 就是 ExchangeDeclareArguments 对象了.ExchangeDeclareArguments arguments = (ExchangeDeclareArguments) basicArguments;ok = virtualHost.exchangeDeclare(arguments.getExchangeName(), arguments.getExchangeType(),arguments.isDurable());} else if (request.getType() == 0x4) {
//        删除交换机ExchangeDeleteArguments arguments = (ExchangeDeleteArguments) basicArguments;ok = virtualHost.exchangeDelete(arguments.getExchangeName());} else if (request.getType() == 0x5) {
//            创建队列QueueDeclareArguments arguments = (QueueDeclareArguments) basicArguments;ok = virtualHost.queueDeclare(arguments.getQueueName(), arguments.isDurable());} else if (request.getType() == 0x6) {
//        删除队列QueueDeleteArguments arguments = (QueueDeleteArguments) basicArguments;ok = virtualHost.queueDelete((arguments.getQueueName()));} else if (request.getType() == 0x7) {
//            创建绑定QueueBindArguments arguments = (QueueBindArguments) basicArguments;ok = virtualHost.queueBind(arguments.getQueueName(), arguments.getExchangeName(), arguments.getBindingKey());} else if (request.getType() == 0x8) {//    删除绑定QueueUnbindArguments arguments = (QueueUnbindArguments) basicArguments;ok = virtualHost.queueUnbind(arguments.getQueueName(), arguments.getExchangeName());} else if (request.getType() == 0x9) {BasicPublishArguments arguments = (BasicPublishArguments) basicArguments;ok = virtualHost.basicPublish(arguments.getExchangeName(), arguments.getRoutingKey(),arguments.getBasicProperties(), arguments.getBody());} else if (request.getType() == 0xa) {BasicConsumeArguments arguments = (BasicConsumeArguments) basicArguments;ok = virtualHost.basicConsume(arguments.getConsumerTag(), arguments.getQueueName(), arguments.isAutoAck(),new Consumer() {// 这个回调函数要做的工作, 就是把服务器收到的消息可以直接推送回对应的消费者客户端@Overridepublic void handleDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) throws MqException, IOException {// 先知道当前这个收到的消息, 要发给哪个客户端.// 此处 consumerTag 其实是 channelId. 根据 channelId 去 sessions 中查询, 就可以得到对应的// socket 对象了, 从而可以往里面发送数据了// 1. 根据 channelId 找到 socket 对象Socket clientSocket = sessions.get(consumerTag);if (clientSocket == null || clientSocket.isClosed()) {throw new MqException("[BrokerServer] 订阅消息的客户端已经关闭!");}// 2. 构造响应数据SubScribeReturns subScribeReturns = new SubScribeReturns();subScribeReturns.setChannelId(consumerTag);subScribeReturns.setRid(""); // 由于这里只有响应, 没有请求, 不需要去对应. rid 暂时不需要.subScribeReturns.setOk(true);subScribeReturns.setConsumerTag(consumerTag);subScribeReturns.setBasicProperties(basicProperties);subScribeReturns.setBody(body);byte[] payload = BinaryTool.toBytes(subScribeReturns);Response response = new Response();// 0xc 表示服务器给消费者客户端推送的消息数据.response.setType(0xc);// response 的 payload 就是一个 SubScribeReturnsresponse.setLength(payload.length);response.setPayload(payload);// 3. 把数据写回给客户端.//    注意! 此处的 dataOutputStream 这个对象不能 close !!!//    如果 把 dataOutputStream 关闭, 就会直接把 clientSocket 里的 outputStream 也关了.//    此时就无法继续往 socket 中写入后续数据了.DataOutputStream dataOutputStream = new DataOutputStream(clientSocket.getOutputStream());writeResponse(dataOutputStream, response);}});} else if (request.getType() == 0xb) {// 调用 basicAck 确认消息.BasicAckArguments arguments = (BasicAckArguments) basicArguments;ok = virtualHost.basicAck(arguments.getQueueName(), arguments.getMessageId());} else {// 当前的 type 是非法的.throw new MqException("[BrokerServer] 未知的 type! type=" + request.getType());}// 3. 构造响应BasicReturns basicReturns = new BasicReturns();basicReturns.setChannelId(basicArguments.getChannelId());basicReturns.setRid(basicArguments.getRid());basicReturns.setOk(ok);byte[] payload = BinaryTool.toBytes(basicReturns);Response response = new Response();response.setType(request.getType());response.setLength(payload.length);response.setPayload(payload);System.out.println("[Response] rid=" + basicReturns.getRid() + ", channelId=" + basicReturns.getChannelId()+ ", type=" + response.getType() + ", length=" + response.getLength());return response;}

🍅 6、清理过期的sessions:clearClosedSession()

    //    遍历sessions hash表,把该被关闭的socket对应的键值对都删掉private void clearClosedSession(Socket clientSocket) {List<String> toDeleteChannelId = new ArrayList<>();for(Map.Entry<String,Socket> entry : sessions.entrySet()){if(entry.getValue() == clientSocket){
//                使用集合类,不能一边遍历,一边删除toDeleteChannelId.add(entry.getKey());}}for (String channelId : toDeleteChannelId){sessions.remove(channelId);}System.out.println("[BrokerServer]清理session完成~ 被清理的channeId = " + toDeleteChannelId);}

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

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

相关文章

Linux常用命令学习总结

Linux命令分类 1. Linux目录操作命令2. Linux文件名称3. Linux磁盘命令4. Linux进程与防火墙5. Linux用户与组的关系6. Linux权限操作(chmod命令)7. Linux中的文件类型文件所有者修改 最近系统地学习下Linux命令的使用&#xff0c;因此作如下记录&#xff0c;以便随时复习和翻阅…

windows 安装免费3用户ccproxy ubuntu 代理上网

Windows 上进行安装 ubuntu 上进行设置 方法一 (临时的手段) 如果仅仅是暂时需要通过http代理使用apt-get&#xff0c;您可以使用这种方式。 在使用apt-get之前&#xff0c;在终端中输入以下命令&#xff08;根据您的实际情况替换yourproxyaddress和proxyport&#xff09;。 终…

NetSuite 固定资产租赁101

目录 前言 1.新租赁准则的相关内容 1.1 主要变化 1.2 IFRS 16/ASC 842/CAS 21的区别与联系 1.3 新租赁准则实行的意义 2.NetSuite中的租赁功能 2.1 概述 2.2 设置 2.2.1 相关科目设置 2.2.2 资产类型设置 2.3 功能详细说明 2.3.1 案例一 2.3.2 案例二 3.新租赁准则…

ubuntu切换python版本

在没有安装类似anoconda的管理工具的时候&#xff0c;我们常常会被Ubuntu下的Python版本切换问题所头疼。 可以使用update-alternatives工具进行python版本的任意切换 当使用update-alternatives工具来切换Ubuntu系统上的Python版本时&#xff0c;您实际上是在系统范围内选择…

week4刷题

题解: F(n)F(n−1)F(n−2) 由于斐波那契数存在递推关系&#xff0c;因此可以使用动态规划求解。动态规划的状态转移方程即为上述递推关系&#xff0c;边界条件为 F(0)F(0)F(0) 和 F(1)F(1)F(1)。 class Solution { public:int fib(int n) {int MOD 1000000007;if (n < 2)…

6.pip简介,第三方库的安装

引言 使用过Visual Studio的小伙伴可能对npm不陌生,没错,pip与npm的功能是一样的。 首先要知道,Python这门语言拥有着丰富的标准库以及先辈们开发的各种功能强大的第三方库。而今天我们主要学习的呢就是关于Python中的包管理工具。它是Python的默认软件包管理工具,可以方便…

mybatis如何生成和执行动态sql

文章目录 1. 相关代码2. SQL 语句解析流程2.1 XMLStatementBuilder2.2 SqlSource2.3 DynamicContext上下文2.4 SqlNode和组合模式2.5 MappedStatement2.6 解析标签2.6.1 \<include>2.6.2 \<selectKey>2.6.3 处理 SQL 语句 3. 获取真正执行的sql 1. 相关代码 packa…

棒球在国际上的流行·棒球1号位

棒球在国际上的流行 1. 棒球的起源与历史 棒球的起源源于美国。19世纪中叶&#xff0c;由于美国领土的扩张&#xff0c;当时的美国殖民地的印第安人将棒球类游戏&#xff0c;带到了当时的弗吉尼亚州的奥克兰。后来&#xff0c;棒球运动流传到了加利福尼亚州的圣迭戈。早期的棒…

LeetCode--HOT100题(25)

目录 题目描述&#xff1a;141. 环形链表&#xff08;简单&#xff09;题目接口解题思路代码 PS: 题目描述&#xff1a;141. 环形链表&#xff08;简单&#xff09; 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连…

QtCreator ui设置界面 Layout 的属性 layoutStretch

layoutStretch 用于控制Layout在被用户进行缩放时。里面控件的缩放比例。如一个水平布局里面有两个控件 一个 QLineEdit 和 QPushButton。首先将两个控件的尺寸策列的水平策略都设置为Expanding。此时在将包含这两个控件的水平布局的 layoutStretch 进行如下设置。 运行程序就…

利用python实现网络设备配置批量上传和批量下载功能

利用python实现网络设备配置批量上传和批量下载功能 利用ensp实现网络设备和物理主机互通配置网络设备配置批量上传功能配置批量下载功能常见问题 提示&#xff1a; 本文章代码所使用目录均使用相对目录&#xff0c;只需将配置存放目录和文件下载目录&#xff08;已用符号标出…

8.利用matlab完成 符号微积分和极限 (matlab程序)

1.简述 一、符号微积分 微积分的数值计算方法只能求出以数值表示的近似解&#xff0c;而无法得到以函数形式表示的解析解。在 MATLAB 中&#xff0c;可以通过符号运算获得微积分的解析解。 1. 符号极限 MATLAB 中求函数极限的函数是 limit&#xff0c;可用来求函数在指定点的…

Node.js新手在哪儿找小项目练手?

前言 可以参考一下下面的nodejs相关的项目&#xff0c;希望对你的学习有所帮助&#xff0c;废话少说&#xff0c;让我们直接进入正题>> 1、 NodeBB Star: 13.3k 一个基于Node.js的现代化社区论坛软件&#xff0c;具有快速、可扩展、易于使用和灵活的特点。它支持多种数…

数据结构-队列(C语言的简单实现)

简介 队列也是一种数据结构&#xff0c;队列也可以用来存放数字每次只能向队列里将入一个数字&#xff0c;每次只能从队列里获得一个数字在队列中&#xff0c;允许插入的一段称为入队口&#xff0c;允许删除的一段称为出队口它的原则是先进先出(FIFO: first in first out)&…

Titanic--细节记录二

目录 merge、join以及concat的方法的不同以及相同 merge join concat stack函数 agg函数 countplot--计算条形统计图 FacetGrid kdeplot--核密度估计图 facet.set facet.add_legend() 折线图表示年龄分布情况 为什么所有的曲线都被添加到同一个图上&#xff1a; 填充…

标记垃圾,有三种色彩:四千长文带你深入了解三色标记算法

&#x1f52d; 嗨&#xff0c;您好 &#x1f44b; 我是 vnjohn&#xff0c;在互联网企业担任 Java 开发&#xff0c;CSDN 优质创作者 &#x1f4d6; 推荐专栏&#xff1a;Spring、MySQL、Nacos、Java&#xff0c;后续其他专栏会持续优化更新迭代 &#x1f332;文章所在专栏&…

MFC计算分贝

分贝的一种定义是&#xff0c;表示功率量之比的一种单位&#xff0c;等于功率强度之比的常用对数的10倍&#xff1b; 主要用于度量声音强度&#xff0c;常用dB表示&#xff1b; 其计算&#xff0c;摘录网上一段资料&#xff1b; 声音的分贝值可以通过以下公式计算&#xff1…

【数据结构】‘双向链表’冲冲冲

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

Mybatis-Plus

1. Mybatis-Plus概念 1.1 Mybatis-Plus介绍 官⽹&#xff1a; https://mybatis.plus/ 或 https://mp.baomidou.com/ Mybatis-Plus 介绍 MyBatis-Plus &#xff08;简称 MP &#xff09;是⼀个 MyBatis 的增强⼯具&#xff0c;在 MyBatis 的基础上只做增强不做改变&#xff0c;…

“可一学院”区块链学习平台正式启动,助力BSV技术普及与传播

2023年8月8日&#xff0c;上海可一澈科技有限公司&#xff08;以下简称“可一科技”&#xff09; 正式发布区块链学习平台“可一学院”。“可一学院” 立足于BSV区块链技术本源&#xff0c;汇集了多层次的专业课程和学习资源&#xff0c;致力于打造一个适合各类人群使用的一站式…