文章目录
- 核心组件
- 网络通信层
- 事件调度层
- 服务编排层
- Spring实现客服聊天
- 技术方案对比
- WebScoket建立连接
- 用户上线
- 实现指定用户私聊
- 群聊
- 离线
- SpringBoot+WebSocket+Html+jQuery实现客服聊天
- 1. 目录结构
- 2. 配置类
- 3. 实体类、service、controller
- 4. ChatWebSocketHandler消息处理
- 5.前端页面
- 6.效果
- 代码链接
核心组件
网络通信层
- Bootstrap
负责客户端启动并用来链接远程Netty Server; - ServerBootStrap
负责服务端监听,用来监听指定端口: - Channel
相当于完成网络通信的载体。
事件调度层
- EventLoopGroup
本质上是一个线程池,主要负责接收/O请求,并分配线程执行处理请 - EventLoop
相当于线程池中的线程。
服务编排层
- ChannelPipeline
负责将多个ChannelHandler链接在一起。 - ChannelHandler
针对l/O的数据处理器数据接收后,通过指定的Handleri进行处理。 - ChannelHandlerContext
用来保存ChannelHandler的上下文信息。
Spring实现客服聊天
- 新建一个maven项目
- 引入依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.99.Final</version>
</dependency>
- 启动IMServer的方法
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class ImServer {
/**
* 启动IM服务器的方法
*/
public static void start() {
// 创建两个EventLoopGroup,bossGroup用于处理连接请求,workerGroup用于处理已连接的客户端的I/O操作
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try {
// 创建ServerBootstrap实例,用于配置服务器
ServerBootstrap bootstrap = new ServerBootstrap();
// 设置EventLoopGroup
bootstrap.group(boss, worker)
// 设置通道类型为NioServerSocketChannel
.channel(NioServerSocketChannel.class)
// 设置子处理器,用于处理新连接的初始化
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 在这里可以添加各种ChannelHandler来处理消息
}
});
// 绑定服务器到指定端口,并同步等待成功
ChannelFuture future = bootstrap.bind(8001).sync();
// 等待服务器socket关闭
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 优雅地关闭EventLoopGroup
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
- 启动
public class ImApplication {
public static void main(String[] args) {
ImServer.start();
}
}
启动成功
技术方案对比
技术 | 说明 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
WebSocket | 双向持久连接,服务器和客户端可以随时发送消息 | 低延迟、实时性强、双向通信 | 需要浏览器支持,可能被防火墙拦截 | 客服聊天、游戏、协作编辑 |
轮询(Polling) | 客户端定期请求服务器获取新消息 | 兼容性好,所有浏览器支持 | 占用带宽,高并发时服务器压力大 | 简单场景,低频率更新的聊天 |
长轮询(Long Polling) | 客户端请求后,服务器等待新消息再返回 | 比普通轮询节省带宽 | 服务器压力仍然较大 | 稍微实时的聊天应用 |
SSE(Server-Sent Events) | 服务器单向推送消息到客户端 | 轻量级、兼容 HTTP/2 | 仅支持服务器向客户端推送 | 客服系统中的通知功能 |
WebScoket建立连接
- 设置消息处理器
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
public class ImServer {
/**
* 启动IM服务器的方法
*/
public static void start() {
// 创建两个EventLoopGroup,bossGroup用于处理连接请求,workerGroup用于处理已连接的客户端的I/O操作
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try {
// 创建ServerBootstrap实例,用于配置服务器
ServerBootstrap bootstrap = new ServerBootstrap();
// 设置EventLoopGroup
bootstrap.group(boss, worker)
// 设置通道类型为NioServerSocketChannel
.channel(NioServerSocketChannel.class)
// 设置子处理器,用于处理新连接的初始化
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//用于HTTP请求和响应的编解码
pipeline.addLast(new HttpServerCodec())
//支持分块写入大文件或流式数据
.addLast(new ChunkedWriteHandler())
//将多个HTTP消息片段合并成一个完整的HTTP消息,最大聚合大小为64KB。
.addLast(new HttpObjectAggregator(1024*64))
// 添加WebSocket协议处理器,用于处理WebSocket握手和协议升级
.addLast(new WebSocketServerProtocolHandler("/"))
// 添加自定义的WebSocket业务逻辑处理器,用于处理WebSocket消息的接收和发送。
.addLast(new WebSocketHandler());
}
});
// 绑定服务器到指定端口,并同步等待成功
ChannelFuture future = bootstrap.bind(8001).sync();
// 等待服务器socket关闭
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 优雅地关闭EventLoopGroup
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
- 消息处理的实现
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
/**
* 处理接收到的 TextWebSocketFrame 消息
* @param channelHandlerContext 表示当前通道的上下文,包含与通道相关的所有信息和操作方法。
* 可用于执行如写数据、关闭通道、触发事件等操作。
* @param textWebSocketFrame 表示接收到的 WebSocket 文本帧消息。
* 包含客户端发送的具体数据内容,可以通过其方法(如 text())获取消息内容并进行处理。
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
System.out.println(textWebSocketFrame.text());
}
}
- 前端页面
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简单聊天页面</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; margin: 20px; }
#chatBox { width: 100%; height: 300px; border: 1px solid #ccc; overflow-y: auto; padding: 10px; text-align: left; }
#messageInput { width: 80%; padding: 8px; margin-top: 10px; }
#sendBtn { padding: 8px; cursor: pointer; }
</style>
</head>
<body>
<h2>简单 WebSocket 聊天</h2>
<div id="chatBox"></div>
<input type="text" id="messageInput" placeholder="输入消息...">
<button id="sendBtn">发送</button>
<script>
// 连接 WebSocket 服务器
const socket = new WebSocket("ws://localhost:8001");
// 监听 WebSocket 连接
socket.onopen = function () {
console.log("WebSocket 已连接");
appendMessage("✅ 连接成功");
};
// 监听收到的消息
socket.onmessage = function (event) {
appendMessage("💬 服务器: " + event.data);
};
// 监听 WebSocket 关闭
socket.onclose = function () {
appendMessage("❌ 连接已关闭");
};
// 监听发送按钮点击
document.getElementById("sendBtn").addEventListener("click", function () {
sendMessage();
});
// 监听回车键发送消息
document.getElementById("messageInput").addEventListener("keypress", function (event) {
if (event.key === "Enter") {
sendMessage();
}
});
// 发送消息
function sendMessage() {
const input = document.getElementById("messageInput");
const message = input.value.trim();
if (message) {
socket.send(message);
appendMessage("📝 我: " + message);
input.value = "";
}
}
// 在聊天框中追加消息
function appendMessage(text) {
const chatBox = document.getElementById("chatBox");
const messageElement = document.createElement("p");
messageElement.textContent = text;
chatBox.appendChild(messageElement);
chatBox.scrollTop = chatBox.scrollHeight; // 滚动到底部
}
</script>
</body>
</html>
- 启动测试
用户上线
- 定义一个实体,用来接收消息
import lombok.Data;
@Data
public class Command {
private Integer code;
private String name;
}
- 定义一个枚举,用来区分消息类型
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum CommandType {
/**
* 登陆连接
*/
CONNECTION(1001),
/**
* 错误
*/
ERROR(-1)
;
private final Integer code;
public static CommandType getCommandType(Integer code) {
for (CommandType commandType : CommandType.values()) {
if (commandType.getCode().equals(code)) {
return commandType;
}
}
return ERROR;
}
}
- ImServer定义一个map,用来存储登陆的用户
public static final ConcurrentHashMap<String, Channel> USERS = new ConcurrentHashMap<>(1024);
- 添加一个登陆处理的实现类
ConnectionHandler
import com.wzw.Command;
import com.wzw.ImServer;
import com.wzw.Result;
import io.netty.channel.ChannelHandlerContext;
public class ConnectionHandler {
/**
* 处理客户端的连接请求
*
* @param channelHandlerContext 与客户端通信的ChannelHandlerContext
* @param command 包含客户端发送的命令信息
*/
public static void execute(ChannelHandlerContext channelHandlerContext, Command command) {
// 检查用户名是否已经存在
if (ImServer.USERS.containsKey(command.getName())) {
// 如果用户名重复,发送失败消息
channelHandlerContext.channel().writeAndFlush(Result.fail("用户名重复"));
//并断开连接
channelHandlerContext.disconnect();
return;
}
// 将新的用户添加到在线用户列表中
ImServer.USERS.put(command.getName(), channelHandlerContext.channel());
// 发送连接成功的消息
channelHandlerContext.channel().writeAndFlush(Result.success("连接成功"));
// 发送当前在线用户列表
channelHandlerContext.channel().writeAndFlush(Result.success("当前在线用户:" + ImServer.USERS.keySet()));
}
}
- WebSocketHandler中添加消息处理的实现,如果登陆服务,调用ConnectionHandler
import com.alibaba.fastjson2.JSON;
import com.wzw.Command;
import com.wzw.CommandType;
import com.wzw.Result;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
/**
* 处理接收到的 TextWebSocketFrame 消息
* @param channelHandlerContext 表示当前通道的上下文,包含与通道相关的所有信息和操作方法。
* 可用于执行如写数据、关闭通道、触发事件等操作。
* @param textWebSocketFrame 表示接收到的 WebSocket 文本帧消息。
* 包含客户端发送的具体数据内容,可以通过其方法(如 text())获取消息内容并进行处理。
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
try {
//收到的消息转为Command对象
Command command = JSON.parseObject(textWebSocketFrame.text(), Command.class);
//判断消息是不是连接登陆
switch (CommandType.getCommandType(command.getCode())){
case CONNECTION -> ConnectionHandler.execute(channelHandlerContext, command);
default -> channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
}
} catch (Exception e) {
channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
}
}
}
- 前端代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简单聊天页面</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; display: flex; }
#leftPanel { width: 30%; padding-right: 20px; box-sizing: border-box; }
#rightPanel { width: 70%; }
#chatBox { width: 100%; height: 300px; border: 1px solid #ccc; overflow-y: auto; padding: 10px; text-align: left; }
#messageInput { width: 80%; padding: 8px; margin-top: 10px; }
button { padding: 8px; cursor: pointer; }
#nameInput { width: 80%; padding: 8px; margin-top: 10px; }
</style>
</head>
<body>
<div id="leftPanel">
<h2>登录</h2>
<input type="text" id="nameInput" placeholder="输入昵称...">
<button id="sign">登陆</button>
</div>
<div id="rightPanel">
<h2>对话</h2>
<div id="chatBox"></div>
<input type="text" id="messageInput" placeholder="输入消息...">
<button id="sendBtn">发送</button>
</div>
<script>
let socket;
// 监听“登陆”按钮点击
document.getElementById("sign").addEventListener("click", function () {
// 获取昵称输入框的值
const name = document.getElementById("nameInput").value.trim();
if (!name) {
appendMessage("❌ 请输入昵称");
return;
}
// 连接 WebSocket 服务器
socket = new WebSocket("ws://localhost:8001");
// 监听 WebSocket 错误
socket.onerror = function (error) {
console.error("WebSocket 连接错误: ", error);
appendMessage("连接服务器异常");
};
// 监听 WebSocket 连接
socket.onopen = function () {
console.log("WebSocket 已连接");
// 将昵称包含在初始消息中
socket.send(JSON.stringify({
"code": 1002,
"name": name
}));
};
// 监听收到的消息
socket.onmessage = function (event) {
const data = JSON.parse(event.data);
const message = data.message;
appendMessage(message);
};
// 监听 WebSocket 关闭
socket.onclose = function () {
appendMessage("❌ 连接已关闭");
};
});
// 监听发送按钮点击
document.getElementById("sendBtn").addEventListener("click", function () {
sendMessage();
});
// 监听回车键发送消息
document.getElementById("messageInput").addEventListener("keypress", function (event) {
if (event.key === "Enter") {
sendMessage();
}
});
// 发送消息
function sendMessage() {
if (socket && socket.readyState === WebSocket.OPEN) {
const input = document.getElementById("messageInput");
const message = JSON.stringify({
"code": 1001,
"name": input.value.trim()
})
socket.send(message);
appendMessage("📝 我: " + message);
input.value = "";
} else {
appendMessage("❌ 未连接到服务器,请先登录");
}
}
// 在聊天框中追加消息
function appendMessage(text) {
const chatBox = document.getElementById("chatBox");
const messageElement = document.createElement("p");
messageElement.textContent = text;
chatBox.appendChild(messageElement);
chatBox.scrollTop = chatBox.scrollHeight; // 滚动到底部
}
</script>
</body>
</html>
- 测试上线
实现指定用户私聊
- 创建消息对象,用来接收发送消息
import lombok.Data;
@Data
public class ChatMessage extends Command {
/**
* 消息类型
*/
private Integer type;
/**
* 接收人
*/
private String target;
/**
* 消息内容
*/
private String content;
}
- CommandType补充一个消息类型,代表发送消息
/**
* 登陆连接
*/
CONNECTION(1001),
/**
* 消息
*/
CHAT(1002),
/**
* 错误
*/
ERROR(-1)
;
- 加一个枚举,区分私有和群聊消息
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum MessageType {
PRIVATE(1),
GROUP(2),
Error(-1);
private Integer type;
public static MessageType getMessageType(Integer type) {
for (MessageType messageType : values()) {
if (messageType.getType().equals(type)) {
return messageType;
}
}
return Error;
}
}
- 消息处理类
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.wzw.Result;
import com.wzw.command.ChatMessage;
import com.wzw.command.MessageType;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import static com.wzw.ImServer.USERS;
public class ChatHandler {
/**
* 处理接收到的WebSocket文本消息
*
* @param channelHandlerContext 与客户端通信的ChannelHandlerContext
* @param textWebSocketFrame 接收到的WebSocket文本帧
*/
public static void execute(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) {
try {
// 将接收到的文本消息解析为ChatMessage对象
ChatMessage chatMessage = JSON.parseObject(textWebSocketFrame.text(), ChatMessage.class);
// 根据消息类型进行处理
switch (MessageType.getMessageType(chatMessage.getType())) {
case PRIVATE -> {
// 如果目标用户为空,发送失败消息
if (StrUtil.isBlank(chatMessage.getTarget())) {
channelHandlerContext.channel().writeAndFlush(Result.fail("消息发送失败,请指定消息接收对象"));
return;
}
// 获取目标用户的Channel
Channel channel = USERS.get(chatMessage.getTarget());
// 如果目标用户不在线,发送失败消息
if (channel == null) {
channelHandlerContext.channel().writeAndFlush(Result.fail("消息发送失败,用户" + chatMessage.getTarget() + "不在线"));
} else {
// 目标用户在线,发送私聊消息
channel.writeAndFlush(Result.success("私聊消息(" + chatMessage.getName() + "):" + chatMessage.getContent()));
}
}
default -> {
// 如果消息类型不支持,发送失败消息
channelHandlerContext.channel().writeAndFlush(Result.fail("消息发送失败,不支持的消息类型"));
}
}
} catch (Exception e) {
// 捕获并处理解析消息时的异常,发送格式错误消息
channelHandlerContext.channel().writeAndFlush(Result.fail("消息格式错误"));
}
}
}
- WebSocketHandler中新增一个聊天消息处理调用
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
try {
Command command = JSON.parseObject(textWebSocketFrame.text(), Command.class);
switch (CommandType.getCommandType(command.getCode())){
case CONNECTION -> ConnectionHandler.execute(channelHandlerContext, command);
//聊天消息处理
case CHAT -> ChatHandler.execute(channelHandlerContext, textWebSocketFrame);
default -> channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
}
} catch (Exception e) {
channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
}
}
- 前端代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简单聊天页面</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; display: flex; }
#leftPanel { width: 30%; padding-right: 20px; box-sizing: border-box; }
#rightPanel { width: 70%; }
#chatBox { width: 100%; height: 300px; border: 1px solid #ccc; overflow-y: auto; padding: 10px; text-align: left; }
#messageInput { width: 80%; padding: 8px; margin-top: 10px; }
button { padding: 8px; cursor: pointer; }
#nameInput { width: 80%; padding: 8px; margin-top: 10px; }
#targetInput { width: 80%; padding: 8px; margin-top: 10px; }
.message { margin: 5px 0; }
.message.sent { text-align: right; }
.message.received { text-align: left; }
.message.sent p { background-color: #e1ffc7; padding: 8px; border-radius: 10px; max-width: 70%; display: inline-block; }
.message.received p { background-color: #f0f0f0; padding: 8px; border-radius: 10px; max-width: 70%; display: inline-block; }
</style>
</head>
<body>
<div id="leftPanel">
<h2>登录</h2>
<input type="text" id="nameInput" placeholder="输入昵称...">
<button id="sign">登陆</button>
<h2>接收人</h2>
<input type="text" id="targetInput" placeholder="输入对方昵称...">
</div>
<div id="rightPanel">
<h2>对话</h2>
<div id="chatBox"></div>
<input type="text" id="messageInput" placeholder="输入消息...">
<button id="sendBtn">发送好友消息</button>
</div>
<script>
let socket;
let myName;
// 监听“登陆”按钮点击
document.getElementById("sign").addEventListener("click", function () {
// 获取昵称输入框的值
myName = document.getElementById("nameInput").value.trim();
if (!myName) {
appendMessage("❌ 请输入昵称", "received");
return;
}
// 连接 WebSocket 服务器
socket = new WebSocket("ws://localhost:8001");
// 监听 WebSocket 错误
socket.onerror = function (error) {
console.error("WebSocket 连接错误: ", error);
appendMessage("连接服务器异常", "received");
};
// 监听 WebSocket 连接
socket.onopen = function () {
console.log("WebSocket 已连接");
// 将昵称包含在初始消息中
socket.send(JSON.stringify({
"code": 1001,
"name": myName
}));
};
// 监听收到的消息
socket.onmessage = function (event) {
const data = JSON.parse(event.data);
const message = data.message;
const sender = data.name || "服务器";
if (sender === myName) {
appendMessage("📝 " + sender + ": " + message, "sent");
} else {
appendMessage("💬 " + sender + ": " + message, "received");
}
};
// 监听 WebSocket 关闭
socket.onclose = function () {
appendMessage("❌ 连接已关闭", "received");
};
});
// 监听发送按钮点击
document.getElementById("sendBtn").addEventListener("click", function () {
sendMessage();
});
// 监听回车键发送消息
document.getElementById("messageInput").addEventListener("keypress", function (event) {
if (event.key === "Enter") {
sendMessage();
}
});
// 发送消息
function sendMessage() {
if (socket && socket.readyState === WebSocket.OPEN) {
const target = document.getElementById("targetInput").value.trim();
const input = document.getElementById("messageInput").value.trim();
if (!input) {
appendMessage("❌ 请输入消息", "received");
return;
}
console.log("发送消息:" + input);
const message = JSON.stringify({
"code": 1002,
"target": target,
"name": myName,
"type": 1,
"content": input
});
socket.send(message);
appendMessage("📝 " + myName + ": " + input, "sent");
input.value = "";
} else {
appendMessage("❌ 未连接到服务器,请先登录", "received");
}
}
// 在聊天框中追加消息
function appendMessage(text, type) {
const chatBox = document.getElementById("chatBox");
const messageElement = document.createElement("div");
messageElement.className = "message " + type;
const pElement = document.createElement("p");
pElement.textContent = text;
messageElement.appendChild(pElement);
chatBox.appendChild(messageElement);
chatBox.scrollTop = chatBox.scrollHeight; // 滚动到底部
}
</script>
</body>
</html>
- 测试
群聊
- CommandType加入类型
/**
* 加入群聊
*/
JOIN_GROUP(1003),
- ImServer新增一个群聊对象
public static final ChannelGroup GROUP = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
- 修改ChatHandler
public static void execute(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) {
try {
// 将接收到的文本消息解析为ChatMessage对象
ChatMessage chatMessage = JSON.parseObject(textWebSocketFrame.text(), ChatMessage.class);
// 根据消息类型进行处理
switch (MessageType.getMessageType(chatMessage.getType())) {
case PRIVATE -> {
//...
}
//加入群聊消息发送
case GROUP -> ImServer.GROUP.writeAndFlush(Result.success("群聊消息(" + chatMessage.getName() + "):" + chatMessage.getContent()));
default -> {
// ...
}
}
} catch (Exception e) {
// ...
}
}
- 加入群聊JoinFGroupHandler
import com.wzw.ImServer;
import com.wzw.Result;
import io.netty.channel.ChannelHandlerContext;
public class JoinFGroupHandler {
public static void execute(ChannelHandlerContext channelHandlerContext) {
ImServer.GROUP.add(channelHandlerContext.channel());
channelHandlerContext.channel().writeAndFlush(Result.success("加入群聊成功"));
}
}
- WebSocketHandler加入处理加入群聊
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
try {
Command command = JSON.parseObject(textWebSocketFrame.text(), Command.class);
switch (CommandType.getCommandType(command.getCode())){
case CONNECTION -> ConnectionHandler.execute(channelHandlerContext, command);
case CHAT -> ChatHandler.execute(channelHandlerContext, textWebSocketFrame);
//加入群聊处理
case JOIN_GROUP -> JoinFGroupHandler.execute(channelHandlerContext);
case DISCONNECT -> ConnectionHandler.disconnect(channelHandlerContext,command);
default -> channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
}
} catch (Exception e) {
channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
}
}
- 前端代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简单聊天页面</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; display: flex; }
#leftPanel { width: 30%; padding-right: 20px; box-sizing: border-box; }
#rightPanel { width: 70%; }
#chatBox { width: 100%; height: 300px; border: 1px solid #ccc; overflow-y: auto; padding: 10px; text-align: left; }
#messageInput { width: 80%; padding: 8px; margin-top: 10px; }
button { padding: 8px; cursor: pointer; }
#nameInput { width: 80%; padding: 8px; margin-top: 10px; }
#targetInput { width: 80%; padding: 8px; margin-top: 10px; }
.message { margin: 5px 0; }
.message.sent { text-align: right; }
.message.received { text-align: left; }
.message.sent p { background-color: #e1ffc7; padding: 8px; border-radius: 10px; max-width: 70%; display: inline-block; }
.message.received p { background-color: #f0f0f0; padding: 8px; border-radius: 10px; max-width: 70%; display: inline-block; }
</style>
</head>
<body>
<div id="leftPanel">
<h2>登录</h2>
<input type="text" id="nameInput" placeholder="输入昵称...">
<button id="sign">登陆</button>
<h2>接收人</h2>
<input type="text" id="targetInput" placeholder="输入对方昵称...">
</div>
<div id="rightPanel">
<h2>对话</h2>
<div id="chatBox"></div>
<input type="text" id="messageInput" placeholder="输入消息...">
<button id="sendPrivateBtn">发送好友消息</button>
<button id="sendGroupBtn">发送群消息</button>
</div>
<script>
let socket;
let myName;
// 监听“登陆”按钮点击
document.getElementById("sign").addEventListener("click", function () {
// 获取昵称输入框的值
myName = document.getElementById("nameInput").value.trim();
if (!myName) {
appendMessage("❌ 请输入昵称", "received");
return;
}
// 连接 WebSocket 服务器
socket = new WebSocket("ws://localhost:8001");
// 监听 WebSocket 错误
socket.onerror = function (error) {
console.error("WebSocket 连接错误: ", error);
appendMessage("连接服务器异常", "received");
};
// 监听 WebSocket 连接
socket.onopen = function () {
console.log("WebSocket 已连接");
// 上线
socket.send(JSON.stringify({
"code": 1001,
"name": myName
}));
// 加入群聊
socket.send(JSON.stringify({
"code": 1003,
"name": myName
}));
};
// 监听收到的消息
socket.onmessage = function (event) {
const data = JSON.parse(event.data);
const message = data.message;
const sender = data.name || "服务器";
if (sender === myName) {
appendMessage("📝 " + sender + ": " + message, "sent");
} else {
appendMessage("💬 " + sender + ": " + message, "received");
}
};
// 监听 WebSocket 关闭
socket.onclose = function () {
appendMessage("❌ 连接已关闭", "received");
};
});
// 监听发送按钮点击
document.getElementById("sendPrivateBtn").addEventListener("click", function () {
sendPrivateMessage();
});
// 监听发送按钮点击
document.getElementById("sendGroupBtn").addEventListener("click", function () {
sendGroupMessage();
});
// 监听回车键发送消息
document.getElementById("messageInput").addEventListener("keypress", function (event) {
if (event.key === "Enter") {
sendPrivateMessage();
}
});
// 发送私聊消息
function sendPrivateMessage() {
if (socket && socket.readyState === WebSocket.OPEN) {
const target = document.getElementById("targetInput").value.trim();
const input = document.getElementById("messageInput").value.trim();
if (!input) {
appendMessage("❌ 请输入消息", "received");
return;
}
console.log("发送消息:" + input);
const message = JSON.stringify({
"code": 1002,
"target": target,
"name": myName,
"type": 1,
"content": input
});
socket.send(message);
appendMessage("📝 " + myName + ": " + input, "sent");
input.value = "";
} else {
appendMessage("❌ 未连接到服务器,请先登录", "received");
}
}
// 发送群消息
function sendGroupMessage() {
if (socket && socket.readyState === WebSocket.OPEN) {
const target = document.getElementById("targetInput").value.trim();
const input = document.getElementById("messageInput").value.trim();
if (!input) {
appendMessage("❌ 请输入消息", "received");
return;
}
console.log("发送消息:" + input);
const message = JSON.stringify({
"code": 1002,
"target": target,
"name": myName,
"type": 2,
"content": input
});
socket.send(message);
appendMessage("📝 " + myName + ": " + input, "sent");
input.value = "";
} else {
appendMessage("❌ 未连接到服务器,请先登录", "received");
}
}
// 在聊天框中追加消息
function appendMessage(text, type) {
const chatBox = document.getElementById("chatBox");
const messageElement = document.createElement("div");
messageElement.className = "message " + type;
const pElement = document.createElement("p");
pElement.textContent = text;
messageElement.appendChild(pElement);
chatBox.appendChild(messageElement);
chatBox.scrollTop = chatBox.scrollHeight; // 滚动到底部
}
// 监听窗口关闭事件
window.addEventListener("beforeunload", function () {
if (socket && socket.readyState === WebSocket.OPEN) {
const message = JSON.stringify({
"code": 1000,
"name": myName
});
socket.send(message);
socket.close();
}
});
</script>
</body>
</html>
- 效果
离线
- 离线的代码实现
/**
* 断开
* @param channelHandlerContext
* @param command
*/
public static void disconnect(ChannelHandlerContext channelHandlerContext, Command command) {
ImServer.USERS.remove(command.getName());
channelHandlerContext.disconnect();
}
- 调用离线方法
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
try {
Command command = JSON.parseObject(textWebSocketFrame.text(), Command.class);
switch (CommandType.getCommandType(command.getCode())){
case CONNECTION -> ConnectionHandler.execute(channelHandlerContext, command);
case CHAT -> ChatHandler.execute(channelHandlerContext, textWebSocketFrame);
//离线
case DISCONNECT -> ConnectionHandler.disconnect(channelHandlerContext,command);
default -> channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
}
} catch (Exception e) {
channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
}
}
- 前端代码,关闭页面的时候,断开连接
// 监听窗口关闭事件
window.addEventListener("beforeunload", function () {
if (socket && socket.readyState === WebSocket.OPEN) {
const message = JSON.stringify({
"code": 1000,
"name": myName
});
socket.send(message);
socket.close();
}
});
SpringBoot+WebSocket+Html+jQuery实现客服聊天
已经实现客户功能,支持多人会话聊天、交互,如果需要保存聊天记录,自己实现,保存到数据库即可。
1. 目录结构
2. 配置类
- CorsConfig
package com.wzw.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 允许所有路径
.allowedOrigins("*") // 允许所有域
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的请求方法
.allowedHeaders("*") // 允许的请求头
.allowCredentials(false); // 不允许携带 Cookie
}
};
}
}
- RedisConfig
package com.wzw.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 配置 key 的序列化方式
template.setKeySerializer(new StringRedisSerializer());
// 配置 value 的序列化方式,这里使用 Jackson 来序列化对象为 JSON 格式
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// 同样地配置 Hash 类型的 key 和 value 的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
// 初始化序列化配置
template.afterPropertiesSet();
return template;
}
}
- WebSocketConfig
package com.wzw.config; // 包声明,定义配置类所在的包路径
import com.wzw.handler.ChatWebSocketHandler; // 导入处理WebSocket消息的处理器类
import org.springframework.context.annotation.Configuration; // Spring配置类注解
import org.springframework.web.socket.config.annotation.EnableWebSocket; // 启用WebSocket支持
import org.springframework.web.socket.config.annotation.WebSocketConfigurer; // WebSocket配置接口
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; // WebSocket处理器注册类
@Configuration // 声明这是一个Spring配置类
@EnableWebSocket // 启用WebSocket功能
public class WebSocketConfig implements WebSocketConfigurer { // 实现WebSocket配置接口
private final ChatWebSocketHandler chatWebSocketHandler; // 注入处理WebSocket消息的处理器实例
// 构造函数,通过依赖注入获取ChatWebSocketHandler实例
public WebSocketConfig(ChatWebSocketHandler chatWebSocketHandler) {
this.chatWebSocketHandler = chatWebSocketHandler;
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 注册WebSocket处理器,配置连接路径和跨域设置
registry
.addHandler(chatWebSocketHandler, "/chat") // 将处理器绑定到路径 "/chat"
.setAllowedOrigins("*"); // 允许所有来源的跨域请求(生产环境建议限制具体域名)
}
}
3. 实体类、service、controller
- ChatMessage
package com.wzw.entity;
import lombok.Data;
@Data
public class ChatMessage {
/**
* 类型
* session 连接成功
* join 加入会话
*/
private String type;
private String sessionId;
/**
* 发送者
*/
private String from;
/**
* 发送者昵称
*/
private String fromName;
/**
* 接收者
*/
private String to;
/**
* 消息内容
*/
private String message;
}
- ChatService
package com.wzw.service;
import com.alibaba.fastjson2.JSON;
import com.wzw.entity.ChatMessage;
import com.wzw.handler.ChatWebSocketHandler;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
@Service
public class ChatService {
/**
* 所有会话
* @return
*/
public Map<String, Object> chatAll(String sessionId) {
Map<String, Object> map = new HashMap<>();
ChatWebSocketHandler.chatSessions.forEach(
(key, value) -> {
List<String> users=new ArrayList<>();
if(!key.equals(sessionId)){
value.forEach((k, v) -> {
users.add(ChatWebSocketHandler.getUserIdFromSession(v));
});
map.put(key, users);
}
}
);
return map;
}
/**
* 加入会话
* @param chatMessage
* @return
*/
public Map<String, Object> join(ChatMessage chatMessage) {
Map<String, Map<String, WebSocketSession>> chatSessions = ChatWebSocketHandler.chatSessions;
Map<String,Object> result=new HashMap<>();
chatSessions.forEach(
(key, value) -> {
if (key.equals(chatMessage.getTo())) {
//找到要加入的目标会话
Map<String, WebSocketSession> map = new HashMap<>(chatSessions.get(chatMessage.getTo()));
//找到来源会话信息
Map<String, WebSocketSession> map1 = new HashMap<>(chatSessions.get(chatMessage.getFrom()));
AtomicReference<String> user= new AtomicReference<>("");
map.forEach((k, v) -> {
user.set(ChatWebSocketHandler.getUserIdFromSession(v));
try {
chatMessage.setFromName("系统消息");
chatMessage.setSessionId(chatMessage.getTo());
chatMessage.setType("join");
chatMessage.setMessage(ChatWebSocketHandler.getUserIdFromSession(map1.get(chatMessage.getFrom()))+"加入会话");
v.sendMessage(new TextMessage(JSON.toJSONString(chatMessage)));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
//将来源会话加入到要加入的目标会话中
map.putAll(map1);
chatSessions.put(key, map);
result.put("1","加入群聊成功");
result.put("to",user);
}
}
);
return CollectionUtils.isEmpty(result)?Map.of("-1","加入群聊失败"):result;
}
/**
* 断开会话
* @param chatMessage
* @return
*/
public Map<String, Object> disconnect(ChatMessage chatMessage) {
Map<String, Map<String, WebSocketSession>> chatSessions = ChatWebSocketHandler.chatSessions;
Map<String,Object> result = new HashMap<>();
// 遍历外层Map
chatSessions.forEach((key, value) -> {
if (key.equals(chatMessage.getTo())) {
value.forEach((k, v) -> {
// 创建可变副本进行操作
Map<String, WebSocketSession> mutableMap = new HashMap<>(value);
mutableMap.remove(chatMessage.getFrom()); // 安全删除
// 更新原始Map(需同步处理)
chatSessions.put(key, mutableMap);
result.put("1", "成功断开会话: " + chatMessage.getFrom());
});
}
});
return CollectionUtils.isEmpty(result) ?
Collections.singletonMap("-1", "断开会话失败") : result;
}
}
- ChatController
package com.wzw.controller;
import com.wzw.entity.ChatMessage;
import com.wzw.service.ChatService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/chat")
public class ChatController {
@Resource
private ChatService chatService;
/**
* 所有会话
* @param sessionId
* @return
*/
@GetMapping("/list")
public Map<String, Object> chat(String sessionId) {
return chatService.chatAll(sessionId);
}
/**
* 加入会话
* @param chatMessage
* @return
*/
@PostMapping("/join")
public Map<String, Object> join(@RequestBody ChatMessage chatMessage) {
return chatService.join(chatMessage);
}
/**
* 退出会话
* @param chatMessage
* @return
*/
@PostMapping("/disconnect")
public Map<String, Object> disconnect(@RequestBody ChatMessage chatMessage) {
return chatService.disconnect(chatMessage);
}
}
4. ChatWebSocketHandler消息处理
package com.wzw.handler; // 定义包路径
import com.alibaba.fastjson2.JSON; // JSON序列化/反序列化工具
import com.wzw.entity.ChatMessage; // 聊天消息实体类
import com.wzw.util.RedisUtil; // Redis工具类(用于缓存操作)
import io.netty.channel.group.ChannelGroup; // Netty通道组(用于管理WebSocket连接)
import io.netty.channel.group.DefaultChannelGroup; // 默认通道组实现
import io.netty.util.concurrent.GlobalEventExecutor; // Netty全局事件执行器
import jakarta.annotation.Resource; // Spring依赖注入注解
import lombok.extern.slf4j.Slf4j; // 日志记录工具
import org.springframework.stereotype.Component; // Spring组件注解
import org.springframework.web.socket.*; // WebSocket相关接口和类
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Slf4j // 启用日志记录
@Component // 声明为Spring组件
public class ChatWebSocketHandler implements WebSocketHandler { // 实现WebSocket处理器接口
// 存储所有会话的静态Map:外层Key为会话ID,内层Key为用户Session ID
public static Map<String, Map<String, WebSocketSession>> chatSessions = new HashMap<>();
@Resource // Spring依赖注入Redis工具类
private RedisUtil redisUtil;
// WebSocket连接建立时触发
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 从会话中获取用户ID(通过URL参数)
String userId = getUserIdFromSession(session);
// 将当前会话存入chatSessions(外层Key为session.getId())
chatSessions.put(session.getId(), Map.of(session.getId(), session));
// 构建系统消息通知用户连接成功
ChatMessage chatMessage = new ChatMessage();
chatMessage.setFromName("系统消息");
chatMessage.setMessage("连接成功");
chatMessage.setFrom(userId);
chatMessage.setSessionId(session.getId());
chatMessage.setType("session");
// 将消息发送给当前用户
session.sendMessage(new TextMessage(JSON.toJSONString(chatMessage)));
// 将用户ID存入Redis(示例用途,可能用于会话关联)
redisUtil.set("a", userId);
log.info("用户连接: {}", userId);
}
// 处理会话消息
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
// 获取原始消息内容(JSON字符串)
String payload = message.getPayload().toString();
// 将JSON反序列化为ChatMessage对象
ChatMessage chatMessage = JSON.parseObject(payload, ChatMessage.class);
log.info("收到消息: {}", payload);
// 根据消息目标会话ID获取目标会话集合
Map<String, WebSocketSession> map = chatSessions.get(chatMessage.getTo());
// 遍历目标会话中的所有用户
map.forEach((key, value) -> {
if (value.isOpen()) { // 检查会话是否处于打开状态
try {
// 将消息广播给目标会话的所有用户
value.sendMessage(new TextMessage(payload));
} catch (IOException e) {
log.error("发送消息失败", e);
}
}
});
}
// 处理WebSocket传输错误
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
log.error("WebSocket错误", exception);
}
// WebSocket连接关闭时触发
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
// 从chatSessions中移除已关闭的会话
chatSessions.remove(session.getId());
log.info("用户断开连接: {}", session.getId());
}
// 是否支持分片消息(通常返回false)
@Override
public boolean supportsPartialMessages() {
return false;
}
// 从WebSocket会话中提取用户ID(通过URL参数)
public static String getUserIdFromSession(WebSocketSession session) {
String query = session.getUri().getQuery(); // 获取URL查询参数
if (query != null && query.contains("userId=")) {
return query.split("userId=")[1]; // 提取userId参数值
}
return "anonymous-" + session.getId(); // 若未找到参数,生成匿名ID
}
}
5.前端页面
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客服界面</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f9;
margin: 0;
padding: 20px;
display: flex;
}
.left-container, .right-container {
flex: 1;
padding: 10px;
}
.left-container {
margin-right: 20px;
}
h2 {
color: #333;
}
#chatBox {
border: 1px solid #ccc;
width: 100%;
height: 300px;
overflow-y: auto;
background-color: #fff;
padding: 10px;
border-radius: 8px;
margin-bottom: 10px;
}
#messageInput {
width: calc(100% - 110px);
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
margin-right: 10px;
}
#sendButton {
padding: 10px 20px;
border: none;
background-color: #007bff;
color: #fff;
border-radius: 4px;
cursor: pointer;
}
#sendButton:hover {
background-color: #0056b3;
}
.message {
margin: 10px 0;
padding: 0 6px;
border-radius: 10px;
max-width: 70%;
word-wrap: break-word;
clear: both; /* 确保每条消息独占一行 */
}
.message.sent {
background-color: #a9f3a9;
color: #000000;
float: right;
}
.message.received {
background-color: #ccc;
color: black;
float: left;
}
.center-message {
display: block;
text-align: center; /* 确保水平居中 */
margin: 10px 0;
padding: 0 6px;
border-radius: 10px;
max-width: 100%;
word-wrap: break-word;
background-color: #f0f0f0;
color: #333;
clear: both; /* 确保系统消息和加入聊天提示独占一行 */
}
.input-container {
display: flex;
width: 100%;
}
#messageInput {
flex: 1;
}
#chatList {
border: 1px solid #ccc;
background-color: #fff;
padding: 10px;
border-radius: 8px;
height: 200px;
overflow-y: auto;
}
#chatList table {
width: 100%;
border-collapse: collapse;
}
#chatList th, #chatList td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
#chatList th {
background-color: #f2f2f2;
}
#chatList button {
padding: 5px 10px;
border: none;
background-color: #007bff;
color: #fff;
border-radius: 4px;
cursor: pointer;
}
#chatList button:hover {
background-color: #0056b3;
}
</style>
<!-- 引入jQuery库 -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div class="left-container">
<button id="fetchChatListButton">获取聊天列表</button>
<div id="chatList">
<table>
<thead>
<tr>
<th>序号</th>
<th>描述</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<!-- 表格行将动态插入到这里 -->
</tbody>
</table>
</div>
</div>
<div class="right-container">
<h2>客服聊天</h2>
<p>你的 ID: <span id="userId"></span></p>
<div id="chatBox"></div>
<div class="input-container">
<input type="text" id="messageInput" placeholder="输入消息">
<button id="sendButton">发送</button>
</div>
</div>
<script>
$(document).ready(function() {
const userId = "agent-" + Math.random().toString(36).substring(7);
let sessionId = null;
let toSessionId = null;
$("#userId").text(userId);
const socket = new WebSocket("ws://localhost:8080/chat?userId=" + userId);
socket.onopen = function () {
console.log("WebSocket 已连接");
};
socket.onmessage = function (event) {
const data = JSON.parse(event.data);
if(data.type==="session"){
sessionId=data.sessionId;
}
if(data.from!==sessionId){
addMessageToChatBox(data.fromName, data.message, "received");
}
};
socket.onerror = function (error) {
console.error("WebSocket 发生错误:", error);
};
$("#sendButton").click(function() {
const message = $("#messageInput").val();
const toUserId = $("#toUserId").val();
const chatMessage = {
from: sessionId,
to: toSessionId,
fromName: userId,
message: message
};
console.log(chatMessage);
socket.send(JSON.stringify(chatMessage));
addMessageToChatBox(userId, message, "sent");
$("#messageInput").val("");
});
$("#fetchChatListButton").click(function() {
$.ajax({
url: 'http://127.0.0.1:8080/chat/list?sessionId='+sessionId,
method: 'GET',
success: function(data) {
const chatList = $("#chatList tbody");
chatList.empty(); // 清空表格
let index = 1;
for (const [key, value] of Object.entries(data)) {
const tr = $(`
<tr>
<td>${index}</td>
<td>${value}</td>
<td><button class="joinButton" data-key="${key}">加入</button></td>
</tr>
`);
toSessionId=key;
chatList.append(tr);
index++;
}
},
error: function(error) {
console.error("获取聊天列表失败:", error);
}
});
});
$(document).on("click", ".joinButton", function() {
const key = $(this).data("key");
$.ajax({
url: 'http://127.0.0.1:8080/chat/join',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
to: key,
from: sessionId
}),
success: function(response) {
console.log("加入聊天成功:", response);
addMessageToChatBox("系统消息", `已成功加入 ${response.to} 聊天`, "center");
},
error: function(error) {
console.error("加入聊天失败:", error);
addMessageToChatBox("系统消息", "加入聊天失败,请重试", "center");
}
});
});
// 新增函数:添加消息到聊天框
function addMessageToChatBox(fromName, message, type) {
const messageElement = $(`<div class="message ${type}"><p><b>${fromName}:</b> ${message}</p></div>`);
$("#chatBox").append(messageElement);
$("#chatBox").scrollTop($("#chatBox")[0].scrollHeight);
}
});
</script>
</body>
</html>
6.效果
会话列表:只要访问页面,就会创建一个会话。目前需要手动刷新,如果有人加入,描述中会出现此会话,显示所有参与人的昵称。
加入会话:只能加入一个会话,如果已经加入过其它会话,会断开上一个加入的会话,然后加入新的会话中。
聊天框:绿色是自己发出的消息,左边灰色是收到的消息,中间是系统消息
如果有人加入了你当前的会话,会出现提示,直接发送消息,加入会话中的人都可以看到
代码链接
所有代码链接:https://gitee.com/w452339689/im.git