Bootstrap

SpringBoot实现WebSocket

参考链接:https://www.kancloud.cn/king_om/mic_03/2783864

一、环境搭建

1.创建SpringBoot项目,引入相关依赖

<dependencies>
        <!-- Spring Boot核心启动器,引入常用依赖基础 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- Spring Boot对Thymeleaf模板引擎支持,用于视图渲染 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- nekohtml库,用于HTML解析,指定版本1.9.22 -->
        <dependency>
            <groupId>net.sourceforge.nekohtml</groupId>
            <artifactId>nekohtml</artifactId>
            <version>1.9.22</version>
        </dependency>

        <!-- JUnit 4测试框架依赖,仅测试阶段用,版本4.13.2 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

        <!-- JUnit Jupiter(JUnit 5部分),用于测试,仅测试环境 -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- Spring Boot Web开发启动器,构建Web应用相关 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Boot测试相关依赖,确保应用正确性等 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-test</artifactId>
        </dependency>

        <!-- Spring Boot对WebSocket启动器,实现双向通信功能 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
    </dependencies>

2.在resources下边创建statictemplates文件夹
3.配置application.yml

spring:
  thymeleaf:
    cache: false # 关闭Thymeleaf页面缓存,开发时便于即时看到模板修改效果
    encoding: UTF-8 # 模板编码设为UTF-8,确保字符正确解析,避免乱码
    prefix: classpath:/templates/  # 页面映射路径:模板文件查找路径前缀,在类路径下的templates目录找
    suffix:.html # 视图对应的模板文件后缀名
    mode: HTML5 # 设置模板模式为HTML5,遵循HTML5规范解析处理
    mvc:
      pathmatch:
        matching-strategy: ant_path_matcher # Spring MVC路径匹配采用ant_path_matcher策略,更灵活处理URL路径
      static-path-pattern: /static/** # 定义静态资源访问路径模式,通过/static/开头的URL可访问static目录下静态资源

二、配置类开启WebSocket支持

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @ Description: 开启WebSocket支持
 * 用于在Spring框架的应用中配置和启用WebSocket功能。
 * 通过相关注解和方法的定义,使得应用能够正确地处理WebSocket连接和通信。
 */
@Configuration
public class WebSocketConfig {
    @Bean //用于将方法返回的ServerEndpointExporter对象作为一个Bean注册到Spring的容器中
    public ServerEndpointExporter serverEndpointExporter() {
        //创建并返回一个ServerEndpointExporter对象。
        // ServerEndpointExporter主要作用是扫描带有@ServerEndpoint注解的WebSocket端点类,并将它们注册到Servlet容器中,
        // 从而使得应用能够正确地处理WebSocket连接请求,实现WebSocket的通信功能。
        return new ServerEndpointExporter();
    }
}

三、服务层

import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个 websocket 服务器端。
 * 注解的值将被用于监听用户连接的终端访问 URL 地址,客户端可以通过这个 URL 来连接到 WebSocket 服务器端
 */
@Component
@Service
/**@ServerEndpoint注解用于将当前类定义为一个WebSocket服务器端端点。注解中的值"/api/websocket/{sid}"指定了客户端连接到这个WebSocket服务器端的URL地址,
其中{sid}是一个路径参数,可以在后续的方法中获取并使用,不同的客户端可以通过带有不同sid值的这个URL来建立与服务器的WebSocket连接。
*/
 @ServerEndpoint("/api/websocket/{sid}")
public class WebSocketServer {

    private Session session;  //用户信息
    //存放每个客户端对应的MyWebSocket对象,保存所有已连接的客户端对应的WebSocketServer实例
    private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();
    //当前在线连接数,用于记录当前有多少个客户端与WebSocket服务器建立了连接
    private static int onlineCount = 0;
    //用于接收从客户端连接URL路径参数中获取的sid值,这个sid可以用于标识不同的客户端连接或者与客户端相关的业务逻辑处理
    private String sid = "";


    /**
     * 连接建立成功调用的方法
     * 这个方法会处理与新连接建立相关的初始化操作,
     * 比如记录客户端的会话信息、将当前对象添加到已连接客户端集合中、更新在线连接数等。
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        this.session = session;
        webSocketSet.add(this);     //加入set中,将当前新建立连接的WebSocketServer对象添加到存放所有客户端的集合中
        this.sid = sid;
        addOnlineCount();           //在线数加1,调用方法增加当前的在线连接数统计值
        try {
            sendMessage("conn_success"); // 向当前新连接的客户端发送一条消息"conn_success",告知客户端连接成功。
            // 在控制台打印出有新窗口开始监听的sid以及当前的在线人数信息。
            System.out.println("有新窗口开始监听:" + sid + ",当前在线人数为:" + getOnlineCount());
        } catch (IOException e) {
            // 如果发送消息过程中出现IO异常,在控制台打印出相应提示信息
            System.out.println("websocket IO Exception");
        }
    }

    /**
     * 连接关闭调用的方法
     */
    // @OnClose注解标记的方法会在WebSocket连接关闭时被自动调用。这个方法主要处理与连接关闭相关的清理操作,
    // 比如从已连接客户端集合中删除对应的对象、更新在线连接数等。
    @OnClose
    public void onClose() {
        webSocketSet.remove(this);  //从set中删除,将当前关闭连接的WebSocketServer对象从存放所有客户端的集合中移除
        subOnlineCount();           //在线数减1,调用方法减少当前的在线连接数统计值

        // 在控制台打印出当前关闭连接所对应的sid值
        System.out.println("释放的sid为:"+sid);
        // 在控制台打印出有一连接关闭的提示信息以及更新后的当前在线人数
        System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
    }
    /**
     * 接受到用户信息后调用的方法
     * @param message
     * @param session
     */
    // @OnMessage注解标记的方法会在WebSocket服务器接收到客户端发送的消息时被自动调用。这个方法主要负责处理接收到的消息,
    // 比如在控制台打印出消息来源及内容,并且可以根据业务需求对消息进行进一步的处理,这里是将接收到的消息群发出去。
    @OnMessage
    public void onMessage(String message,Session session){
        // 在控制台打印出收到消息的来源窗口(通过sid标识)以及消息的具体内容
        System.out.println("收到来自窗口" + sid + "的信息:" + message);
        //群发消息
        for (WebSocketServer item : webSocketSet) {
            try {
                //遍历所有已连接的客户端对应的WebSocketServer对象,调用每个对象的sendMessage方法将接收到的消息发送给每个客户端,实现群发功能
                item.sendMessage(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * @ Param session
     * @ Param error
     */
    // @OnError注解标记的方法会在WebSocket连接过程中出现错误时被自动调用。这个方法主要负责处理错误情况,
    // 比如在控制台打印出错误提示信息以及打印出详细的错误堆栈信息,以便于排查问题
    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("发生错误");
        error.printStackTrace();
    }

    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
        // 通过当前客户端的会话对象(this.session)获取基本的远程通信端点(getBasicRemote),然后使用sendText方法将指定的消息发送给客户端
        this.session.getBasicRemote().sendText(message);
    }

    /**
     * 群发自定义消息
     */
    public static void sendInfo(String message, @PathParam("sid") String sid) throws IOException {
        // 在控制台打印出要推送消息的目标窗口(通过sid标识)以及推送的具体内容
        System.out.println("推送消息到窗口" + sid + ",推送内容:" + message);

        for (WebSocketServer item : webSocketSet) {
            try {
                //为null则全部推送
                if (sid == null) {
                    item.sendMessage(message);
                } else if (item.sid.equals(sid)) {
                    // 遍历所有已连接的客户端对应的WebSocketServer对象,如果sid为null则表示要向所有客户端推送消息,
                    // 如果当前对象的sid与要推送的目标sid相等,则调用该对象的sendMessage方法将消息发送给对应的客户端。
                    item.sendMessage(message);
                }
            } catch (IOException e) {
                // 如果在发送消息给某个客户端过程中出现IO异常,跳过当前循环,继续尝试给下一个客户端发送消息。
                continue;
            }
        }
    }

    // 以下这几个方法用于对在线连接数(onlineCount)以及存放客户端的集合(webSocketSet)进行操作,
    // 并且都使用了synchronized关键字来保证在多线程环境下对这些共享资源的操作是线程安全的。

    /*该方法用于获取当前的在线连接数。
     * 由于在线连接数(onlineCount)是一个被多个方法可能同时访问和修改的共享变量,
     * 为了确保在多线程环境下获取到的在线连接数是准确的,使用了synchronized关键字进行同步。
     * 这样在某个线程调用此方法获取在线连接数时,其他线程不能同时对onlineCount进行修改操作,保证了数据的一致性。
    */
    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    /**
     * 该方法用于增加在线连接数。
     * 当有新的客户端与WebSocket服务器成功建立连接时(如在onOpen方法中)会调用此方法。
     * 同样因为onlineCount是共享变量,多个线程可能同时尝试增加它的值(比如多个客户端同时连接),
     * 使用synchronized关键字确保在同一时刻只有一个线程能够执行此方法对onlineCount进行自增操作,
     * 避免了数据不一致的情况,比如多个线程同时增加导致计数错误的问题。
     */
    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }

    /**
     * 该方法用于减少在线连接数。
     * 当有客户端与WebSocket服务器的连接关闭时(如在onClose方法中)会调用此方法。
     * 与增加在线连接数的方法类似,为了保证在多线程环境下对onlineCount进行准确的自减操作,
     * 使用synchronized关键字进行同步,防止多个线程同时对其进行操作而导致数据错误。
     */
    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }

    /**
     * 该方法用于获取存放所有已连接客户端对应的WebSocketServer对象的集合(webSocketSet)。
     * 虽然这里获取集合的操作相对简单,但由于webSocketSet也是一个可能被多个线程访问的共享资源,
     * 使用synchronized关键字进行同步,确保在获取集合时,其他线程不会对其进行修改等操作,
     * 从而保证获取到的集合状态是准确的,可以安全地在获取到集合后进行后续的遍历等操作(如在sendInfo方法中遍历集合发送消息)
     * @return
     */
    public static CopyOnWriteArraySet<WebSocketServer> getWebSocketSet() {
        return webSocketSet;
    }
}

四、前端页面

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Java 后端 WebSocket 的 Tomcat 实现</title>
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
</head>

<body>
Welcome<br/><input id="text" type="text" />
<button onclick="send()">发送消息</button>
<hr/>
<button onclick="closeWebSocket()">关闭WebSocket连接</button>
<hr/>
<div id="message"></div>
</body>
<script type="text/javascript">
    // 定义一个全局变量websocket,用于存储创建的WebSocket对象,初始值为null
    var websocket = null;
    //判断当前浏览器是否支持WebSocket
    if('WebSocket' in window) {
        // 如果浏览器支持WebSocket,创建一个WebSocket连接对象,连接到指定的服务器地址,
        // 这里连接到本地的8080端口,路径为/api/websocket/100,其中100可能是一个示例的连接标识或参数
        websocket = new WebSocket("ws://localhost:8080/api/websocket/100");
    } else {
        // 如果浏览器不支持WebSocket,弹出一个警告框,提示用户当前浏览器不支持WebSocket
        alert('当前浏览器 Not support websocket')
    }
    // 连接发生错误回调方法
    // 当WebSocket连接过程中出现错误时,会自动调用此函数
    websocket.onerror = function() {
        // 在网页上显示"WebSocket连接发生错误"的提示信息
        setMessageInnerHTML("WebSocket连接发生错误");
    };

    //连接成功建立回调方法
    // 当WebSocket连接成功建立时,会自动调用此函数
    websocket.onopen = function() {
        // 在网页上显示"WebSocket连接成功"的提示信息
        setMessageInnerHTML("WebSocket连接成功");
    }
    var U01data, Uidata, Usdata
    //接收消息回调方法
    // 当WebSocket服务器发送消息过来时,会自动调用此函数
    websocket.onmessage = function(event) {
        //在控制台打印接收到的消息事件对象,用于调试查看消息的详细信息
        console.log(event);
        // 将接收到的消息内容显示在网页上
        setMessageInnerHTML(event.data);
    }

    //连接关闭回调方法
    // 当WebSocket连接关闭时,会自动调用此函数
    websocket.onclose = function() {
        setMessageInnerHTML("WebSocket连接关闭");
    }

    //监听窗口关闭事件
    // 当用户尝试关闭浏览器窗口时,会自动调用此函数
    window.onbeforeunload = function() {
        // 调用closeWebSocket函数,关闭当前的WebSocket连接
        closeWebSocket();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
        //在控制台打印要显示在网页上的内容
        console.log(innerHTML)
        // 通过id获取网页上的div元素(id为"message"),并将传入的内容添加到该元素的innerHTML属性中
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //关闭WebSocket连接
    function closeWebSocket() {
        // 调用WebSocket对象的close方法,关闭当前建立的WebSocket连接
        websocket.close();
    }

    //发送消息
    function send() {
        // 通过id获取网页上文本输入框(id为"text")中的值,即用户输入的消息内容
        var message = document.getElementById('text').value;
        // 使用WebSocket对象的send方法,将用户输入的消息以特定格式(这里是一个包含"msg"字段的JSON字符串)发送给服务器
        websocket.send('{"msg":"' + message + '"}');
        // 调用setMessageInnerHTML函数,将用户输入的消息显示在网页上,并添加一个换行符(&#13;)
        setMessageInnerHTML(message + "&#13;");
    }
</script>

</html>

五、运行效果

两个浏览器模拟两个用户对话:
在这里插入图片描述
控制台
在这里插入图片描述

;