《SpringBoot+WebSocket:在线聊天室从0到1》
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
我是小坏,今天咱们聊点有意思的。你做过聊天功能吗?是不是还在用轮询,隔几秒就问一次"有新消息吗"?太费资源了!今天教你用WebSocket,实现真正的实时通信。
一、HTTP轮询的痛
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
看看传统的做法:
// 前端:每5秒问一次
setInterval(() => {
fetch('/api/messages')
.then(res => res.json())
.then(messages => {
// 处理消息
})
}, 5000);
问题:
- 浪费带宽:大部分请求都是空跑
- 延迟高:最坏情况要等5秒
- 服务器压力大:1万用户就是1万次/5秒的请求
WebSocket的优势:
- 一次连接,双向通信
- 真正的实时:消息立即到达
- 节省资源:没有重复的HTTP头部
二、5分钟搭建聊天服务
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
2.1 加个依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.2 写个配置
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private ChatHandler chatHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatHandler, "/chat")
.setAllowedOrigins("*"); // 允许跨域
}
}
2.3 写个处理器
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
@Component
@Slf4j
public class ChatHandler extends TextWebSocketHandler {
// 保存所有连接
private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String userId = getUserId(session);
sessions.put(userId, session);
log.info("用户 {} 连接成功", userId);
// 通知所有人:用户上线
broadcast(userId + " 上线了");
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String msg = message.getPayload();
String userId = getUserId(session);
log.info("收到消息:{},来自:{}", msg, userId);
// 处理消息
handleMessage(userId, msg);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String userId = getUserId(session);
sessions.remove(userId);
log.info("用户 {} 断开连接", userId);
// 通知所有人:用户下线
broadcast(userId + " 下线了");
}
}
2.4 前端连接
<!DOCTYPE html>
<html>
<body>
<input id="message" placeholder="输入消息">
<button onclick="send()">发送</button>
<div id="chat"></div>
<script>
const ws = new WebSocket('ws://localhost:8080/chat?userId=123');
ws.onopen = () => {
console.log('连接成功');
};
ws.onmessage = (event) => {
const msg = event.data;
document.getElementById('chat').innerHTML += msg + '<br>';
};
function send() {
const msg = document.getElementById('message').value;
ws.send(msg);
}
</script>
</body>
</html>
三、核心功能实现
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
3.1 单聊(一对一)
@Service
public class ChatService {
// 发送私聊消息
public void sendPrivate(String fromUserId, String toUserId, String message) {
WebSocketSession toSession = sessions.get(toUserId);
if (toSession != null && toSession.isOpen()) {
// 对方在线,直接发送
toSession.sendMessage(new TextMessage(formatPrivateMsg(fromUserId, message)));
} else {
// 对方不在线,存到数据库
saveOfflineMessage(fromUserId, toUserId, message);
}
}
// 格式:私聊消息
private String formatPrivateMsg(String fromUserId, String message) {
return JSON.toJSONString(Map.of(
"type", "private",
"from", fromUserId,
"time", new Date(),
"content", message
));
}
}
3.2 群聊(聊天室)
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
@Service
public class GroupChatService {
// 群成员映射:群ID -> 用户ID列表
private final Map<String, Set<String>> groups = new ConcurrentHashMap<>();
// 加入群聊
public void joinGroup(String groupId, String userId) {
groups.computeIfAbsent(groupId, k -> new HashSet<>())
.add(userId);
// 通知群里其他人
sendToGroup(groupId, userId + " 加入了群聊");
}
// 发送群消息
public void sendToGroup(String groupId, String fromUserId, String message) {
Set<String> members = groups.get(groupId);
if (members == null) return;
String formattedMsg = JSON.toJSONString(Map.of(
"type", "group",
"groupId", groupId,
"from", fromUserId,
"time", new Date(),
"content", message
));
// 发给群里所有人(除了自己)
for (String memberId : members) {
if (!memberId.equals(fromUserId)) {
WebSocketSession session = sessions.get(memberId);
if (session != null && session.isOpen()) {
session.sendMessage(new TextMessage(formattedMsg));
}
}
}
}
}
3.3 广播(系统消息)
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
@Service
public class BroadcastService {
// 广播给所有人
public void broadcast(String message) {
String formattedMsg = JSON.toJSONString(Map.of(
"type", "broadcast",
"time", new Date(),
"content", message
));
for (WebSocketSession session : sessions.values()) {
if (session.isOpen()) {
session.sendMessage(new TextMessage(formattedMsg));
}
}
}
// 广播给特定角色
public void broadcastToRole(String role, String message) {
// 先查询有该角色的用户
List<String> userIds = userService.findByRole(role);
for (String userId : userIds) {
WebSocketSession session = sessions.get(userId);
if (session != null && session.isOpen()) {
session.sendMessage(new TextMessage(message));
}
}
}
}
四、进阶功能
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
4.1 心跳检测(防假死)
@Component
@Slf4j
public class HeartbeatHandler extends TextWebSocketHandler {
// 心跳超时时间:30秒
private static final long HEARTBEAT_TIMEOUT = 30000;
// 用户最后心跳时间
private final Map<String, Long> lastHeartbeat = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
// 定时检查心跳
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(this::checkHeartbeat, 30, 30, TimeUnit.SECONDS);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String msg = message.getPayload();
if ("ping".equals(msg)) {
// 收到心跳
String userId = getUserId(session);
lastHeartbeat.put(userId, System.currentTimeMillis());
// 回复pong
session.sendMessage(new TextMessage("pong"));
} else {
// 其他消息
super.handleTextMessage(session, message);
}
}
// 检查心跳
private void checkHeartbeat() {
long now = System.currentTimeMillis();
for (Map.Entry<String, Long> entry : lastHeartbeat.entrySet()) {
if (now - entry.getValue() > HEARTBEAT_TIMEOUT) {
String userId = entry.getKey();
WebSocketSession session = sessions.get(userId);
if (session != null) {
try {
session.close();
log.warn("用户 {} 心跳超时,断开连接", userId);
} catch (IOException e) {
log.error("断开连接失败", e);
}
}
}
}
}
}
4.2 断线重连
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
// 前端实现
let ws = null;
let reconnectTimer = null;
function connect() {
ws = new WebSocket('ws://localhost:8080/chat');
ws.onopen = () => {
console.log('连接成功');
clearTimeout(reconnectTimer);
};
ws.onclose = () => {
console.log('连接断开,5秒后重连');
reconnectTimer = setTimeout(connect, 5000);
};
ws.onerror = (error) => {
console.error('连接错误', error);
ws.close();
};
}
// 开始连接
connect();
4.3 消息历史
@Service
public class MessageHistoryService {
// 保存聊天记录
public void saveMessage(String fromUserId, String toUserId,
String type, String content) {
ChatMessage message = new ChatMessage();
message.setFromUserId(fromUserId);
message.setToUserId(toUserId);
message.setType(type); // private, group, broadcast
message.setContent(content);
message.setCreateTime(new Date());
chatMessageRepository.save(message);
}
// 获取历史消息
public List<ChatMessage> getHistory(String userId, String targetId,
String type, int limit) {
Pageable pageable = PageRequest.of(0, limit,
Sort.by(Sort.Direction.DESC, "createTime"));
if ("private".equals(type)) {
return chatMessageRepository.findPrivateHistory(
userId, targetId, pageable);
} else if ("group".equals(type)) {
return chatMessageRepository.findGroupHistory(
targetId, pageable);
}
return Collections.emptyList();
}
}
五、性能优化
5.1 连接数限制
@Component
public class ConnectionLimitHandler extends TextWebSocketHandler {
// 最大连接数
private static final int MAX_CONNECTIONS = 10000;
@Override
public void afterConnectionEstablished(WebSocketSession session) {
if (sessions.size() >= MAX_CONNECTIONS) {
session.close(CloseStatus.POLICY_VIOLATION);
return;
}
super.afterConnectionEstablished(session);
}
}
5.2 消息压缩
// 配置消息大小限制
@Bean
public WebSocketServletWebHandlerCustomizer webSocketHandlerCustomizer() {
return handler -> {
handler.setMessageSizeLimit(128 * 1024); // 128KB
handler.setSendBufferSizeLimit(512 * 1024); // 512KB
};
}
// 发送时压缩
public void sendCompressedMessage(WebSocketSession session, String message) {
if (message.length() > 1024) { // 大于1KB才压缩
byte[] compressed = compress(message);
session.sendMessage(new BinaryMessage(compressed));
} else {
session.sendMessage(new TextMessage(message));
}
}
5.3 集群部署
@Configuration
public class RedisWebSocketConfig {
@Bean
public SimpMessageSendingOperations messagingTemplate() {
// 使用Redis广播消息
return new SimpMessagingTemplate(brokerChannel);
}
@Bean
public MessageChannel brokerChannel() {
return new MessageChannel() {
// 集成Redis Pub/Sub
};
}
}
六、实战:在线聊天室完整实现
6.1 消息格式定义
@Data
public class ChatMessage {
private String type; // 类型:connect, chat, image, file, system
private String from; // 发送者
private String to; // 接收者(用户ID或群ID)
private String content; // 内容
private Long time; // 时间戳
private Map<String, Object> extra; // 扩展字段
}
6.2 完整处理器
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
@Component
@Slf4j
public class ChatServerHandler extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String userId = getUserId(session);
// 保存连接
sessions.put(userId, session);
// 发送连接成功消息
sendSystemMessage(session, "连接成功");
// 发送未读消息
sendUnreadMessages(userId);
// 通知好友上线
notifyFriendsOnline(userId);
}
@Override
protected void handleTextMessage(WebSocketSession session,
TextMessage textMessage) {
try {
ChatMessage message = JSON.parseObject(
textMessage.getPayload(),
ChatMessage.class
);
// 根据消息类型处理
switch (message.getType()) {
case "chat":
handleChatMessage(session, message);
break;
case "image":
handleImageMessage(session, message);
break;
case "file":
handleFileMessage(session, message);
break;
case "heartbeat":
handleHeartbeat(session, message);
break;
default:
log.warn("未知消息类型: {}", message.getType());
}
} catch (Exception e) {
log.error("处理消息失败", e);
sendErrorMessage(session, "消息格式错误");
}
}
// 处理聊天消息
private void handleChatMessage(WebSocketSession session,
ChatMessage message) {
String fromUserId = getUserId(session);
if (message.getTo().startsWith("group_")) {
// 群消息
groupChatService.sendToGroup(
message.getTo(),
fromUserId,
message.getContent()
);
} else {
// 私聊消息
chatService.sendPrivate(
fromUserId,
message.getTo(),
message.getContent()
);
}
// 保存消息记录
messageHistoryService.saveMessage(
fromUserId,
message.getTo(),
message.getType(),
message.getContent()
);
}
}
6.3 前端完整实现
<!DOCTYPE html>
<html>
<head>
<title>在线聊天室</title>
<style>
.chat-container { width: 600px; margin: 0 auto; }
.messages { height: 400px; border: 1px solid #ccc; overflow-y: scroll; }
.input-area { margin-top: 10px; }
.online-users { width: 200px; float: right; }
</style>
</head>
<body>
<div class="chat-container">
<h2>在线聊天室</h2>
<div class="online-users">
<h3>在线用户</h3>
<ul id="userList"></ul>
</div>
<div class="messages" id="messages"></div>
<div class="input-area">
<input type="text" id="messageInput" placeholder="输入消息...">
<button onclick="sendMessage()">发送</button>
<button onclick="sendImage()">发送图片</button>
<select id="receiver">
<option value="all">所有人</option>
</select>
</div>
</div>
<script>
// WebSocket连接
let ws = null;
let userId = null;
// 初始化
function init() {
userId = prompt("请输入你的用户名");
if (!userId) return;
connect();
loadHistory();
}
// 连接WebSocket
function connect() {
ws = new WebSocket(`ws://localhost:8080/chat?userId=${userId}`);
ws.onopen = onConnected;
ws.onmessage = onMessage;
ws.onclose = onDisconnected;
ws.onerror = onError;
}
// 发送消息
function sendMessage() {
const input = document.getElementById('messageInput');
const receiver = document.getElementById('receiver').value;
const message = {
type: 'chat',
from: userId,
to: receiver,
content: input.value,
time: Date.now()
};
ws.send(JSON.stringify(message));
input.value = '';
}
// 处理收到的消息
function onMessage(event) {
const message = JSON.parse(event.data);
switch (message.type) {
case 'chat':
displayMessage(message);
break;
case 'system':
displaySystemMessage(message);
break;
case 'userList':
updateUserList(message.users);
break;
}
}
// 显示消息
function displayMessage(message) {
const messagesDiv = document.getElementById('messages');
const msgHtml = `
<div class="message">
<strong>${message.from}</strong>
<span>${new Date(message.time).toLocaleTimeString()}</span>
<p>${message.content}</p>
</div>
`;
messagesDiv.innerHTML += msgHtml;
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// 初始化
window.onload = init;
</script>
</body>
</html>
七、避坑指南
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
坑1:Nginx代理
# ❌ 错误:普通HTTP代理
location /chat {
proxy_pass http://backend;
}
# ✅ 正确:WebSocket代理
location /chat {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
坑2:连接数限制
// 解决:调整服务器配置
@Bean
public WebSocketServletServerContainerFactory containerFactory() {
TomcatServletWebSocketContainerFactory factory =
new TomcatServletWebSocketContainerFactory();
factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/404"));
factory.setMaxSessionIdleTimeout(15 * 60 * 1000L); // 15分钟
factory.setMaxBinaryMessageBufferSize(1024 * 1024); // 1MB
factory.setMaxTextMessageBufferSize(512 * 1024); // 512KB
return factory;
}
坑3:消息顺序
// 解决:给消息加序列号
let seq = 0;
function sendMessage(content) {
const message = {
seq: seq++,
content: content,
timestamp: Date.now()
};
ws.send(JSON.stringify(message));
}
八、今日要点总结
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
- WebSocket vs HTTP轮询:真实时 vs 假实时
- 5步搭建:加依赖 → 写配置 → 处理器 → 前端连接 → 完成
- 三种聊天模式:单聊、群聊、广播
- 进阶功能:心跳检测、断线重连、消息历史
- 性能优化:连接限制、消息压缩、集群部署
- 生产注意:Nginx配置、连接数限制、消息顺序
九、思考题
场景:你要做一个万人直播聊天室
- 如何管理上万连接?
- 如何防止消息风暴?
- 如何实现禁言、踢人功能?
- 如何保证消息不丢失?
评论区聊聊你的方案,明天咱们讲任务调度。
明天预告:《SpringBoot定时任务:从简单到集群》
今日福利:关注后回复"WebSocket",获取完整聊天室源码。
公众号运营小贴士:
💡 互动:
- 你做过实时通信功能吗?用的什么方案?
- 投票:你觉得WebSocket最难的部分是什么?
- 留言提问,明天文章解答
🎁 福利:
- 留言区抽3人送《WebSocket实战》
- 转发截图送企业级聊天室源码