websocket实现简单的单聊、群聊demo

一.后端:

maven依赖:

XML 复制代码
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
java 复制代码
import org.springframework.boot.autoconfigure.AutoConfiguration;
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;
import org.violet.common.core.constants.ComConst;

@AutoConfiguration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint(ComConst.WEB_SOCKET_ENDPOINT)
                .setAllowedOriginPatterns("*")
                .withSockJS();
    }

    /**
     * 配置消息代理
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 启用简单消息代理,订阅前缀为 /topic、/queue、/user
        registry.enableSimpleBroker("/topic", "/queue", "/user");
        // 定义客户端发送消息的前缀(客户端发送消息需要加上/app前缀)
        registry.setApplicationDestinationPrefixes("/app");
        // 定义用户目的地前缀(用于点对点消息,例如:/user/10086/notice)
        registry.setUserDestinationPrefix("/user");
    }

}

websocket工具类:

java 复制代码
import lombok.RequiredArgsConstructor;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Component;

/**
 * WebSocket 消息发送工具
 */
@Component
@RequiredArgsConstructor
public class WebSocketUtil {

    private final SimpMessagingTemplate messagingTemplate;

    /**
     * 广播消息 → 所有在线前端都能收到
     */
    public void sendBroadcast(String destination, Object msg) {
        messagingTemplate.convertAndSend(destination, msg);
    }

    /**
     * 点对点发送 → 只发给指定用户
     */
    public void sendToUser(String userId, String destination, Object msg) {
        messagingTemplate.convertAndSendToUser(userId, destination, msg);
    }
}

控制层:

java 复制代码
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.violet.common.launch.entity.JsonResult;
import org.violet.zhdd.util.WebSocketUtil;

@Slf4j
@RestController
@RequestMapping("/ws")
@RequiredArgsConstructor
public class ZhddWebSocketController {

    private final WebSocketUtil webSocketUtil;

    // 广播测试
    @RequestMapping("/send-all")
    public JsonResult sendAll(String msg) {
        webSocketUtil.sendBroadcast("/topic/all", msg);
        return JsonResult.OK("发送成功:全员广播");
    }

    // 点对点测试
    @RequestMapping("/send-user/{userId}")
    public JsonResult sendUser(@PathVariable String userId, String msg) {
        webSocketUtil.sendToUser(userId, "/notice", msg);
        return JsonResult.OK("发送成功:用户" + userId);
    }

    /**
     * 接收前端发送的消息
     * 前端发送地址:/app/chat
     * 后端只需要写 /chat
     */
    @MessageMapping("/chat")
    public void receiveChatMessage(String message) {
        if (!JSONUtil.isTypeJSON(message)) {
            log.error("消息格式错误,请发送JSON格式的消息,message:{}", message);
        }
        JSONObject jsonObject = JSONUtil.parseObj(message);
        if (!jsonObject.containsKey("msg") && StrUtil.isBlank(jsonObject.getStr("msg"))) {
            log.error("消息内容msg不能为空,msg:{}", message);
        }
        // sendUserId:发送者;receiveUserId:接收者;msg:消息内容;
        if (jsonObject.containsKey("receiveUserId")) {
            String receiveUserId = jsonObject.getStr("receiveUserId");
            webSocketUtil.sendToUser(receiveUserId, "/notice", message);
            log.info("发送成功:接收者{},消息:{}", receiveUserId, message);
        } else {
            // 消息广播
            webSocketUtil.sendBroadcast("/topic/all", message);
            log.info("发送成功:全员广播,消息:{}", message);
        }
        log.info("前端发来的消息:{}", message);
    }
}

前端单聊demo:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>简易单聊</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: "Microsoft YaHei", sans-serif;
        }

        body {
            max-width: 900px;
            margin: 20px auto;
            padding: 0 20px;
            background: #f5f7fa;
        }

        /* 顶部栏 */
        .top-bar {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
        }

        .top-bar .title {
            font-size: 22px;
            color: #333;
        }

        .account-box {
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .account-box label {
            font-size: 14px;
            color: #666;
        }

        .account-box select {
            padding: 6px 10px;
            border: 1px solid #dcdfe6;
            border-radius: 4px;
            outline: none;
        }

        /* 消息框 */
        .msg-box {
            height: 480px;
            background: #fff;
            border: 1px solid #e4e7ed;
            border-radius: 8px;
            padding: 15px;
            overflow-y: auto;
            margin-bottom: 15px;
            line-height: 1.6;
        }

        /* 单条消息容器 */
        .msg-item {
            margin-bottom: 14px;
            display: flex;
            flex-direction: column;
            max-width: 75%;
            width: fit-content;
        }
        /* 自己发的靠右 */
        .msg-item.self {
            margin-left: auto;
            align-items: flex-end;
        }
        /* 别人发的靠左 */
        .msg-item.personal {
            margin-right: auto;
            align-items: flex-start;
        }
        /* 昵称 */
        .msg-name {
            font-size: 12px;
            color: #999;
            margin-bottom: 4px;
            padding: 0 4px;
        }
        /* 气泡:自动换行修复 */
        .msg-bubble {
            padding: 8px 12px;
            border-radius: 6px;
            font-size: 14px;
            line-height: 1.5;
            white-space: pre-wrap;
            word-wrap: break-word;
            overflow-wrap: break-word;
            width: 100%;
        }
        /* 自己气泡绿色 */
        .self .msg-bubble {
            background: #95ec69;
        }
        /* 别人气泡灰色 */
        .personal .msg-bubble {
            background: #e5e5e5;
        }
        /* 广播消息 */
        .msg-broadcast {
            background: #f6ffed;
            border-left: 3px solid #52c41a;
            text-align: center;
            padding: 6px 12px;
            margin: 10px auto;
            font-size: 13px;
            color: #333;
        }

        /* 发送栏 */
        .send-bar {
            display: flex;
            gap: 10px;
            align-items: center;
        }

        .send-bar select,
        .send-bar input {
            padding: 12px 14px;
            border: 1px solid #dcdfe6;
            border-radius: 6px;
            outline: none;
            font-size: 14px;
        }

        .send-bar input {
            flex: 2;
        }

        .send-bar select {
            flex: 1;
        }

        .send-bar button {
            padding: 12px 24px;
            background: #1890ff;
            color: #fff;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
        }

        .send-bar button:hover {
            background: #40a9ff;
        }
    </style>
</head>

<body>
    <div class="top-bar">
        <h1 class="title">WebSocket 单聊</h1>
        <div class="account-box">
            <label>当前账号:</label>
            <select id="currentUser">
                <option value="">请选择账号</option>
                <option value="zhangsan">张三</option>
                <option value="lisi">李四</option>
                <option value="wangwu">王五</option>
                <option value="zhaoliu">赵六</option>
            </select>
        </div>
    </div>

    <div id="msgBox" class="msg-box"></div>

    <div class="send-bar">
        <select id="receiveUserId">
            <option value="">选择接收人</option>
            <option value="zhangsan">张三</option>
            <option value="lisi">李四</option>
            <option value="wangwu">王五</option>
            <option value="zhaoliu">赵六</option>
        </select>
        <input id="content" placeholder="请输入消息内容..." autocomplete="off" />
        <button onclick="sendMsg()">发送</button>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>

    <script>
        // 用户ID → 昵称映射
        const userMap = {
            zhangsan: "张三",
            lisi: "李四",
            wangwu: "王五",
            zhaoliu: "赵六"
        };

        let userId = "";
        const socketUrl = "http://10.55.206.140:8094/xzzf-zhdd/websocket";
        let stomp = null;
        let socket = null;

        // 切换账号
        document.getElementById("currentUser").addEventListener("change", function () {
            userId = this.value;
            userId ? connectWebSocket() : disconnectWebSocket();
        });

        // 回车键发送
        document.getElementById("content").addEventListener("keydown", function (e) {
            if (e.key === "Enter") {
                sendMsg();
            }
        });

        // 连接 WebSocket
        function connectWebSocket() {
            if (stomp?.connected) stomp.disconnect();

            socket = new SockJS(socketUrl);
            stomp = Stomp.over(socket);
            stomp.debug = () => {};

            stomp.connect({}, () => {
                console.log("✅ 连接成功:" + userId);
                stomp.subscribe(`/user/${userId}/notice`, (res) => {
                    showMsg(res.body, "personal");
                });
            });
        }

        // 断开连接
        function disconnectWebSocket() {
            if (stomp?.connected) {
                stomp.disconnect();
                stomp = null;
            }
        }

        // 发送消息
        function sendMsg() {
            if (!userId) {
                alert("请先在右上角选择账号!");
                return;
            }
            if (!stomp?.connected) {
                alert("未连接,请重新选择账号!");
                return;
            }

            const receiveUserId = document.getElementById("receiveUserId").value;
            if (!receiveUserId) {
                alert("请选择接收人!");
                return;
            }

            const content = document.getElementById("content").value.trim();
            if (!content) {
                alert("请输入消息内容!");
                return;
            }

            const data = {
                sendUserId: userId,
                receiveUserId: receiveUserId,
                msg: content
            };

            stomp.send("/app/chat", {}, JSON.stringify(data));
            showMsg(JSON.stringify(data), "self");
            document.getElementById("content").value = "";
        }

        // 渲染消息
        function showMsg(text, type) {
            const msgBox = document.getElementById("msgBox");
            let sendId = "";
            let msgText = "";

            try {
                const json = JSON.parse(text);
                sendId = json.sendUserId;
                msgText = json.msg;
            } catch (e) {
                msgText = text;
            }

            const userName = userMap[sendId] || sendId;

            if (type === "broadcast") {
                const div = document.createElement("div");
                div.className = "msg-broadcast";
                div.innerText = `【系统】${msgText}`;
                msgBox.appendChild(div);
                msgBox.scrollTop = msgBox.scrollHeight;
                return;
            }

            const item = document.createElement("div");
            item.className = `msg-item ${type}`;

            const name = document.createElement("div");
            name.className = "msg-name";
            name.innerText = userName;

            const bubble = document.createElement("div");
            bubble.className = "msg-bubble";
            bubble.innerText = msgText;

            item.appendChild(name);
            item.appendChild(bubble);
            msgBox.appendChild(item);

            msgBox.scrollTop = msgBox.scrollHeight;
        }
    </script>
</body>
</html>

前端群聊demo:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>简易群聊</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: "Microsoft YaHei", sans-serif;
        }

        body {
            max-width: 900px;
            margin: 20px auto;
            padding: 0 20px;
            background: #f5f7fa;
        }

        /* 顶部栏 */
        .top-bar {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
        }

        .top-bar .title {
            font-size: 22px;
            color: #333;
        }

        .account-box {
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .account-box label {
            font-size: 14px;
            color: #666;
        }

        .account-box select {
            padding: 6px 10px;
            border: 1px solid #dcdfe6;
            border-radius: 4px;
            outline: none;
        }

        /* 消息框 */
        .msg-box {
            height: 480px;
            background: #fff;
            border: 1px solid #e4e7ed;
            border-radius: 8px;
            padding: 15px;
            overflow-y: auto;
            margin-bottom: 15px;
            line-height: 1.6;
        }

        /* 单条消息容器 */
        .msg-item {
            margin-bottom: 14px;
            display: flex;
            flex-direction: column;
            max-width: 75%; /* 气泡最大宽度 */
            width: fit-content; /* 关键:自适应内容宽度 */
        }
        /* 自己发的靠右 */
        .msg-item.self {
            margin-left: auto;
            align-items: flex-end;
        }
        /* 别人发的靠左 */
        .msg-item.broadcast {
            margin-right: auto;
            align-items: flex-start;
        }
        /* 发送人昵称 */
        .msg-name {
            font-size: 12px;
            color: #999;
            margin-bottom: 4px;
            padding: 0 4px;
        }
        /* 消息气泡:修复自动换行 + 完整显示 */
        .msg-bubble {
            padding: 8px 12px;
            border-radius: 6px;
            font-size: 14px;
            line-height: 1.5;
            white-space: pre-wrap;   /* 保证自动换行 */
            word-wrap: break-word;  /* 长单词/长文本强制换行 */
            overflow-wrap: break-word;
            width: 100%;            /* 占满容器宽度 */
        }
        /* 自己气泡绿色 */
        .self .msg-bubble {
            background: #95ec69;
        }
        /* 别人气泡灰色 */
        .broadcast .msg-bubble {
            background: #e5e5e5;
        }

        /* 发送栏 */
        .send-bar {
            display: flex;
            gap: 10px;
            align-items: center;
        }

        .send-bar input {
            flex: 1;
            padding: 12px 14px;
            border: 1px solid #dcdfe6;
            border-radius: 6px;
            outline: none;
            font-size: 14px;
        }

        .send-bar button {
            padding: 12px 24px;
            background: #1890ff;
            color: #fff;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
        }

        .send-bar button:hover {
            background: #40a9ff;
        }
    </style>
</head>

<body>
    <div class="top-bar">
        <h1 class="title">WebSocket 群聊</h1>
        <div class="account-box">
            <label>当前账号:</label>
            <select id="currentUser">
                <option value="">请选择账号</option>
                <option value="zhangsan">张三</option>
                <option value="lisi">李四</option>
                <option value="wangwu">王五</option>
                <option value="zhaoliu">赵六</option>
            </select>
        </div>
    </div>

    <div id="msgBox" class="msg-box"></div>

    <div class="send-bar">
        <input id="content" placeholder="输入群聊消息..." autocomplete="off" />
        <button onclick="sendMsg()">发送</button>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>

    <script>
        // 用户ID→中文昵称映射
        const userMap = {
            zhangsan: "张三",
            lisi: "李四",
            wangwu: "王五",
            zhaoliu: "赵六"
        }
        // ===================== 配置 =====================
        let userId = "";
        const socketUrl = "http://10.55.206.140:8094/xzzf-zhdd/websocket";
        let stomp = null;
        let socket = null;
        // ==================================================

        // 切换账号
        document.getElementById("currentUser").addEventListener("change", function () {
            userId = this.value;
            userId ? connectWebSocket() : disconnectWebSocket();
        });

        // 回车键发送
        document.getElementById("content").addEventListener("keydown", function (e) {
            if (e.key === "Enter") {
                sendMsg();
            }
        });

        // 连接 WebSocket
        function connectWebSocket() {
            if (stomp?.connected) stomp.disconnect();

            socket = new SockJS(socketUrl);
            stomp = Stomp.over(socket);
            stomp.debug = () => {};

            stomp.connect({}, () => {
                console.log("✅ 已连接:" + userId);
                stomp.subscribe("/topic/all", (res) => {
                    try {
                        const json = JSON.parse(res.body);
                        if (json.sendUserId === userId) return;
                    } catch (e) {}
                    showMsg(res.body, "broadcast");
                });
            });
        }

        // 断开连接
        function disconnectWebSocket() {
            if (stomp?.connected) {
                stomp.disconnect();
                stomp = null;
            }
        }

        // 发送消息
        function sendMsg() {
            if (!userId) {
                alert("请先在右上角选择账号!");
                return;
            }
            if (!stomp?.connected) {
                alert("未连接,请重新选择账号!");
                return;
            }

            const content = document.getElementById("content").value.trim();
            if (!content) {
                alert("请输入消息");
                return;
            }

            const data = { sendUserId: userId, msg: content };
            stomp.send("/app/chat", {}, JSON.stringify(data));
            showMsg(JSON.stringify(data), "self");
            document.getElementById("content").value = "";
        }

        // 渲染消息
        function showMsg(text, type) {
            const msgBox = document.getElementById("msgBox");
            let sendId = "";
            let msgText = "";
            try {
                const json = JSON.parse(text);
                sendId = json.sendUserId;
                msgText = json.msg;
            } catch (e) {
                msgText = text;
            }
            const userName = userMap[sendId] || sendId;

            const itemDiv = document.createElement("div");
            itemDiv.className = `msg-item ${type}`;
            
            const nameSpan = document.createElement("div");
            nameSpan.className = "msg-name";
            nameSpan.innerText = userName;
            
            const bubbleDiv = document.createElement("div");
            bubbleDiv.className = "msg-bubble";
            bubbleDiv.innerText = msgText;

            itemDiv.appendChild(nameSpan);
            itemDiv.appendChild(bubbleDiv);
            msgBox.appendChild(itemDiv);
            msgBox.scrollTop = msgBox.scrollHeight;
        }
    </script>
</body>
</html>
相关推荐
海绵宝宝de派小星1 小时前
MCP与A2A协议深度解析:Agent时代的“TCP/IP“如何诞生
arm开发·网络协议·tcp/ip·ai
一只小鱼儿吖1 小时前
代理IP与内网穿透:网络世界的“隐形斗篷”与“任意门”
网络
天行健,君子而铎1 小时前
自适应、全链路与智能识别——政务数据安全泛监测系统
java·网络·政务
搞科研的小刘选手1 小时前
【西安交通大学主办】第六届人工智能、自动化与高性能计算国际会议 (AIAHPC 2026)
网络·人工智能·机器学习·数据挖掘·自动化·云计算·并行式
XiaoLin laile2 小时前
数据合规越查越严,企业内网通讯软件成安全刚需
网络
酉鬼女又兒2 小时前
零基础入门计算机网络可靠传输:从基本概念到三大实现机制(停止 - 等待 / 回退 N 帧 / 选择重传)全解析
网络·网络协议·计算机网络·考研·职场和发展·计算机外设·求职招聘
luweis2 小时前
企智孪生 ETA (6.5 人机协同:定义“协作界面 (Collaboration UI)”)【杭州联保致新科技有限公司 卢伟舜】
网络·人工智能·科技·程序人生·创业创新·学习方法
专注VB编程开发20年2 小时前
上位机监控接收数据(从站)-Modbus TCP 从机(Slave)模式多站点设计
网络·网络协议·tcp/ip
上海云盾-小余2 小时前
游戏端口隐蔽防护:端口映射 + 高防集群拦截爆破实操指南
网络·安全·web安全·游戏