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

news2024/11/24 5:48:14

目录

一、自定义应用层协议

🍅 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();
        while (runnable){
            Socket clientSocket = serverSocket.accept();
//            把处理连接的逻辑给线程池
            executorService.submit(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }

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和DataOutputStream
            try(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 {
            clientSocket.close();
//          一个TCP连接中,可能含有多个channel,需要把当前socket对应的channel也顺便清理掉
            clearClosedSession(clientSocket);
        }
    }

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

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

//    读取请求并且解析
    private Request readRequest(DataInputStream dataInputStream) throws IOException {
        Request request = new Request();
 //        读取出请求中4个字节的type
        request.setType(dataInputStream.readInt());
//        读出4个字节的length
        request.setLength(dataInputStream.readInt());
        byte[] payload = new byte[request.getLength()];
        int n = dataInputStream.read(payload);
        if (n != request.getLength()){
            throw new IOException("读取请求格式出错");
        }
        request.setPayload(request.getPayload());
        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){
//            创建channel
            sessions.put(basicArguments.getChannelId(),clientSocket);
            System.out.println("[BrokerServer]创建channel完成!channelId = " + basicArguments.getChannelId());
        }else if (request.getType() == 0x2){
            sessions.remove(basicArguments.getChannelId());
            System.out.println("[BrokerServer]销毁完成!channelId = " + basicArguments.getChannelId());
        }else if (request.getType() == 0x3){
//            创建交换机
            ExchangeDeclareArguments arguments = (ExchangeDeclareArguments) basicArguments;
            ok = virtualHost.exchangeDeclare(arguments.getExchangeName(),arguments.getExchangeType(),arguments.isDurable());
        }else if (request.getType() == 0x4){
            ExchangeDeclareArguments arguments = (ExchangeDeclareArguments) 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() {
//                            该回调函数,将服务器收到的消息推送回对应的消费者客户端
                        @Override
                        public void handleDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) throws MqException, IOException {
//                            先知道当前收到的消息,要发送给那个客户端,
//                            此处的consumerTag其实就是channelId,根据channelId去session中查询,就可以得到对应的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);
//                            此处rid不设置,因为这里只有响应没有请求,rid不需要去对应
                            subScribeReturns.setRid("");
                            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就是一个subScribeReturns
                            response.setLength(payload.length);
                            response.setPayload(payload);
                            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 +
                ",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.coloradmin.cn/o/871310.html

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

相关文章

CSDN互利共赢玩法实战!!!

csdn项目第一波基本都顺利跑了起来&#xff0c;我们总计找来了一两千个新的项目源码&#xff0c;来让大家变现。 在实战中&#xff0c;主要两个玩法&#xff0c;一个引流&#xff0c;一个付费资源。付费资源门槛越来越高&#xff0c;所以我们这一波升级完成的号&#xff0c;就非…

AKStream+ZLM简单配置

下载AKStream源代码 下载AKStream源代码 git clone https://gitee.com/chatop2020/AKStreamVS2022打开AKStream&#xff0c;低于.net6的版本无法编译通过 打开 .sln 解决方案 如下配置AKStreamWeb 数据库配置&#xff1a; MySQL AKStreamWeb.json中配置 port 是MySQL的端口…

单片机第一季:零基础13——AD和DA转换

1&#xff0c;AD转换基本概念 51 单片机系统内部运算时用的全部是数字量&#xff0c;即0 和1&#xff0c;因此对单片机系统而言&#xff0c;无法直接操作模拟量&#xff0c;必须将模拟量转换成数字量。所谓数字量&#xff0c;就是用一系列0 和1 组成的二进制代码表示某个信号大…

掌握Python的X篇_33_MATLAB的替代组合NumPy+SciPy+Matplotlib

numPy 通常与 SciPy( Scientific Python )和 Matplotlib (绘图库)一起使用&#xff0c;这种组合广泛用于替代 MatLab&#xff0c;是一个强大的科学计算环境&#xff0c;有助于我们通过 Python 学习数据科学或者机器学习。 文章目录 1. numpy1.1 numpy简介1.2 矩阵类型的nparra…

【设计模式】前端控制器模式

前端控制器模式&#xff08;Front Controller Pattern&#xff09;是用来提供一个集中的请求处理机制&#xff0c;所有的请求都将由一个单一的处理程序处理。该处理程序可以做认证/授权/记录日志&#xff0c;或者跟踪请求&#xff0c;然后把请求传给相应的处理程序。以下是这种…

XML 数据传输格式

目录 XML简介 一、初识XML 1.什么是 XML&#xff1f; 2.XML 和 HTML 之间的差异 3.XML 不会做任何事情 4.通过 XML 您可以发明自己的标签 5.XML 不是对 HTML 的替代 二、XML 用途 1.XML 把数据从 HTML 分离 2.XML 简化数据共享 3.XML 简化数据传输 三、XML 树结构 1.一个 XML 文…

简单介绍C++中的模板

目录 一、泛型编程 泛型编程的概念: 泛型编程举例: 二、函数模板 函数模板的概念&#xff1a; 函数模板的格式&#xff1a; 函数模板的实例化: 隐式实例化&#xff1a; 显式实例化&#xff1a; 模板参数的匹配原则: 三、类模板 类模板的格式定义&#xff1a; 类模…

PyQt5组件之QLabel显示图像和视频

目录 一、显示图像和视频 1、显示图像 2、显示视频 二、QtDesigner 窗口简单介绍 三、相关函数 1、打开本地图片 2、保存图片到本地 3、打开文件夹 4、打开本地文本文件并显示 5、保存文本到本地 6、关联函数 7、图片 “.png” | “.jpn” Label 自适应显示 一、显…

C++ 之 线性插值 贝塞尔曲线 非线性动画

非线性动画在程序&#xff0c;游戏和动画中运用非常广泛&#xff0c;那么我们应该如何实现&#xff1f; 非线性动画上的点在s-t图像上非线性&#xff0c;即不为一次函数&#xff0c;实则为处处连续的曲线 对于此曲线可模拟&#xff0c;这里我们用贝塞尔曲线 一&#xff0c;基本…

Azure DevOps基于 Net6.0 的 WPF 程序如何进行持续集成、持续编译

正文 1&#xff0c; Azure DevOps 创建项目 Project name&#xff1a;”NetCore_WPF_Sample“ Visibility&#xff1a;”Private“&#xff08;根据实际项目需求&#xff09; Version control&#xff1a;”Git“ Work item process&#xff1a;”Agile“ 点击 ”Create“…

【linux】2 软件管理器yum和编辑器vim

目录 1. linux软件包管理器yum 1.1 什么是软件包 1.2 关于rzsz 1.3 注意事项 1.4 查看软件包 1.5 如何安装、卸载软件 1.6 centos 7设置成国内yum源 2. linux开发工具-Linux编辑器-vim使用 2.1 vim的基本概念 2.2 vim的基本操作 2.3 vim正常模式命令集 2.4 vim末行…

【设计模式】MVC 模式

MVC 模式代表 Model-View-Controller&#xff08;模型-视图-控制器&#xff09; 模式。这种模式用于应用程序的分层开发。 Model&#xff08;模型&#xff09; - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑&#xff0c;在数据变化时更新控制器。View&#xff…

【爬虫】爬取旅行评论和评分

以马蜂窝“普达措国家公园”为例&#xff0c;其评论高达3000多条&#xff0c;但这3000多条并非是完全向用户展示的&#xff0c;向用户展示的只有5页&#xff0c;数了一下每页15条评论&#xff0c;也就是75条评论&#xff0c;有点太少了吧&#xff01; 因此想了个办法尽可能多爬…

Linux 终端命令之文件浏览(2) more

Linux 文件浏览命令 cat, more, less, head, tail&#xff0c;此五个文件浏览类的命令皆为外部命令。 hannHannYang:~$ which cat /usr/bin/cat hannHannYang:~$ which more /usr/bin/more hannHannYang:~$ which less /usr/bin/less hannHannYang:~$ which head /usr/bin/he…

最新智能AI系统+ChatGPT源码搭建部署详细教程+知识库+附程序源码

近期有网友问宝塔如何搭建部署AI创作ChatGPT&#xff0c;小编这里写一个详细图文教程吧。 使用Nestjs和Vue3框架技术&#xff0c;持续集成AI能力到AIGC系统&#xff01; 增加手机端签到功能、优化后台总计绘画数量逻辑&#xff01;新增 MJ 官方图片重新生成指令功能同步官方 …

nginx负载均衡配置过程

一、环境说明 主机名IPnginx服务器nginx-server192.168.198.141web页面1web1192.168.198.100web页面2web2192.168.198.200 关闭所有主机的防火墙和Selinux服务 二、配置过程 自定义页面 自定义web1和web2的页面 主配置文件 查看nginx的主配置文件 vim /usr/local/nginx/c…

全球八分之一的河流受到缺氧影响

一项全球研究发现&#xff0c;世界各地河流中的溶解氧含量低得危险。缺氧的真实发生率可能更高。 小型、低梯度的城市河流&#xff0c;例如图中北卡罗来纳州的那条河流&#xff0c;是最容易缺氧的河流之一。图片来源&#xff1a;乔安娜布拉扎克 2023 年 3 月&#xff0c;《卫报…

LeetCode--HOT100题(29)

目录 题目描述&#xff1a;19. 删除链表的倒数第 N 个结点&#xff08;中等&#xff09;题目接口解题思路代码 PS: 题目描述&#xff1a;19. 删除链表的倒数第 N 个结点&#xff08;中等&#xff09; 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链…

Microsoft365家庭版1年订阅新功能及版本对比

Microsoft 365可帮助您工作、学习、组织、连接和创&#xff0c;只需一项方便的订阅&#xff0c;即可尽享具有 Microsft 365 的6款精品应用、可同时登录5 台设备&#xff08;包括 Windows、macOS、iOS 和 Android 设备&#xff09;、高级安全性等&#xff0c;并且可以自由管理授…

升级STM32电机PID速度闭环编程:从F1到F4的移植技巧与实例解析

引言&#xff1a; 在嵌入式系统开发中&#xff0c;STM32系列微控制器广泛应用于各种应用领域。而对于直流有刷电机的控制&#xff0c;PID速度闭环是一种常用的控制方式。本文将以此为例&#xff0c;探讨如何从STM32F1系列移植到STM32F4系列&#xff0c;并详细介绍HAL库在不同型…