一、前言
最近在学习WebSocket技术,做了一个简单的聊天室Demo。这个项目虽然不大,但涵盖了WebSocket的核心功能实现。下面我将详细介绍这个聊天室的实现过程,希望能帮助到同样想学习WebSocket的朋友们。
二、技术选型
后端:Spring Boot + WebSocket
前端:SockJS + STOMP.js
通信协议:STOMP(简单的面向文本的消息协议)
三、后端实现
1. WebSocket配置类
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 设置消息代理前缀
config.enableSimpleBroker("/topic");
// 设置应用目的地前缀
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册WebSocket端点
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS(); // 支持SockJS
}
}
2. 消息处理控制器
@Controller
public class WebSocketController {
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public String greeting(String message) {
System.out.println("收到消息: " + message);
return message;
}
}
四、前端实现
完整代码:
<!DOCTYPE html>
<html>
<meta charset="UTF-8">
<head>
<title>WebSocket Chat Demo</title>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<style>
:root {
--primary-color: #4CAF50;
--secondary-color: #45a049;
--error-color: #f44336;
--border-color: #ddd;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 20px;
}
#chat-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
#message-container {
height: 400px;
overflow-y: auto;
padding: 15px;
border-bottom: 1px solid var(--border-color);
background-color: #fafafa;
}
.message {
margin-bottom: 12px;
padding: 10px 15px;
border-radius: 18px;
max-width: 70%;
word-wrap: break-word;
animation: fadeIn 0.3s ease;
}
.user-message {
background-color: var(--primary-color);
color: white;
margin-left: auto;
border-bottom-right-radius: 4px;
}
.server-message {
background-color: #e5e5ea;
color: black;
margin-right: auto;
border-bottom-left-radius: 4px;
}
#status {
padding: 10px 15px;
background-color: #f8f8f8;
font-size: 14px;
color: #666;
text-align: center;
border-bottom: 1px solid var(--border-color);
}
#message-form {
display: flex;
padding: 15px;
background-color: white;
}
#message-input {
flex-grow: 1;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 16px;
outline: none;
transition: border 0.3s;
}
#message-input:focus {
border-color: var(--primary-color);
}
#send-btn {
padding: 12px 20px;
margin-left: 10px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
#send-btn:hover {
background-color: var(--secondary-color);
}
#send-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
.connection-status {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 5px;
}
.connected {
background-color: var(--primary-color);
}
.disconnected {
background-color: var(--error-color);
}
.timestamp {
font-size: 12px;
color: #999;
margin-top: 4px;
text-align: right;
}
</style>
</head>
<body>
<h1>WebSocket Chat Demo</h1>
<div id="chat-container">
<div id="status">
<span id="connection-status" class="connection-status disconnected"></span>
<span id="status-text">正在连接服务器...</span>
</div>
<div id="message-container"></div>
<form id="message-form">
<input type="text" id="message-input" placeholder="输入消息..." autocomplete="off" autofocus>
<button type="submit" id="send-btn" disabled>发送</button>
</form>
</div>
<script>
// 全局变量
const socket = new SockJS('http://localhost:8080/ws');
const stompClient = Stomp.over(socket);
let username = '用户' + Math.floor(Math.random() * 1000);
// DOM元素
const messageContainer = document.getElementById('message-container');
const messageForm = document.getElementById('message-form');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const statusText = document.getElementById('status-text');
const connectionStatus = document.getElementById('connection-status');
// 初始化连接
function connect() {
updateStatus('正在连接服务器...', 'disconnected');
// 禁用调试信息(生产环境)
stompClient.debug = null;
stompClient.connect({},
function(frame) {
onConnectSuccess(frame);
},
function(error) {
onConnectError(error);
}
);
}
// 连接成功回调
function onConnectSuccess(frame) {
updateStatus('已连接', 'connected');
sendBtn.disabled = false;
// 订阅消息
stompClient.subscribe('/topic/greetings', function(response) {
const message = JSON.parse(response.body);
// 检查是否是自己的消息
if (message.username !== username) {
showMessage(`${message.username}: ${message.content}`, 'server-message');
}
});
// 发送欢迎消息
sendSystemMessage(`${username} 加入了聊天室`);
}
// 连接错误回调
function onConnectError(error) {
console.error('连接错误:', error);
updateStatus('连接失败,5秒后重试...', 'disconnected');
sendBtn.disabled = true;
// 5秒后自动重连
setTimeout(connect, 5000);
}
// 更新状态显示
function updateStatus(text, status) {
statusText.textContent = text;
connectionStatus.className = 'connection-status ' + status;
}
// 显示消息
function showMessage(content, messageType) {
const messageElement = document.createElement('div');
messageElement.className = `message ${messageType}`;
const contentElement = document.createElement('div');
contentElement.textContent = content;
const timestampElement = document.createElement('div');
timestampElement.className = 'timestamp';
timestampElement.textContent = new Date().toLocaleTimeString();
messageElement.appendChild(contentElement);
messageElement.appendChild(timestampElement);
messageContainer.appendChild(messageElement);
messageContainer.scrollTop = messageContainer.scrollHeight;
}
// 发送用户消息
function sendUserMessage() {
const content = messageInput.value.trim();
if (!content) return;
// 显示用户消息
showMessage(content, 'user-message');
// 发送到服务器
stompClient.send(
"/app/hello",
{},
JSON.stringify({
username: username,
content: content,
timestamp: new Date().getTime()
})
);
messageInput.value = '';
}
// 发送系统消息
function sendSystemMessage(message) {
showMessage(message, 'server-message');
}
// 事件监听
messageForm.addEventListener('submit', function(e) {
e.preventDefault();
sendUserMessage();
});
// 回车发送消息
messageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendUserMessage();
}
});
// 初始化
connect();
// 窗口关闭前发送离开消息
window.addEventListener('beforeunload', function() {
if (stompClient.connected) {
sendSystemMessage(`${username} 离开了聊天室`);
}
});
</script>
</body>
</html>
JavaScript核心代码
// 初始化连接
const socket = new SockJS('http://localhost:8080/ws');
const stompClient = Stomp.over(socket);
// 连接成功回调
function onConnectSuccess(frame) {
updateStatus('已连接', 'connected');
sendBtn.disabled = false;
// 订阅消息
stompClient.subscribe('/topic/greetings', function(response) {
const message = JSON.parse(response.body);
// 检查是否是自己的消息
if (message.username !== username) {
showMessage(`${message.username}: ${message.content}`, 'server-message');
}
});
}
// 发送消息
function sendUserMessage() {
const content = messageInput.value.trim();
if (!content) return;
// 显示用户消息
showMessage(content, 'user-message');
// 发送到服务器
stompClient.send(
"/app/hello",
{},
JSON.stringify({
username: username,
content: content,
timestamp: new Date().getTime()
})
);
}
五、遇到的问题及解决方案
问题:消息重复显示
现象:自己发送的消息会显示两次
原因:
前端发送后立即显示
服务器广播后又显示一次
解决方案:
stompClient.subscribe('/topic/greetings', function(response) {
const message = JSON.parse(response.body);
// 只显示别人发的消息
if (message.username !== username) {
showMessage(`${message.username}: ${message.content}`, 'server-message');
}
});
六、项目特点
简单易用:代码简洁,适合初学者理解WebSocket原理
实时通信:基于WebSocket实现真正的双向通信
自动重连:网络断开后会自动尝试重新连接
用户区分:每个用户有随机生成的唯一用户名
七、总结
这个简单的WebSocket聊天室Demo虽然功能不多,但涵盖了WebSocket的核心功能。通过这个项目,我学到了:
WebSocket的基本工作原理
STOMP协议的使用方法
前后端如何通过WebSocket进行实时通信
如何处理常见的消息回显问题
如果想扩展功能,可以考虑添加:
用户列表显示
私聊功能
消息历史记录
图片/文件传输
未来计划:
Ai 聊天室 --- 未来可期