一、环境准备:
添加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方式
效果展示: