【实战】Spring Boot + WebSocket 实现仿微信即时通讯(支持语音/图片/长连接)- 从原理到落地
摘要:在开发“适老化”应用或社交平台时,即时通讯(IM)是绕不开的核心功能。传统的 HTTP 轮询不仅浪费资源,还无法保证消息的实时性。本文将基于 Spring Boot 和原生 WebSocket,结合 Vue 3 前端,手把手带你实现一个支持文字、图片、语音消息(带时长)的点对点即时聊天系统。
关键词:Spring Boot, WebSocket, Vue3, 即时通讯, 实战
一、 为什么选择 WebSocket?
在 Web 开发中,最常见的 HTTP 协议是“请求-响应”模式,服务器极其被动,无法主动向客户端推送消息。
如果我们要实现聊天功能,使用 HTTP 通常需要轮询(Polling),即前端每隔 1 秒问一次后端:“有新消息吗?”。这就像你每隔几秒就去敲邻居门问他在不在,效率极低且服务器压力大。
WebSocket 则是一种在单个 TCP 连接上进行全双工通信的协议。
-
建立连接:一次握手,永久保持连接。
-
通信方式:服务器可以主动给客户端发消息,客户端也可以随时给服务器发消息。
-
场景:聊天室、即时通讯、股票行情推送。

二、 后端架构设计 (Spring Boot)
1. 依赖与配置
首先,我们需要在 Spring Boot 中开启 WebSocket 支持。核心配置类实现 WebSocketConfigurer 接口。
WebSocketConfig.java 解析:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private ChatHandler chatHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 核心配置:
// 1. 绑定处理器 chatHandler
// 2. 定义连接端点 /chat/{userId},前端连接时需带上自己的 ID
// 3. setAllowedOrigins("*") 允许跨域,方便前后端分离调试
registry.addHandler(chatHandler, "/chat/{userId}")
.setAllowedOrigins("*");
}
}
2. 核心处理器 (The Core)
这是整个即时通讯的大脑。我们需要继承 TextWebSocketHandler。这里有三个关键点:
-
用户管理:使用
ConcurrentHashMap存储所有在线用户的Session。 -
消息转发:接收发送者的消息 -> 存入数据库 -> 找到接收者的 Session -> 推送。
-
断开清理:用户下线时移除 Session,防止内存泄漏。
ChatHandler.java 核心代码:
@Component
public class ChatHandler extends TextWebSocketHandler {
@Autowired
private MessageMapper messageMapper;
// 线程安全的 Map,用来存放 [用户ID -> WebSocketSession] 的映射
private static final Map<Long, WebSocketSession> onlineUsers = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 连接建立成功后触发
* 从 URL 路径 /chat/{userId} 中解析出当前登录的用户 ID
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String path = session.getUri().getPath();
String userIdStr = path.substring(path.lastIndexOf('/') + 1);
onlineUsers.put(Long.parseLong(userIdStr), session);
System.out.println("【WebSocket】用户上线: " + userIdStr);
}
/**
* 处理收到的文本消息
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
try {
// 1. 反序列化消息(包含 msgType, duration, toId 等)
Message msg = objectMapper.readValue(payload, Message.class);
// 2. 消息持久化入库(这一步非常重要,防止消息丢失)
// 注意:语音消息需要保存 duration (时长)
messageMapper.insertMessage(msg);
// 3. 实时转发给目标用户
WebSocketSession targetSession = onlineUsers.get(msg.getToId());
// 如果对方在线,直接通过 WebSocket 推送
if (targetSession != null && targetSession.isOpen()) {
// 重新序列化,确保 ID 和时间等数据库生成的字段被返回
String responseJson = objectMapper.writeValueAsString(msg);
targetSession.sendMessage(new TextMessage(responseJson));
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 连接关闭后移除用户
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
onlineUsers.entrySet().removeIf(entry -> entry.getValue().equals(session));
}
}
3. 消息模型设计
为了支持语音和多媒体,数据库模型必须具备通用性。
Message.java:
@Data
public class Message {
private Long id;
private Long fromId; // 发送者
private Long toId; // 接收者
private String content;// 内容(文本是文字,语音/图片则是 URL 地址)
private String msgType;// 消息类型:text, voice, image
private Integer duration; // 【关键】语音时长,用于前端展示语音条长度
private Date createTime;
}
三、 前端实现 (Vue 3 + WebSocket)
前端主要负责建立连接、心跳保活、以及处理多媒体输入(录音、传图)。
1. 建立连接
在 Vue 的 mounted 生命周期中初始化 WebSocket。
initWS(uid) {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// 连接地址对应后端的 registry.addHandler
this.socket = new WebSocket(`${protocol}//${window.location.host}/chat/${uid}`);
this.socket.onmessage = (e) => {
const m = JSON.parse(e.data);
// 判断消息是不是发给当前聊天窗口的
if(m.fromId == this.currentTargetId) {
this.messageList.push(m);
this.goBottom(); // 滚动到底部
}
};
},
2. 核心难点:语音消息与文件上传
WebSocket 协议本身适合传输文本或小二进制数据。对于图片和语音文件,最佳实践是“HTTP 上传 + WebSocket 通知”。
逻辑流程:
-
前端录音或选择图片。
-
通过 Axios (
POST /api/common/upload) 将文件上传到服务器/OSS。 -
服务器返回文件的 URL。
-
前端将 URL、消息类型、语音时长 封装成 JSON,通过 WebSocket 发送给对方。
前端代码片段 (msg.html):
// 发送消息的统一方法
emitMessage(content, type, dur = 0) {
const msg = {
fromId: this.currentUser.id,
toId: this.currentTargetId,
content: content, // 这里可能是文字,也可能是上传后的 URL
msgType: type,
duration: dur
};
// 发送 JSON 字符串
this.socket.send(JSON.stringify(msg));
// 自己界面上也要立刻显示出来
this.messageList.push(msg);
this.goBottom();
},
// 录音结束后的处理
async stopRecord() {
// ... 省略 MediaRecorder 停止逻辑 ...
const dur = Math.round((Date.now() - this.recStart) / 1000); // 计算时长
// 1. 上传文件
const fd = new FormData();
fd.append('file', blob, 'v.webm');
const res = await axios.post('/api/common/upload', fd);
// 2. 通过 WebSocket 发送链接和时长
if(res.data.code === 200) {
this.emitMessage(res.data.url, 'voice', dur);
}
}
3. 动态渲染气泡
为了让界面更像微信,我们需要根据 msgType 渲染不同的 DOM 结构。
<div class="msg-content">
<template v-if="msg.msgType === 'text'">
<div class="bubble">{{ msg.content }}</div>
</template>
<template v-else-if="msg.msgType === 'voice'">
<div class="bubble voice-bubble" @click="playVoice(msg.content)"
:style="{ width: (150 + msg.duration * 10) + 'px' }">
<i class="fa fa-rss voice-icon"></i>
<span>{{ msg.duration }}''</span>
</div>
</template>
<template v-else-if="msg.msgType === 'image'">
<img :src="msg.content" class="media-obj">
</template>
</div>
四、 遇到的问题与解决方案
1. 并发 Map 的选择
在 ChatHandler 中,必须使用 ConcurrentHashMap 而不是普通的 HashMap。因为 WebSocket 是多线程环境,多个用户同时上下线时,普通的 Map 会导致并发修改异常或数据不一致。
2. 语音时长的丢失问题
一开始实现时,发现接收方收到的语音消息时长总是显示默认值。
原因:转发时直接转发了前端发来的字符串,或者数据库读取时漏了字段。
解决:
-
前端发送 JSON 时必须带上
duration。 -
后端
MessageMapper插入时必须保存duration。 -
后端
MessageMapper查询历史记录时,SQL 语句必须显式查出duration字段。
// MessageMapper.java
@Select("SELECT ..., duration, DATE_FORMAT(create_time, '%H:%i') as time ...")
3. 滚动条自动触底
聊天软件的体验关键在于新消息到来时自动滚动到底部。
技巧:在 Vue 中使用 $nextTick,确保 DOM 更新(消息渲染完毕)后再执行滚动逻辑。
JavaScript
goBottom(delay = 50) {
setTimeout(() => {
this.$nextTick(() => {
const el = document.getElementById('chat-box');
el.scrollTop = el.scrollHeight; // 暴力拉到底
});
}, delay);
}
五、 总结
通过 Spring Boot 的 spring-boot-starter-websocket 和 Vue 3,我们用极少的代码量实现了一个功能完备的即时通讯系统,未来将实现视频/语音通话,还在努力学习中。
核心流程回顾:
-
连接:Websocket 握手,绑定 UserID。
-
通信:前端
sendJSON -> 后端handleTextMessage-> 存库 ->sendMessage给目标。 -
媒体:HTTP 上传文件换取 URL,WebSocket 仅传输“指令”和“元数据”。
六、 界面展示
聊天界面
添加好友

好友申请列表

未读消息提醒
更多推荐




所有评论(0)