springboot 实现websocket通信

一、环境准备:

添加maven依赖

复制代码
   <!-- websocket包 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
		
			<!-- 通用工具 -->
		<dependency>
			<groupId>cn.hutool</groupId>
			<artifactId>hutool-all</artifactId>
			<version>5.8.25</version>
		</dependency>

二、前端代码

css 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DeepSeek WebSocket客户端</title>
    <style>
        :root {
            --primary: #4b6cb7;
            --secondary: #182848;
            --accent: #36D1DC;
            --light: #f8f9fa;
            --dark: #343a40;
            --success: #28a745;
            --danger: #dc3545;
            --warning: #ffc107;
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }

        body {
            background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
            color: #333;
            min-height: 100vh;
            padding: 20px;
            display: flex;
            flex-direction: column;
            align-items: center;
        }

        .container {
            width: 100%;
            max-width: 1000px;
            background: rgba(255, 255, 255, 0.95);
            border-radius: 15px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
            overflow: hidden;
            margin: 20px 0;
            display: flex;
            flex-direction: column;
        }

        header {
            background: linear-gradient(90deg, var(--primary), var(--secondary));
            color: white;
            padding: 20px;
            text-align: center;
        }

        h1 {
            font-size: 2.2rem;
            margin-bottom: 10px;
        }

        .description {
            font-size: 1.1rem;
            opacity: 0.9;
            margin-bottom: 15px;
        }

        .api-info {
            background: rgba(0, 0, 0, 0.2);
            padding: 10px;
            border-radius: 8px;
            font-family: monospace;
            word-break: break-all;
            margin-top: 10px;
            font-size: 0.9rem;
        }

        .main-content {
            display: flex;
            flex-direction: row;
            height: 500px;
        }

        @media (max-width: 768px) {
            .main-content {
                flex-direction: column;
                height: auto;
            }
        }

        .chat-container {
            flex: 3;
            display: flex;
            flex-direction: column;
            padding: 20px;
            border-right: 1px solid #eee;
        }

        .chat-messages {
            flex: 1;
            overflow-y: auto;
            padding: 15px;
            background: #f9f9f9;
            border-radius: 10px;
            margin-bottom: 15px;
        }

        .message {
            margin-bottom: 15px;
            padding: 12px;
            border-radius: 10px;
            max-width: 80%;
            line-height: 1.5;
        }

        .user-message {
            background: #e3f2fd;
            margin-left: auto;
            border-bottom-right-radius: 3px;
        }

        .ai-message {
            background: #f5f5f5;
            margin-right: auto;
            border-bottom-left-radius: 3px;
        }

        .streaming-message {
            background: #e8f5e9;
            border-left: 4px solid var(--success);
        }

        .input-area {
            display: flex;
            gap: 10px;
        }

        input {
            flex: 1;
            padding: 12px 15px;
            border: 1px solid #ddd;
            border-radius: 25px;
            font-size: 1rem;
            outline: none;
            transition: border-color 0.3s;
        }

        input:focus {
            border-color: var(--primary);
        }

        button {
            background: var(--primary);
            color: white;
            border: none;
            border-radius: 25px;
            padding: 12px 25px;
            cursor: pointer;
            font-size: 1rem;
            font-weight: 600;
            transition: background 0.3s;
        }

        button:hover {
            background: var(--secondary);
        }

        button:disabled {
            background: #cccccc;
            cursor: not-allowed;
        }

        .sidebar {
            flex: 1;
            padding: 20px;
            background: #f5f5f5;
            display: flex;
            flex-direction: column;
        }

        .connection-panel {
            margin-bottom: 20px;
        }

        .control-group {
            margin-bottom: 15px;
        }

        label {
            display: block;
            font-weight: 600;
            margin-bottom: 5px;
            font-size: 0.9rem;
        }

        select, input[type="text"] {
            width: 100%;
            padding: 10px;
            border-radius: 6px;
            border: 1px solid #ddd;
        }

        .status-panel {
            margin-top: auto;
            padding: 15px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        }

        .status-item {
            display: flex;
            justify-content: space-between;
            margin-bottom: 10px;
        }

        .status-value {
            font-weight: 600;
        }

        .connected {
            color: var(--success);
        }

        .disconnected {
            color: var(--danger);
        }

        .connecting {
            color: var(--warning);
        }

        .code {
            font-family: 'Courier New', monospace;
            background: #2d2d2d;
            color: #f8f8f2;
            padding: 10px;
            border-radius: 5px;
            overflow-x: auto;
            margin: 10px 0;
            font-size: 0.9rem;
        }

        .message-time {
            font-size: 0.7rem;
            color: #777;
            text-align: right;
            margin-top: 5px;
        }

        .btn-secondary {
            background: #5a6268;
            padding: 8px 15px;
            font-size: 0.9rem;
        }

        .btn-secondary:hover {
            background: #4e555b;
        }

        .btn-danger {
            background: var(--danger);
        }

        .btn-danger:hover {
            background: #bd2130;
        }
    </style>
</head>
<body>
<div class="container">
    <header>
        <h1>DeepSeek WebSocket客户端</h1>
        <p class="description">通过WebSocket实现与DeepSeek API的流式通信</p>
        <div class="api-info">
            模型: /deepseek-ai/DeepSeek-R1-Distill-Qwen-32B | 传输: WebSocket
        </div>
    </header>

    <div class="main-content">
        <div class="chat-container">
            <div class="chat-messages" id="chatMessages">
                <div class="message ai-message">
                    <p>欢迎使用DeepSeek WebSocket客户端!请先连接到服务器,然后开始对话。</p>
                    <div class="message-time" id="messageTime">系统消息</div>
                </div>
            </div>

            <div class="input-area">
                <input type="text" id="userInput" placeholder="输入您的问题..." autocomplete="off" disabled>
                <button id="sendButton" disabled>发送</button>
            </div>
        </div>

        <div class="sidebar">
            <div class="connection-panel">
                <h3>连接设置</h3>

                <div class="control-group">
                    <label for="serverUrl">WebSocket服务器</label>
                    <input type="text" id="serverUrl" value="wss://127.0.0.1:90/imServer/zwj">
                </div>

                <div class="control-group">
                    <button id="connectButton">连接</button>
                    <button id="disconnectButton" class="btn-danger" disabled>断开</button>
                </div>
            </div>

            <div class="settings-panel">
                <h3>设置</h3>

                <div class="control-group">
                    <label for="streamOption">响应类型</label>
                    <select id="streamOption">
                        <option value="stream">流式响应 (实时显示)</option>
                        <option value="complete">完整响应</option>
                    </select>
                </div>

                <div class="control-group">
                    <button id="clearChat" class="btn-secondary">清除对话</button>
                </div>
            </div>

            <div class="status-panel">
                <h3>连接状态</h3>

                <div class="status-item">
                    <span>状态:</span>
                    <span class="status-value" id="connectionStatus">未连接</span>
                </div>

                <div class="status-item">
                    <span>消息数:</span>
                    <span class="status-value" id="messageCount">0</span>
                </div>

                <div class="status-item">
                    <span>最后活动:</span>
                    <span class="status-value" id="lastActivity">-</span>
                </div>
            </div>
        </div>
    </div>
</div>

<script>
    document.addEventListener('DOMContentLoaded', function() {
        // DOM元素
        const chatMessages = document.getElementById('chatMessages');
        const userInput = document.getElementById('userInput');
        const sendButton = document.getElementById('sendButton');
        const connectButton = document.getElementById('connectButton');
        const disconnectButton = document.getElementById('disconnectButton');
        const clearChatButton = document.getElementById('clearChat');
        const connectionStatus = document.getElementById('connectionStatus');
        const messageCount = document.getElementById('messageCount');
        const lastActivity = document.getElementById('lastActivity');
        const serverUrl = document.getElementById('serverUrl');

        // 状态变量
        let ws = null;
        let isConnected = false;
        let msgCount = 0;
        let currentAiMessageElement = null;

        // 格式化时间
        function formatTime(date = new Date()) {
            return date.toLocaleTimeString('zh-CN', {
                hour: '2-digit',
                minute: '2-digit',
                second: '2-digit'
            });
        }

        // 更新状态显示
        function updateStatus(status, className) {
            connectionStatus.textContent = status;
            connectionStatus.className = 'status-value ' + className;
            lastActivity.textContent = formatTime();
        }

        // 添加消息到聊天窗口
        function addMessage(content, isUser = false, isStreaming = false) {
            const messageElement = document.createElement('div');
            messageElement.classList.add('message');
            messageElement.classList.add(isUser ? 'user-message' : 'ai-message');

            if (isStreaming) {
                messageElement.classList.add('streaming-message');
                currentAiMessageElement = messageElement;
            }

            messageElement.innerHTML = `
                    <p>${content}</p>
                    <div class="message-time">${isUser ? '您' : 'DeepSeek'} · ${formatTime()}</div>
                `;

            chatMessages.appendChild(messageElement);
            chatMessages.scrollTop = chatMessages.scrollHeight;

            // 更新消息计数
            msgCount++;
            messageCount.textContent = msgCount;

            return messageElement;
        }

        // 更新消息内容
        function updateMessage(element, newContent) {
            element.querySelector('p').textContent = newContent;
            element.querySelector('.message-time').textContent = `DeepSeek · ${formatTime()} (更新中)`;
            chatMessages.scrollTop = chatMessages.scrollHeight;
        }

        // 连接到WebSocket服务器
        function connect() {
            if (isConnected) return;

            try {
                updateStatus('连接中...', 'connecting');

                // 创建WebSocket连接
                ws = new WebSocket(serverUrl.value);

                ws.onopen = function() {
                    isConnected = true;
                    updateStatus('已连接', 'connected');
                    userInput.disabled = false;
                    sendButton.disabled = false;
                    connectButton.disabled = true;
                    disconnectButton.disabled = false;

                    addMessage('已成功连接到DeepSeek服务器', false);
                };

                ws.onmessage = function(event) {
                    debugger
                    const data = JSON.parse(event.data);

                    if (data.type === 'response') {
                        if (data.is_complete) {
                            // 响应完成
                            if (currentAiMessageElement) {
                                currentAiMessageElement.classList.remove('streaming-message');
                                currentAiMessageElement = null;
                            }
                        } else if (currentAiMessageElement) {
                            // 更新流式响应
                            updateMessage(currentAiMessageElement, data.content);
                        } else {
                            // 新响应
                            addMessage(data.content, false, true);
                        }
                    } else if (data.type === 'error') {
                        addMessage(`错误: ${data.message}`, false);
                    } else if (data.type === 'info') {
                        addMessage(`系统: ${data.message}`, false);
                    }
                };

                ws.onclose = function() {
                    isConnected = false;
                    updateStatus('已断开', 'disconnected');
                    userInput.disabled = true;
                    sendButton.disabled = true;
                    connectButton.disabled = false;
                    disconnectButton.disabled = true;

                    addMessage('与服务器的连接已断开', false);
                };

                ws.onerror = function(error) {
                    addMessage(`连接错误: ${error}`, false);
                    updateStatus('连接错误', 'disconnected');
                };
            } catch (error) {
                addMessage(`连接异常: ${error}`, false);
                updateStatus('连接异常', 'disconnected');
            }
        }

        // 断开WebSocket连接
        function disconnect() {
            if (ws) {
                ws.close();
                ws = null;
            }
        }

        // 发送消息
        function sendMessage() {
            if (!isConnected || !ws) {
                addMessage('错误: 未连接到服务器', false);
                return;
            }

            const message = userInput.value.trim();
            if (!message) return;

            // 添加用户消息到聊天
            addMessage(message, true);

            // 清空输入框
            userInput.value = '';

            // 准备AI消息元素用于流式响应
            if (document.getElementById('streamOption').value === 'stream') {
                addMessage('思考中...', false, true);
            }

            // 通过WebSocket发送消息
            try {
                ws.send(JSON.stringify({
                    type: 'query',
                    content: message,
                    stream: document.getElementById('streamOption').value === 'stream'
                }));
            } catch (error) {
                addMessage(`发送错误: ${error}`, false);
            }
        }

        // 清除聊天记录
        function clearChat() {
            chatMessages.innerHTML = '';
            msgCount = 0;
            messageCount.textContent = '0';
            addMessage('聊天记录已清除', false);
        }

        // 事件监听器
        connectButton.addEventListener('click', connect);
        disconnectButton.addEventListener('click', disconnect);
        sendButton.addEventListener('click', sendMessage);
        clearChatButton.addEventListener('click', clearChat);

        userInput.addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                sendMessage();
            }
        });

        // 初始化
        addMessage('WebSocket客户端已就绪,请点击"连接"按钮开始对话。', false);
    });
</script>
</body>
</html>

三、后端代码

java 复制代码
 
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.alibaba.druid.support.json.JSONUtils;
import com.alibaba.fastjson.JSONObject;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@ServerEndpoint(value = "/imServer/{username}")
@Component
public class WebSocketServer {
    private  static final Logger logs = LoggerFactory.getLogger(WebSocketServer.class);

    public static  final Map<String, Session> sessionMap = new ConcurrentHashMap<>();


    /**
     * 创建链接
     * @param session
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("username") String username){
        sessionMap.put(username,session);
        logs.info("新用户加入username:{},当前人数:{}",username,sessionMap.size());
        JSONObject result = new JSONObject();
        JSONArray array = new JSONArray();
        result.put("users",array);
        for(Object key :sessionMap.keySet()){
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("username",key);
            array.add(jsonObject);
        }
        sedAllMessage(JSONUtils.toJSONString(result));//把当前用户列表返给客户端

    }

    /**
     * 关闭连接
     * @param session
     * @param username 指定关闭哪一个session连接
     */
    @OnClose
    public void onClose(Session session, @PathParam("username") String username){
        sessionMap.remove(username);
        logs.info(username+"连接关闭成功");
    }

    /**
     * 消息中转站
     * 接收到客户端消息后调用此方法
     * @param message 客户端发来的消息
     * @param username 用户名
     */
    @OnMessage
    public void onMessage(String message, @PathParam("username") String username){
        logs.info("服务端收到用户{}的消息:{}",username,message);
        cn.hutool.json.JSONObject parse = JSONUtil.parseObj(message) ;
        String toUserName = parse.getStr("to"); // 要发给谁
        String text = parse.getStr("content");  // 发什么信息
        Session toSession = sessionMap.get(username); // 找到对应用户的Session
        if(toSession != null){
            JSONObject json = new JSONObject();
            json.put("from",username);
            json.put("type","response");
            json.put("content","回答:"+text);
            this.sendMessage(toSession,json.toJSONString());
            logs.info("发送到用户:{},消息:{}",toUserName,json.toJSONString());
        }else {
            logs.info("发送失败,未找到用户:{}的session连接",toUserName);
        }

    }

    @OnError
    public void onError(Throwable error){
        logs.error("webSocket 服务异常"+error.getMessage());
        error.printStackTrace();
    }

    /**
     * 把消息发给所有客户端
     * @param message
     */
    private void sedAllMessage(String message){
        try {
            for(Session session:sessionMap.values()){
                logs.info("服务端给客户端[{}]发送信息{}",session.getId(),message);
                session.getBasicRemote().sendText(message);
            }
        } catch (Exception e) {
            logs.error("服务端发送消息到客户端失败",e);
        }
    }

    /**
     * 向指定用户的session连接中发送消息
     * @param session 指定用户的session
     * @param message 消息文本
     */
    private void sendMessage(Session session,String message){
        try {
            session.getBasicRemote().sendText(message);//向客户端发送信息
        } catch (Exception e) {
            logs.error("服务端发送消息到客户端失败",e);
        }
    }

}

四、如果有shrio权限,设置可访问权限

java 复制代码
 filterChainDefinitionMap.put("/imServer/**", "anon,captchaValidate");

五、执行连接开始对话

注意:wss:服务器是https,ws:服务器是http方式

效果展示:

相关推荐
Kiri霧4 小时前
在actix-web中创建一个提取器
后端·rust·web
^_^ 纵歌4 小时前
rust主要用于哪些领域
开发语言·后端·rust
thginWalker4 小时前
使用Spring Boot构建系统安全层
spring boot
JaguarJack4 小时前
现代 PHP8+ 实战特性介绍 Enums、Fibers 和 Attributes
后端·php
绝无仅有4 小时前
面试真实经历某商银行大厂Java问题和答案总结(四)
后端·面试·github
绝无仅有4 小时前
面试真实经历某商银行大厂Java问题和答案总结(六)
后端·面试·github
IT_陈寒4 小时前
Vue3性能优化实战:这7个技巧让我的应用提速50%,尤雨溪都点赞!
前端·人工智能·后端
yuniko-n4 小时前
【力扣 SQL 50】连接
数据库·后端·sql·算法·leetcode
白萤4 小时前
SpringBoot用户登录注册系统设计与实现
java·spring boot·后端