Spring Boot整合STOMP实现实时通信

news2024/11/24 2:02:14

目录

引言

代码实现

配置类WebSocketMessageBrokerConfig

 DTO

工具类

Controller

common.html

stomp-broadcast.html

运行效果 

完整代码地址


引言

STOMP(Simple Text Oriented Messaging Protocol)作为一种简单文本导向的消息传递协议,提供了一种轻量级且易于使用的方式来实现实时通信。本篇博客将讲解如何使用Spring Boot创建一个基于STOMP的WebSocket应用程序,并展示相关的配置类。同时,还会介绍如何使用Thymeleaf模板引擎生成动态的HTML页面,以展示实时通信的效果。

代码实现

配置类WebSocketMessageBrokerConfig

package com.wsl.websocket.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketMessageBrokerConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // it is OK to leave it here
        registry.addEndpoint("/broadcast");
        //registry.addEndpoint("/broadcast").withSockJS();
        // custom heartbeat, every 60 sec
        //registry.addEndpoint("/broadcast").withSockJS().setHeartbeatTime(60_000);
    }
}

 DTO

package com.wsl.websocket.dto;

import com.wsl.websocket.util.StringUtils;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class ChatMessage {

    private String from;
    private String text;
    private String recipient;
    private String time;

    public ChatMessage() {

    }

    public ChatMessage(String from, String text, String recipient) {
        this.from = from;
        this.text = text;
        this.recipient = recipient;
        this.time = StringUtils.getCurrentTimeStamp();
    }
}

工具类

package com.wsl.websocket.util;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class StringUtils {
    
    private static final String TIME_FORMATTER= "HH:mm:ss";
    
    public static String getCurrentTimeStamp() {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(TIME_FORMATTER);
        return LocalDateTime.now().format(formatter);
    }
}

Controller

package com.wsl.websocket.controller;

import com.wsl.websocket.dto.ChatMessage;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class WebSocketBroadcastController {

    @GetMapping("/stomp-broadcast")
    public String getWebSocketBroadcast() {
        return "stomp-broadcast";
    }

    @GetMapping("/sockjs-broadcast")
    public String getWebSocketWithSockJsBroadcast() {
        return "sockjs-broadcast";
    }

    @MessageMapping("/broadcast")
    @SendTo("/topic/broadcast")
    public ChatMessage send(ChatMessage chatMessage) {
        return new ChatMessage(chatMessage.getFrom(), chatMessage.getText(), "ALL");
    }
}

common.html

src/main/resources/templates/common.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="headerfiles">
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/4.4.1/css/bootstrap.css}"/>
    <link rel="stylesheet" type="text/css" th:href="@{/css/main.css}"/>
    <script th:src="@{/webjars/jquery/3.4.1/jquery.js}"></script>
</head>
<body>
<footer th:fragment="footer" class="my-5 text-muted text-center text-small">
    <p class="mb-1">© 2020 Dariawan</p>
    <ul class="list-inline">
        <li class="list-inline-item"><a href="https://www.dariawan.com">Homepage</a></li>
        <li class="list-inline-item"><a href="#">Articles</a></li>
    </ul>
</footer>
</body>
</html>

stomp-broadcast.html

src/main/resources/templates/stomp-broadcast.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>WebSocket With STOMP Broadcast Example</title>
    <th:block th:include="common.html :: headerfiles"></th:block>
</head>
<body>
<div class="container">
    <div class="py-5 text-center">
        <a href="/"><h2>WebSocket</h2></a>
        <p class="lead">WebSocket Broadcast - with STOMP</p>
    </div>
    <div class="row">
        <div class="col-md-6">
            <div class="mb-3">
                <div class="input-group">
                    <input type="text" id="from" class="form-control" placeholder="Choose a nickname"/>
                    <div class="btn-group">
                        <button type="button" id="connect" class="btn btn-sm btn-outline-secondary" onclick="connect()">
                            Connect
                        </button>
                        <button type="button" id="disconnect" class="btn btn-sm btn-outline-secondary"
                                onclick="disconnect()" disabled>Disconnect
                        </button>
                    </div>
                </div>
            </div>
            <div class="mb-3">
                <div class="input-group" id="sendmessage" style="display: none;">
                    <input type="text" id="message" class="form-control" placeholder="Message">
                    <div class="input-group-append">
                        <button id="send" class="btn btn-primary" onclick="send()">Send</button>
                    </div>
                </div>
            </div>
        </div>
        <div class="col-md-6">
            <div id="content"></div>
            <div>
                        <span class="float-right">
                            <button id="clear" class="btn btn-primary" onclick="clearBroadcast()"
                                    style="display: none;">Clear</button>
                        </span>
            </div>
        </div>
    </div>
</div>

<footer th:insert="common.html :: footer"></footer>

<script th:src="@{/webjars/stomp-websocket/2.3.3-1/stomp.js}" type="text/javascript"></script>
<script type="text/javascript">
    var stompClient = null;
    var userName = $("#from").val();

    function setConnected(connected) {
        $("#from").prop("disabled", connected);
        $("#connect").prop("disabled", connected);
        $("#disconnect").prop("disabled", !connected);
        if (connected) {
            $("#sendmessage").show();
        } else {
            $("#sendmessage").hide();
        }
    }

    function connect() {
        userName = $("#from").val();
        if (userName == null || userName === "") {
            alert('Please input a nickname!');
            return;
        }
        /*<![CDATA[*/
        var url = /*[['ws://'+${#httpServletRequest.serverName}+':'+${#httpServletRequest.serverPort}+@{/broadcast}]]*/ 'ws://localhost:8080/broadcast';
        /*]]>*/
        stompClient = Stomp.client(url);
        stompClient.connect({}, function () {
            stompClient.subscribe('/topic/broadcast', function (output) {
                showBroadcastMessage(createTextNode(JSON.parse(output.body)));
            });

            sendConnection(' connected to server');
            setConnected(true);
        }, function (err) {
            alert('error' + err);
        });
    }

    function disconnect() {
        if (stompClient != null) {
            sendConnection(' disconnected from server');

            stompClient.disconnect(function () {
                console.log('disconnected...');
                setConnected(false);
            });
        }
    }

    function sendConnection(message) {
        var text = userName + message;
        sendBroadcast({'from': 'server', 'text': text});
    }

    function sendBroadcast(json) {
        stompClient.send("/app/broadcast", {}, JSON.stringify(json));
    }

    function send() {
        var text = $("#message").val();
        sendBroadcast({'from': userName, 'text': text});
        $("#message").val("");
    }

    function createTextNode(messageObj) {
        return '<div class="row alert alert-info"><div class="col-md-8">' +
            messageObj.text +
            '</div><div class="col-md-4 text-right"><small>[<b>' +
            messageObj.from +
            '</b> ' +
            messageObj.time +
            ']</small>' +
            '</div></div>';
    }

    function showBroadcastMessage(message) {
        $("#content").html($("#content").html() + message);
        $("#clear").show();
    }

    function clearBroadcast() {
        $("#content").html("");
        $("#clear").hide();
    }
</script>
</body>
</html>

运行效果 

http://localhost:8080/stomp-broadcast

完整代码地址

 GitHub - wangsilingwsl/springboot-stomp: springboot integrates stomp

指定消息的目标用户

此功能基于Spring Boot,和上面代码分隔开,没有关联关系。请结合实际情况参考下列代码。

配置类

package com.twqc.config.websocket;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * WebSocket消息代理配置类
 * <p>
 * 注解@EnableWebSocketMessageBroker表示启用WebSocket消息代理功能。不开启不能使用@MessageMapping和@SendTo注解。
 * 注意:@MessageMapping和@SendTo的使用方法:
 * <p>
 * 注解@MessageMapping的方法用于接收客户端发送的消息,方法的参数用于接收消息内容,方法的返回值用于发送消息内容。
 * 注解@SendTo的方法用于发送消息内容,方法的返回值用于发送消息内容。
 * <p>
 * 示例:@MessageMapping("/example")注解的方法用于接收客户端发送的消息,@SendTo("/topic/example")注解的方法用于发送消息内容。
 * 3.1 对应的客户端连接websocket的路径为:ws://localhost:8080/example
 * 3.2 对应的客户端发送消息的路径为:/app/example
 * 3.3 对应的客户端接收消息的路径为:/topic/example
 * 3.4 app和topic在WebSocketMessageBrokerConfigurer.configureMessageBroker方法中配置
 * 3.5 具体的路径需要自己定义,上文仅为示例,与本项目中使用的路径无关。
 * <p>
 *
 * @author wsl
 * @date 2024/2/29
 * @see WebSocketMessageBrokerConfigurer
 * @see MessageMapping
 * @see SendTo
 * @see <a href="https://www.dariawan.com/tutorials/spring/spring-boot-websocket-stomp-tutorial/">Spring Boot WebSocket with STOMP Tutorial</a>
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 配置消息代理
     *
     * @param config 消息代理配置注册器
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 设置消息代理的目的地前缀,所有以"/websocket/topic"开头的消息都会被代理发送给订阅了该目的地的客户端
        config.enableSimpleBroker("/websocket/topic");
        // 设置应用的目的地前缀,所有以"/websocket/app"开头的消息都会被路由到带有@MessageMapping注解的方法中进行处理
        config.setApplicationDestinationPrefixes("/websocket/app");
        // 设置用户的目的地前缀,所有以"/user"开头的消息都会被代理发送给订阅了该目的地的用户
        config.setUserDestinationPrefix("/user");
    }

    /**
     * 注册消息端点
     * 可以注册多个消息端点,每个消息端点对应一个URL路径。
     *
     * @param registry 消息端点注册器
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 注册消息端点,参数为消息端点的URL路径(对应@MessageMapping注解的路径,也是客户端连接的路径)
        registry.addEndpoint("/websocket/bpm/runFlow")
                // 设置允许的跨域请求来源
                .setAllowedOrigins("*");
    }

    /**
     * 配置客户端入站通道
     *
     * @param registration 客户端入站通道注册器
     */
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        // 设置客户端入站通道的自定义拦截器
        registration.interceptors(new MyWebSocketInterceptor());
    }
}

拦截器

package com.twqc.config.websocket;

import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;

import java.util.List;

/**
 * WebSocket拦截器
 *
 * @author wsl
 * @date 2024/3/4
 */
@Slf4j
public class MyWebSocketInterceptor implements ChannelInterceptor {

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);

        if (StompCommand.CONNECT.equals(accessor.getCommand())) {
            List<String> nativeHeaders = accessor.getNativeHeader("username");
            if (nativeHeaders != null) {
                String username = nativeHeaders.get(0);
                // 存入Principal
                accessor.setUser(() -> username);
                log.info("用户:{}发起了stomp连接", username);
            } else {
                log.info("未提供用户名的stomp连接");
                return message;
            }
        }

        return message;
    }
}

用户的消息推送有两种实现方式

@SendToUser

    @MessageMapping("/websocket/bpm/runFlow")
    @SendToUser("/websocket/topic/websocket/bpm/runFlow")
    public String runFlow2(Principal principal) {
        if (principal == null || principal.getName() == null) {
            log.error("未提供用户名的stomp连接,无法运行流程");
        }
        String username = principal.getName();
        return "success" + username;
    }

SimpMessagingTemplate

    @MessageMapping("/websocket/bpm/runFlow")
    public void runFlow(FlowRequest request, Principal principal) {
        if (principal == null || principal.getName() == null) {
            log.error("未提供用户名的stomp连接,无法运行流程");
        }
        String username = principal.getName();
        flowService.runFlow(request, username);
    }

 flowService

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    private void sendNodeResult(FlowResponse response, String username) {
        messagingTemplate.convertAndSendToUser(username, BpmConstant.Flow.TOPIC_FLOW_RESULTS, response);
    }

前端(客户端)

<template>
  <div id="app">
    <!-- 发送消息表单 -->
    <van-form @submit="onSubmit">
      <van-field
        v-model="content"
        name="内容"
        label="内容"
        rows="3"
        autosize
        type="textarea"
        placeholder="请输入内容"
      />
      <div style="margin: 16px">
        <van-button round block type="info" native-type="submit"
          >提交</van-button
        >
      </div>
    </van-form>

    <!-- 消息回复体 -->
    <van-cell-group>
      <van-cell
        v-for="(msgItem, msgIndex) in msgList"
        :key="msgIndex"
        :title="'回复消息' + (msgIndex + 1)"
        value=""
        :label="msgItem"
      />
    </van-cell-group>
  </div>
</template>

<script>
import Stomp from "stompjs";
let socketTimer = null;

export default {
  name: "App",
  created() {
    this.username = "admin";
    this.initWebsocket();
  },
  data() {
    return {
      content: "",
      stompClient: null,
      msgList: [],
      username: "admin",
    };
  },
  methods: {
    initWebsocket() {
      this.stompClient = Stomp.client(
        "ws://192.168.1.109:7010/websocket/bpm/runFlow"
      );
      this.stompClient.debug = null;
      const headers = {
        username: this.username,
      };
      this.stompClient.connect(
        headers, // 自定义请求头
        () => {
          this.stompClient.subscribe(
            "/user/websocket/topic/websocket/bpm/runFlow",
            (res) => {
              this.msgList.push(JSON.stringify(res));
            }
          );
        },
        (err) => {
          console.log("err", err);
          this.$toast("连接失败:" + JSON.stringify(err));
          // 监听报错信息,手动发起重连
          if (socketTimer) {
            clearInterval(socketTimer);
          }
          // 10s后重新连接一次
          socketTimer = setTimeout(() => {
            this.initWebsocket();
          }, 10000);
        }
      );
      // this.stompClient.heartbeat.outgoing = 10000;
      // 若使用STOMP 1.1 版本,默认开启了心跳检测机制(默认值都是10000ms)
      // this.stompClient.heartbeat.incoming = 0;
      // 客户端不从服务端接收心跳包
    },
    closeWebsocket() {
      if (this.stompClient !== null) {
        this.stompClient.disconnect(() => {
          console.log("关闭连接");
        });
      }
    },
    onSubmit() {
      // 发送信息
      // 转成json对象
      this.stompClient.send(
        "/websocket/app/websocket/bpm/runFlow",
        {},
        // JSON.stringify({ content: this.content })
        this.content
      );
    },
  },
  destroyed() {
    // 页面销毁后记得关闭定时器
    clearInterval(socketTimer);
    this.closeWebsocket();
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>

 请注意,客户端接收信号时,需要在订阅地址前加上/app,否则接收失败。

完整代码地址

GitHub - wangsilingwsl/vue-stomp

参考:Spring Boot + WebSocket With STOMP Tutorial | Dariawan 

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

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

相关文章

基础---nginx 启动不了,跟 Apache2 服务冲突

文章目录 查看 nginx 服务状态nginx 启动后 访问页面 127.0.0.1停止 nginx 服务&#xff0c;访问不了页面停止/启动 Apache2 服务&#xff0c;启动 Apache2 页面访问显示正确nginx 莫名启动不了卸载 Apache2 服务器 启动 nginx &#xff0c;但是总是不能实现反向代理&#xff0…

Java手写简易数据库--持续更新中

MYDB 0. 项目结构0.1 引用计数缓存框架为什么不使用LRU引用计数缓存缓存框架实现 0.2 共享内存数组 1. 事务管理器--TM1.1 XID 文件XID 规则XID 文件结构读取方式事务状态 1.2 代码实现 2. 数据管理器--DM2.1 页面缓存页面结构页面缓存数据页管理第一页普通页 2.2 日志文件 3. …

Linux-新手小白速秒Hadoop集群全生态搭建(图文混编超详细)

在之前的文章中&#xff0c;我教会大家如何一步一步搭建一个Hadoop集群&#xff0c;但是只提供了代码&#xff0c;怕有些朋友会在一些地方产生疑惑&#xff0c;今天我来以图文混排的方式&#xff0c;一站式交给大家如何搭建一个Hadoop高可用集群包括&#xff08;HadoopHA&#…

HTML基础:img图像标签的4个属性值详解

你好&#xff0c;我是云桃桃。今天来聊一聊图片标签。 语法 HTML <img> 标签用于在网页中插入图像&#xff0c;它是 HTML 中的一个自闭合标签。通过在网页中显示图像&#xff0c;可以丰富页面内容、传达信息和提升用户体验。 <img src"img/jay01.jpg" al…

汽车IVI中控开发入门及进阶(十三):语音识别

前言: IVI中控上的语音识别,在目前市场上也是非常显眼的一个创新,大幅改变了传统IVI的操作习惯。 语音识别Speech recognition,也称为自动语音识别(ASR)、计算机语音识别或语音到文本,是一种使程序能够将人类语音处理成书面格式的能力。 语音识别Speech recognition是计…

【JACS】:用于稳定单原子分散的催化剂架构可对吸附到 Pt 原子、氧化 Pt 簇和 TiO2上金属 Pt 簇的 CO 进行特定位点光谱和反应性测量

摘要&#xff1a;氧化物负载的贵金属纳米粒子是广泛使用的工业催化剂。由于费用和稀有性&#xff0c;开发降低贵金属纳米颗粒尺寸并稳定分散物质的合成方案至关重要。负载型原子分散的单贵金属原子代表了最有效的金属利用几何结构&#xff0c;尽管由于合成均匀且稳定的单原子分…

机器学习周记(第三十周:文献阅读-SageFormer)2024.3.11~2024.3.17

目录 摘要 ABSTRACT 1 论文信息 1.1 论文标题 1.2 论文摘要 1.3 论文背景 2 论文模型 2.1 问题描述 2.2 模型信息 2.2.1 Series-aware Global Tokens&#xff08;序列感知全局标记&#xff09; 2.2.2 Graph Structure Learning&#xff08;图结构学习&#xff09; …

【图像分割】使用Otsu 算法及迭代计算最佳全局阈值估计并实现图像分割(代码实现与分析)

本实验要求理解全局阈值分割的概念&#xff0c;并实现文本图像分割。需要大家深入理解Ostu 算法的实现过程及其迭代原理&#xff0c;同时通过学习使用Otsu 算法及其迭代&#xff0c;实践图像分割技术在文本图像处理中的应用。 以下将从实验原理、实验实现、实验结果分析三部分对…

数据结构-队列java实现

队列 队列(queue)1.队列的特点2.数组模拟队列JAVA代码3.上述过程优化 博文主要是自己学习的笔记&#xff0c;供自己以后复习使用&#xff0c; 参考的主要教程是B站的 尚硅谷数据结构和算法 队列(queue) 1.队列的特点 1&#xff09;队列是一个有序列表&#xff0c;可以用数组…

cannot find -xml2: No such file or directory的解决方法

一&#xff0c;问题现象 在编译库的时候出现如下图所示的报错&#xff1a;C:/msys64/mingw32/bin/…/lib/gcc/i686-w64-mingw32/13.2.0/…/…/…/…/i686-w64-mingw32/bin/ld.exe: ca nnot find -lxml2: No such file or directory collect2.exe: error: ld returned 1 exit s…

Linux环境下用IDEA运行Golang记录

一、背景 和存储同时开发AI项目&#xff0c;在Linux环境运行Golang项目&#xff0c;因此需要进行相关的配置。 二、Golang安装 参考&#xff1a;【Linux — 安装 Go】Linux 系统安装 Go 过程总结_linux 安装go-CSDN博客 三、IDEA中Golang配置 1、去除代理 否则在Plugins中…

奇舞周刊第522期:“Vite 又开始搞事情了!!!”

奇舞推荐 ■ ■ ■ Vite 又开始搞事情了&#xff01;&#xff01;&#xff01; Vite 的最新版本将引入一种名为 Rolldown 的新型打包工具。 unocss 究竟比 tailwindcss 快多少&#xff1f; 我们知道 unocss 很快&#xff0c;也许是目前最快的原子化 CSS 引擎 (没有之一)。 巧用…

每日五道java面试题之mybatis篇(一)

目录&#xff1a; 第一题. MyBatis是什么&#xff1f;第二题. ORM是什么?第三题. 为什么说Mybatis是半自动ORM映射工具&#xff1f;它与全自动的区别在哪里&#xff1f;第四题. 传统JDBC开发存在的问题第五题. JDBC编程有哪些不足之处&#xff0c;MyBatis是如何解决这些问题的…

差分逻辑电平 — LVDS、CML、LVPECL、HCSL互连

前言 首先了解差分逻辑电平、单端逻辑电平的基础知识 地址&#xff1a;常见的逻辑电平_常用的逻辑电平-CSDN博客 注&#xff1a; ECL >> PECL >> LVPECL演变&#xff1b; ECL速度快&#xff0c;驱动能力强&#xff0c;噪声小&#xff0c;但是功耗大&#xff0c;使…

SpringBoot(数据库操作 + druid监控功能)

文章目录 1.JDBC HikariDataSource&#xff08;SpringBoot2默认数据源&#xff09;1.数据库表设计2.引入依赖 pom.xml3.配置数据源参数 application.yml4.编写一个bean&#xff0c;映射表5.编写测试类来完成测试1.引入依赖 pom.xml2.使用JdbcTemplate进行测试3.成功&#xff0…

并发编程CountDownLatch浅析

目录 一、CountDownLatch简介二、源码三、使用3.1 demo13.2 demo2 四、应用场景五、参考链接 一、CountDownLatch简介 CountDownLatch(倒计时锁存器)是Java并发包中的一个工具类&#xff0c;用于实现多个线程之间的同步。它通过一个计数器来实现线程之间的等待和唤醒操作&…

51单片机系列-单片机定时器

&#x1f308;个人主页&#xff1a;会编辑的果子君 &#x1f4ab;个人格言:“成为自己未来的主人~” 软件延时的缺点 延时过程中&#xff0c;CPU时间被占用&#xff0c;无法进行其他任务&#xff0c;导致系统效率降低&#xff0c;延时时间越长&#xff0c;该缺点就越明显&…

esp8266调试记录

连接笔记本电脑 使用笔记本电脑的USB接口为NodeMCU开发板供电&#xff0c;你需要确保电压和电流在安全范围内。虽然NodeMCU的输入输出电压限制为3.3V&#xff0c;但是大多数开发板都内置了电压调节器&#xff0c;可以从5V的USB电源降压到3.3V。因此&#xff0c;通常情况下&…

通用的springboot web jar包执行脚本,释放端口并执行jar包

1、通用的springboot web jar包执行脚本&#xff0c;释放端口并执行jar包&#xff1a; #!/bin/bash set -eDATE$(date %Y%m%d%H%M) # 基础路径 BASE_PATH/data/yitu-projects/yitu-xzhq/sftp # 服务名称。同时约定部署服务的 jar 包名字也为它。 SERVER_NAMEyitu-server # 环境…

【图论】拓补排序 - 邻接表

文章目录 题目&#xff1a;310. 最小高度树题目描述代码与注释 题目&#xff1a;310. 最小高度树 题目描述 代码与注释 func findMinHeightTrees(n int, edges [][]int) (ans []int) {if n 1 {return []int{0}}g : make([][]int, n)degree : make([]int, n) // 记录每个节点…