MeteorCat / SpringWebsocket聊天室

Created Wed, 04 Oct 2023 20:27:01 +0800 Modified Wed, 29 Oct 2025 23:25:05 +0800
1518 Words

SpringWebsocket聊天室

这里用于从头搭建自己需要搭建私域交流聊天室, 用于可以内部定制处理, 数据备份写入 Redis 等待其他进程落地关系数据库.


<dependencies>

    <!-- WebSocket库 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>

    <!-- spring 开发配置 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- ....... -->
</dependencies>

默认这样处理就行了, 主要是先搭建好基本框架. 先处理下 echo 流程.

package com.meteorcat.chats;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.lang.NonNull;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

/**
 * 这里先采用 TextWebSocketHandler 来做接收处理
 * TextWebSocketHandler 为文本流, 而 BinaryWebSocketHandler 为二进制流
 *
 * @author MeteorCat
 */
public class ChatRuntime extends TextWebSocketHandler {


    /**
     * 日志句柄
     */
    protected final Log logger = LogFactory.getLog(this.getClass());


    /**
     * 当客户端服务端连接的回调切片(OnOpen)
     *
     * @param session WebSocket句柄
     * @throws Exception 异常
     */
    @Override
    public void afterConnectionEstablished(@NonNull WebSocketSession session) throws Exception {
        logger.debug("OnEstablished");
    }


    /**
     * 当客户端服务端关闭的回调切片(OnClose)
     *
     * @param session WebSocket句柄
     * @param status  关闭状态 null 为正常关闭
     * @throws Exception 异常
     */
    @Override
    public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus status) throws Exception {
        logger.debug("OnClose");
    }


    /**
     * 二进制消息传递的回调, 采用 handleTextMessage
     * 如果集成 BinaryWebSocketHandler 则是需要实现 handleBinaryMessage
     *
     * @param session WebSocket句柄
     * @param message 传输的数据内容
     * @throws Exception 异常
     */
    @Override
    public void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) throws Exception {
        logger.debug("OnMessage");

        // 这里 echo 一下返回给客户端确认调通
        session.sendMessage(message);
    }

}

之后就是挂载配置对象:

package com.meteorcat.chats.config;

import com.meteorcat.chats.ChatRuntime;
import org.springframework.context.annotation.Bean;
import org.springframework.lang.NonNull;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;

/**
 * 注册Websocket配置
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    /**
     * 对ServerContainer读写进行配置
     *
     * @return ServletServerContainerFactoryBean
     */
    @Bean
    public ServletServerContainerFactoryBean servletServerContainerFactoryBean() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();

        // 设置请求缓冲区
        container.setMaxTextMessageBufferSize(8192);
        container.setMaxBinaryMessageBufferSize(8192);
        return container;
    }


    /**
     * 注册服务端点
     *
     * @param registry 注册句柄
     */
    @Override
    public void registerWebSocketHandlers(@NonNull WebSocketHandlerRegistry registry) {
        // 注册消息到 `/chat` 访问端点, 注意加上 setAllowedOrigins 防止域被禁止访问
        registry.addHandler(chatHandler(), "/chat").setAllowedOrigins("*");
    }

    /**
     * 获取配置唯一对象
     *
     * @return WebSocketHandler
     */
    @Bean
    public WebSocketHandler chatHandler() {
        return new ChatRuntime();
    }

}

最后的 application.properties:

# 先最简单配置下
spring.application.name = WebSocketChat
server.address = 127.0.0.1
server.port = 18080


# 日志
logging.level.com.meteorcat = debug

启动完成之后, 这里先配置下 H5 做下客户端请求:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">

    <title>游戏聊天室</title>

    <!-- 新 Bootstrap5 核心 CSS 文件 -->
    <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/5.1.1/css/bootstrap.min.css">

    <!--  popper.min.js 用于弹窗、提示、下拉菜单 -->
    <script src="https://cdn.staticfile.org/popper.js/2.9.3/umd/popper.min.js"></script>

    <!-- 最新的 Bootstrap5 核心 JavaScript 文件 -->
    <script src="https://cdn.staticfile.org/twitter-bootstrap/5.1.1/js/bootstrap.min.js"></script>

    <!--  popper.min.js 用于弹窗、提示、下拉菜单 -->
    <script src="https://cdn.staticfile.org/zepto/1.2.0/zepto.min.js"></script>

</head>
<body>

<!-- Nav Begin -->
<nav class="navbar navbar-expand-sm bg-dark navbar-dark">
    <ul class="navbar-nav">
        <li class="nav-item active">
            <a class="nav-link" href="javascript:void(0)">WebSocket</a>
        </li>
    </ul>
</nav>
<!-- Nav End -->

<div class="container mt-4">
    <div class="row">


        <!-- Input Group Begin -->
        <div class="col-sm-4">
            <form autocomplete="off">
                <div class="mb-3 input-group">
                    <span class="input-group-text">请求地址(ws://)</span>
                    <input type="url" class="form-control" id="address" name="address" value="127.0.0.1">
                </div>
                <div class="mb-3 input-group">
                    <span class="input-group-text">请求端口</span>
                    <input type="number" class="form-control" id="port" name="port" value="18080">
                </div>
                <div class="mb-3 input-group">
                    <span class="input-group-text">请求路径</span>
                    <input type="text" class="form-control" id="path" name="path" value="/chat">
                </div>
                <div class="mb-3">
                    <textarea class="form-control" placeholder="发送内容" name="message" id="message" cols="30"
                              rows="10" style="resize: none"></textarea>
                </div>
                <button type="button" class="btn btn-primary js-connect">连接</button>
                <button type="button" class="btn btn-success js-send-text disabled">发送</button>
                <button type="button" class="btn btn-danger js-close disabled">关闭</button>
            </form>
        </div>
        <!-- Input Group End -->

        <!-- Text Area Begin -->
        <div class="col-sm-8">
            <div class="mb-3">
                <h2>请求结果</h2>
                <div class="alert alert-info js-cards"></div>
            </div>
        </div>
        <!-- Text Area End -->
    </div>
</div>

<script>

    // 全局共享的请求句柄
    window.handlers = {
        url: "",
        socket: null,
        text2buffer: function (text) {
            let buf = new Uint8Array(text.length);
            for (let i = 0; i < text.length; i++) {
                buf[i] = text.charCodeAt(i);
            }
            return buf;
        },
        open: function (event) {
            console.log(event);
        },
        close: function (event) {
            $('.js-close').trigger("click");
        },
        error: function (event) {
            $('.js-close').trigger("click");
        },
        message: function (event) {
            const node = $('.js-cards');
            node.prepend(
                    `<div class="card bg-success text-white">
                    <div class="card-body">响应数据: ${event.data}</div>
                </div>
                <br>`
            );
        }
    };

    $(function () {

        // 连接WebSocket
        $('.js-connect').click(function () {
            // 参数内容
            const address = $('#address').val();
            const port = $('#port').val();
            const path = $('#path').val();

            // 构建请求URL
            let url;
            if (address.startsWith("ws://")) {
                url = `${address}:${port}${path}`;
            } else {
                url = `ws://${address}:${port}${path}`;
            }


            // 初始化对象
            window.handlers.url = url;
            window.handlers.socket = new WebSocket(url);

            // 绑定事件
            window.handlers.socket.onopen = window.handlers.open;
            window.handlers.socket.onclose = window.handlers.close;
            window.handlers.socket.onerror = window.handlers.error;
            window.handlers.socket.onmessage = window.handlers.message;

            // 设置可用请求
            $('.js-send-text').removeClass("disabled");
            $('.js-close').removeClass("disabled");
        });


        // 关闭请求
        $('.js-close').click(function () {
            if (window.handlers.socket instanceof WebSocket) {
                window.handlers.socket.close();
                window.handlers.socket = null;
            }
            $('.js-send-text').addClass("disabled");
            $('.js-close').addClass("disabled");
        });


        // 发送文本数据
        $('.js-send-text').click(function () {
            // 参数内容
            const message = $('#message').val();
            if (window.handlers.socket instanceof WebSocket) {

                // 字符串转为Uint8字符串
                window.handlers.socket.send(message);
            }
        });
    });
</script>
</body>
</html>

确认测试没问题, 之后的 websocket 服务器将以此作为基点来开发.