文章目录
-
- 一、模块核心设计前提
-
- [1.1 技术选型依据](#1.1 技术选型依据)
- [1.2 依赖环境准备](#1.2 依赖环境准备)
- 二、基础功能实现:历史消息查询
-
- [3.1 前后端交互接口约定](#3.1 前后端交互接口约定)
- [3.2 服务端实现](#3.2 服务端实现)
- [3.3 客户端实现](#3.3 客户端实现)
- 三、核心功能实现:WebSocket实时消息收发
-
- [4.1 WebSocket初始化配置](#4.1 WebSocket初始化配置)
- [4.2 在线用户管理组件](#4.2 在线用户管理组件)
- [4.3 消息请求/响应实体类](#4.3 消息请求/响应实体类)
- [4.4 客户端WebSocket实现](#4.4 客户端WebSocket实现)
- 五、功能验证与注意事项
-
- [5.1 核心功能验证](#5.1 核心功能验证)
- [5.2 关键注意事项](#5.2 关键注意事项)
- 六、后续扩展方向

消息传输模块是多用户网页聊天室的核心功能支柱,直接决定了用户聊天体验的流畅度、实时性与可靠性。其核心目标是打通 "实时消息收发、历史消息回溯、消息持久化存储" 三大关键链路,彻底解决传统 HTTP 协议 "请求 - 响应" 模式无法满足实时通信的技术痛点,为用户打造接近原生聊天工具的交互体验。
本模块基于 WebSocket 协议实现全双工实时通信,搭配 MySQL 数据库完成消息持久化存储,既保证了在线用户之间的毫秒级消息传递,又支持离线用户上线后无缝查看历史消息,完全贴合网页版微信、QQ 等主流聊天工具的核心逻辑。以下将从设计思路、技术选型、实现细节、等多个维度,详细拆解模块开发的完整流程。
一、模块核心设计前提
1.1 技术选型依据
传统 Web 开发中,客户端与服务器通过 HTTP 协议通信,属于"请求-响应"模式,服务器只能被动接收客户端请求并返回结果,无法主动向客户端推送数据。若强行使用 HTTP 实现实时通信,只能通过以下方式迂回实现,但均存在明显缺陷:
- 轮询(Polling):客户端每隔固定时间(如 1 秒)发起一次 HTTP 请求,查询是否有新消息。这种方式的问题在于:大部分请求都是 "无效请求"(无新消息),会造成服务器资源浪费、网络带宽占用,且消息延迟取决于轮询间隔(间隔越长延迟越高,间隔越短资源消耗越大)。
WebSocket 协议作为 HTML5 标准特性,专为实时通信场景设计,其核心优势完美匹配聊天系统需求:
- 全双工通信: 连接建立后,客户端与服务器可双向主动发送数据,无需像 HTTP 那样每次通信都重新建立连接,实现 "一次握手,永久通信"。
- 持久化连接: 连接建立后持续有效,直到客户端或服务器主动关闭,减少了连接建立 / 断开的开销,消息传递延迟可低至毫秒级。
- 轻量级协议头: WebSocket 数据帧的协议头仅 2-10 字节,远小于 HTTP 协议的几十甚至几百字节,数据传输效率更高,尤其适合高频次、小体量的消息交互(如文字聊天)。
- 浏览器原生支持: 主流浏览器(Chrome、Firefox、Edge 等)均原生支持 WebSocket API,无需额外安装插件,开发成本低、兼容性好。
- 协议升级机制: WebSocket 基于 HTTP 协议完成握手升级,可复用 HTTP 的端口(80/443),避免防火墙拦截问题,部署更便捷。
基于以上优势,WebSocket 成为实时聊天场景的最优技术选型,也是本模块的核心通信协议。
1.2 依赖环境准备
项目已在初始化阶段引入 WebSocket 依赖(Spring 内置支持),无需额外新增依赖,仅需通过配置类启用 WebSocket 功能即可。
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
二、基础功能实现:历史消息查询
用户切换会话时,需加载该会话的所有历史消息,实现 "消息回溯" 功能。该功能基于 HTTP 接口实现(无需实时推送,属于 "按需查询" 场景),核心逻辑为 "按会话 ID 查询消息表 → 按时间排序 → 前端格式化渲染",具体实现如下:
3.1 前后端交互接口约定
- 请求:GET /message?sessionId=1(sessionId 为目标会话ID)
- 响应:HTTP/1.1 200 OK | Content-Type: application/json
json
[
{
"messageId": 1,
"fromId": 1,
"fromName": "zhangsan",
"sessionId": 1,
"content": "今晚吃啥?",
"postTime": "2038-01-01 00:00:00"
},
{
"messageId": 2,
"fromId": 2,
"fromName": "lisi",
"sessionId": 1,
"content": "随便",
"postTime": "2038-01-01 00:02:00"
}
]
3.2 服务端实现
(1)创建消息实体类
封装消息查询结果,与接口响应字段一一对应:
java
public class Message {
private int messageId;
private int fromId;
private String fromName; // 发送者昵称(关联user表查询得到)
private int sessionId;
private String content;
private Timestamp postTime; // 数据库存储为datetime类型
// 处理时间格式:Spring默认返回CST格式,手动转换为"yyyy-MM-dd HH:mm:ss"
public String getPostTime() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(postTime);
}
// 省略getter、setter方法
}
(2)创建MessageMapper接口
定义消息查询、插入的核心方法:
java
@Mapper
public interface MessageMapper {
// 根据会话ID查询历史消息(限制100条,避免数据量过大)
List<Message> getMessagesBySessionId(int sessionId);
// 新增消息(消息发送时调用,持久化到数据库)
void add(Message message);
// 查询会话的最后一条消息(用于会话列表消息预览)
String getLastMessagesBySessionId(int sessionId);
}
(3)实现Mapper.xml映射文件
通过联合查询(message表 + user表)获取发送者昵称,按发送时间降序排列后取前100条:
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.chatroom.model.MessageMapper">
<!-- 查询会话历史消息:联合user表获取发送者昵称 -->
<select id="getMessagesBySessionId" resultType="com.example.chatroom.model.Message">
select
messageId,
fromId,
user.username as fromName, -- 关联user表查询昵称
sessionId,
content,
postTime
from message, user
where sessionId = #{sessionId}
and message.fromId = user.userId -- 关联发送者ID与用户表
order by postTime desc limit 100; -- 按时间降序,最多返回100条
</select>
<!-- 新增消息:持久化到数据库 -->
<insert id="add">
insert into message values(null, #{fromId}, #{sessionId}, #{content}, #{postTime});
</insert>
<!-- 查询会话最后一条消息(用于会话列表预览) -->
<select id="getLastMessagesBySessionId" resultType="java.lang.String">
select content from message
where sessionId = #{sessionId}
order by postTime desc limit 1;
</select>
</mapper>
(4)创建MessageAPI控制层
提供历史消息查询接口,接收会话ID参数,返回按时间升序排列的消息列表(数据库查询为降序,需手动反转):
java
@RestController
public class MessageAPI {
@Resource
private MessageMapper messageMapper;
@GetMapping("/message")
public Object getMessage(int sessionId) {
// 1. 查询该会话的历史消息(降序)
List<Message> messages = messageMapper.getMessagesBySessionId(sessionId);
// 2. 反转列表,转为升序(符合聊天消息展示逻辑:旧消息在前,新消息在后)
Collections.reverse(messages);
return messages;
}
}
3.3 客户端实现
用户点击会话列表项时,触发历史消息查询,渲染到右侧消息区域,并自动滚动到最新消息位置:
javascript
// 加载会话历史消息(在clickSession函数中调用)
function getHistoryMessage(sessionId) {
console.log('获取历史消息: ' + sessionId);
let titleDiv = document.querySelector('.right>.title');
let messageShowDiv = document.querySelector('.message-show');
// 清空原有消息内容
titleDiv.innerHTML = '';
messageShowDiv.innerHTML = '';
// 设置会话标题(取自选中的会话项)
let selectedH3 = document.querySelector('#session-list>.selected>h3');
if (selectedH3) {
titleDiv.innerHTML = selectedH3.innerHTML.trim();
}
// 新会话无sessionId,无需查询
if (!sessionId) return;
// 发起HTTP请求查询历史消息
$.ajax({
type: 'get',
url: '/message?sessionId=' + sessionId,
success: function(body) {
// 获取当前登录用户名(用于区分自己和他人消息的显示位置)
let selfUsername = document.querySelector('.user').innerHTML.trim();
for (let message of body) {
addMessage(messageShowDiv, message, selfUsername);
}
// 自动滚动到消息底部(显示最新消息)
scrollBottom(messageShowDiv);
},
error: function() {
alert('获取历史消息失败!');
}
});
}
三、核心功能实现:WebSocket实时消息收发
实时消息收发是模块的核心,也是用户最直观的使用场景。基于 WebSocket 实现 "客户端发送消息 → 服务器接收并转发 → 目标客户端接收并渲染" 的全流程,确保消息实时性、可靠性与安全性。
4.1 WebSocket初始化配置
(1)创建WebSocket配置类
启用WebSocket功能,映射请求路径(/WebSocketMessage),并通过拦截器将HTTP Session中的用户信息传递到WebSocket Session,因为Session 是在http中的,因此使用该拦截器将用户信息同样的拷贝到WebSocket中去:
java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private WebSocketAPI webSocketAPI; // 自定义WebSocket处理器
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketAPI, "/WebSocketMessage")
// 传递HTTP Session中的用户信息到WebSocket Session
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
(2)创建WebSocketAPI处理器
继承TextWebSocketHandler,重写连接建立、消息接收、连接关闭等核心方法,处理WebSocket全生命周期事件:
java
@Slf4j
@Component
public class WebSocketAPI extends TextWebSocketHandler {
@Autowired
private OnlineUserManager onlineUserManager;
private ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private MessageSessionMapper messageSessionMapper;
@Autowired
private MessageMapper messageMapper;
// 在websocket 连接出现异常时,被自动调用
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
System.out.println("WebSocket 连接异常!" + exception.toString());
User user = (User) session.getAttributes().get("user");
if(user == null){
return;
}
onlineUserManager.offline(user.getUserId(),session);
}
// 在websocket 连接建立成功后,被自动调用
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("WebSocket 连接成功!");
User user = (User) session.getAttributes().get("user");
if(user == null){
return;
}
System.out.println("获取到的userId: " + user.getUserId());
// 存储键值对
onlineUserManager.online(user.getUserId(),session);
}
// 在连接正常关闭后,被自动调用
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("WebSocket 连接关闭!" + status.toString());
// 下线,移除映射
User user = (User) session.getAttributes().get("user");
if(user == null){
return;
}
onlineUserManager.offline(user.getUserId(),session);
}
// 在 websocket收到小时后自动调用
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
System.out.println("WebSocket 收到消息!" + message.toString());
// 处理消息接收,转发,保存记录
// 获取到当前用户的信息
User user = (User) session.getAttributes().get("user");
if(user == null){
log.info("未登录用户,无法进行消息转发!");
return;
}
// 对请求解析
MessageRequest req = objectMapper.readValue(message.getPayload(), MessageRequest.class);
log.info("解析后的MessageRequest:{}",req);
if(req.getType().equals("message")){
transferMessage(user,req);
messageSessionMapper.updateLastTime(req.getSessionId());
}else {
log.info("WebSocket Type 有误!" + message.getPayload());
}
}
// 完成消息转发
private void transferMessage(User fromUser, MessageRequest req) throws IOException {
// 构造代转发的响应对象
MessageResponse response = new MessageResponse();
response.setFromId(fromUser.getUserId());
response.setFromName(fromUser.getUsername());
response.setSessionId(req.getSessionId());
response.setContent(req.getContent());
// 序列化为json
String respjson = objectMapper.writeValueAsString(response);
log.info("转发数据, {}",respjson);
List<Friend> friendList = messageSessionMapper.getFriendsByUserId(req.getSessionId(),fromUser.getUserId());
Friend myself = new Friend();
myself.setFriendId(fromUser.getUserId());
myself.setFriendName(fromUser.getUsername());
friendList.add(myself);
// 全部转发
for (Friend friend : friendList){
WebSocketSession webSocketSession = onlineUserManager.getWebSocketSession(friend.getFriendId());
// 如果用户不在线,则不转发,用户上线后可从数据库中获取查看
if(webSocketSession == null){
continue;
}
webSocketSession.sendMessage(new TextMessage(respjson));
}
// 将消息添加到数据库中
Message message = new Message();
message.setFromId(fromUser.getUserId());
message.setSessionId(response.getSessionId());
message.setContent(response.getContent());
messageMapper.add(message);
}
}
4.2 在线用户管理组件
创建OnlineUserManager组件,维护userId → WebSocketSession的映射关系,使用哈希表存储用户与WebSocketSession 之间的映射关系,用于快速查找用户在线状态、转发消息:
java
@Component
public class OnlineUserManager {
// 线程安全的Map:存储在线用户的WebSocketSession
private ConcurrentHashMap<Integer, WebSocketSession> sessions = new ConcurrentHashMap<>();
// 用户上线:记录Session(禁止同一用户多端登录)
public void online(int userId, WebSocketSession session) {
if (sessions.get(userId) != null) {
return; // 已在线,直接返回
}
sessions.put(userId, session);
System.out.println("[" + userId + "] 上线!");
}
// 用户离线:移除Session(需校验是否为当前会话)
public void offline(int userId, WebSocketSession session) {
WebSocketSession existSession = sessions.get(userId);
if (existSession != session) {
return; // 非当前会话,不处理
}
sessions.remove(userId);
System.out.println("[" + userId + "] 下线!");
}
// 根据用户ID获取WebSocketSession(用于消息转发)
public WebSocketSession getSession(int userId) {
return sessions.get(userId);
}
}
4.3 消息请求/响应实体类
定义客户端与服务器之间的消息格式,统一JSON序列化规则,确保前后端数据交互的一致性:
java
// 客户端发送消息的请求格式
public class MessageRequest {
private String type; // 消息类型:固定为"message"
private int sessionId; // 目标会话ID
private String content; // 消息正文
// 省略getter、setter方法
}
// 服务器转发消息的响应格式
public class MessageResponse {
private String type; // 消息类型:固定为"message"
private int fromId; // 发送者ID
private String fromName; // 发送者昵称
private int sessionId; // 所属会话ID
private String content; // 消息正文
private Timestamp postTime;// 发送时间(服务器时间)
// 省略getter、setter方法
}
4.4 客户端WebSocket实现
客户端 WebSocket 实现分为 "连接初始化""消息发送""消息接收与渲染""连接异常处理" 四个核心部分,确保用户能快速发送消息、实时接收消息,且体验流畅。
(1)初始化WebSocket连接
在client.js中初始化WebSocket连接,监听连接建立、消息接收、连接关闭等事件:
javascript
// 初始化WebSocket连接(页面加载时执行)
let websocket;
function initWebSocket() {
// 注意:路径为"/message",与服务器配置一致,末尾不带斜杠
websocket = new WebSocket("ws://127.0.0.1:8080/message");
// 1. 连接建立成功回调
websocket.onopen = function (event) {
console.log("websocket 连接成功!");
};
// 2. 接收服务器消息回调(核心:实时渲染消息)
websocket.onmessage = function(event) {
console.log('[收到消息] ' + event.data);
let resp = JSON.parse(event.data);
if (resp.type == 'message') {
handlerMessage(resp); // 处理实时消息
}
};
// 3. 连接关闭回调
websocket.onclose = function () {
console.log("websocket 连接关闭!");
};
// 4. 连接异常回调
websocket.onerror = function () {
console.log("websocket 连接异常!");
};
// 5. 窗口关闭时主动关闭WebSocket连接
window.onbeforeunload = function () {
websocket.close();
};
}
// 页面加载时初始化
initWebSocket();
(2)消息发送功能
绑定"发送"按钮点击事件,获取输入框内容,构造MessageRequest格式,通过WebSocket发送:
javascript
// 初始化发送按钮事件
function initSendButton() {
let sendBtn = document.querySelector('.ctrl>button');
let messageInput = document.querySelector('.message-input>textarea');
sendBtn.onclick = function() {
// 1. 获取输入框内容(去空格)
let content = messageInput.value.trim();
if (!content) {
alert("消息内容不能为空!");
return;
}
// 2. 获取当前选中的会话ID
let selectedSessionLi = document.querySelector('#session-list>.selected');
if (!selectedSessionLi) {
alert("请先选择一个会话!");
return;
}
let sessionId = selectedSessionLi.getAttribute('message-session-id');
// 3. 构造消息请求(与MessageRequest类对应)
let req = {
type: 'message',
sessionId: parseInt(sessionId),
content: content
};
// 4. 发送消息(JSON序列化)
websocket.send(JSON.stringify(req));
// 5. 清空输入框
messageInput.value = '';
};
}
// 页面加载时初始化发送按钮
initSendButton();
(3)实时消息接收与渲染
接收服务器转发的消息,更新会话列表消息预览,若当前会话为选中状态,则直接渲染到消息区域:
javascript
// 处理实时接收的消息
function handlerMessage(resp) {
let sessionListUL = document.querySelector('#session-list');
let selfUsername = document.querySelector('.user').innerHTML.trim();
// 1. 查找该会话对应的列表项,不存在则创建
let curSessionLi = findSessionById(resp.sessionId);
if (curSessionLi == null) {
curSessionLi = document.createElement('li');
curSessionLi.setAttribute('message-session-id', resp.sessionId);
curSessionLi.innerHTML = `<h3>${resp.fromName}</h3><p></p>`;
curSessionLi.onclick = function() {
clickSession(curSessionLi);
};
sessionListUL.insertBefore(curSessionLi, sessionListUL.children[0]);
}
// 2. 更新会话列表的消息预览(超过10字截断)
let previewP = curSessionLi.querySelector('p');
let previewContent = resp.content.length > 10 ? resp.content.substring(0, 10) + '...' : resp.content;
previewP.innerHTML = previewContent;
// 3. 若当前会话为选中状态,直接渲染消息并滚动到底部
if (curSessionLi.className == 'selected') {
let messageShowDiv = document.querySelector('.message-show');
// 构造消息对象(与历史消息格式一致,复用addMessage方法)
let message = {
fromName: resp.fromName,
content: resp.content,
postTime: resp.postTime
};
addMessage(messageShowDiv, message, selfUsername);
scrollBottom(messageShowDiv);
}
}
五、功能验证与注意事项
5.1 核心功能验证
- 实时消息收发:两个用户同时在线,发送消息后对方实时接收并渲染,消息位置正确(自己在右、他人在左);
- 历史消息加载:切换会话时,正确加载该会话的历史消息,时间格式显示为"yyyy-MM-dd HH:mm:ss";
- 消息持久化:发送消息后,数据库message表新增对应记录,刷新页面后历史消息不丢失;
- 离线消息支持:用户A离线时,用户B发送消息,A上线后切换会话可查看离线期间的消息;
- 多端登录限制:同一用户无法同时登录多个页面,避免消息转发异常。
验证示例:

5.2 关键注意事项
- WebSocket路径一致性 :客户端连接路径需与服务器
WebSocketConfig配置的路径完全一致(本文为"/WebSocketMessage"),末尾不能带斜杠; - 时间格式处理 :数据库存储为datetime类型,Spring默认返回CST格式,需通过
SimpleDateFormat手动转换为标准格式; - 线程安全 :
OnlineUserManager使用ConcurrentHashMap存储在线用户,避免多线程环境下的并发问题; - 连接关闭 :窗口关闭时主动调用
websocket.close(),避免服务器抛出"连接重置"异常; - 消息长度限制 :数据库
content字段长度为2048,客户端可添加输入长度限制,避免消息过长导致插入失败。
六、后续扩展方向
- 消息类型扩展 :支持图片、表情消息,需在message表新增
type字段(text/image/emoji),客户端新增图片上传组件; - 未读消息提示 :在会话列表项添加未读消息数字提示,需设计
unread_message表记录未读状态; - 消息撤回 :支持2分钟内撤回消息,需在MessageResponse中新增
isRecall字段,客户端隐藏撤回的消息; - 消息搜索 :实现按关键词搜索历史消息,扩展
MessageMapper接口,添加模糊查询方法; - 群聊支持:当前会话设计已预留群聊扩展(friends数组),仅需新增群聊创建、群成员管理功能即可。