一.后端:
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>