当前位置: 首页 > article >正文

Java+Html实现前后端客服聊天

文章目录

  • 核心组件
    • 网络通信层
    • 事件调度层
    • 服务编排层
  • 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实现客服聊天

  1. 新建一个maven项目
  2. 引入依赖
        <dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.99.Final</version></dependency>
  1. 启动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();// 设置EventLoopGroupbootstrap.group(boss, worker)// 设置通道类型为NioServerSocketChannel.channel(NioServerSocketChannel.class)// 设置子处理器,用于处理新连接的初始化.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {// 在这里可以添加各种ChannelHandler来处理消息}});// 绑定服务器到指定端口,并同步等待成功ChannelFuture future = bootstrap.bind(8001).sync();// 等待服务器socket关闭future.channel().closeFuture().sync();} catch (InterruptedException e) {e.printStackTrace();} finally {// 优雅地关闭EventLoopGroupboss.shutdownGracefully();worker.shutdownGracefully();}}
}
  1. 启动
public class ImApplication {public static void main(String[] args) {ImServer.start();}}

启动成功
在这里插入图片描述

技术方案对比

技术说明优点缺点适用场景
WebSocket双向持久连接,服务器和客户端可以随时发送消息低延迟、实时性强、双向通信需要浏览器支持,可能被防火墙拦截客服聊天、游戏、协作编辑
轮询(Polling)客户端定期请求服务器获取新消息兼容性好,所有浏览器支持占用带宽,高并发时服务器压力大简单场景,低频率更新的聊天
长轮询(Long Polling)客户端请求后,服务器等待新消息再返回比普通轮询节省带宽服务器压力仍然较大稍微实时的聊天应用
SSE(Server-Sent Events)服务器单向推送消息到客户端轻量级、兼容 HTTP/2仅支持服务器向客户端推送客服系统中的通知功能

WebScoket建立连接

  1. 设置消息处理器
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();// 设置EventLoopGroupbootstrap.group(boss, worker)// 设置通道类型为NioServerSocketChannel.channel(NioServerSocketChannel.class)// 设置子处理器,用于处理新连接的初始化.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected 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 {// 优雅地关闭EventLoopGroupboss.shutdownGracefully();worker.shutdownGracefully();}}
}
  1. 消息处理的实现
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*/@Overrideprotected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {System.out.println(textWebSocketFrame.text());}
}
  1. 前端页面
<!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>
  1. 启动测试
    在这里插入图片描述

用户上线

  1. 定义一个实体,用来接收消息
import lombok.Data;@Data
public class Command {private Integer code;private String name;}
  1. 定义一个枚举,用来区分消息类型
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;}
}
  1. ImServer定义一个map,用来存储登陆的用户
public static final ConcurrentHashMap<String, Channel> USERS = new ConcurrentHashMap<>(1024);
  1. 添加一个登陆处理的实现类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()));}
}
  1. 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*/@Overrideprotected 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("未知指令"));}}
}
  1. 前端代码
<!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>
  1. 测试上线
    在这里插入图片描述

实现指定用户私聊

  1. 创建消息对象,用来接收发送消息
import lombok.Data;@Data
public class ChatMessage extends Command {/*** 消息类型*/private Integer type;/*** 接收人*/private String target;/*** 消息内容*/private String content;
}
  1. CommandType补充一个消息类型,代表发送消息
    /*** 登陆连接*/CONNECTION(1001),/*** 消息*/CHAT(1002),/*** 错误*/ERROR(-1);
  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;}
}
  1. 消息处理类
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;}// 获取目标用户的ChannelChannel 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("消息格式错误"));}}
}
  1. WebSocketHandler中新增一个聊天消息处理调用
    @Overrideprotected 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("未知指令"));}}
  1. 前端代码
<!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>
  1. 测试
    在这里插入图片描述

群聊

  1. CommandType加入类型
    /*** 加入群聊*/JOIN_GROUP(1003),
  1. ImServer新增一个群聊对象
public static final ChannelGroup GROUP = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
  1. 修改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) {// ...}}
  1. 加入群聊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("加入群聊成功"));}
}
  1. WebSocketHandler加入处理加入群聊
    @Overrideprotected 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("未知指令"));}}
  1. 前端代码
<!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>
  1. 效果
    在这里插入图片描述

离线

  1. 离线的代码实现
    /*** 断开* @param channelHandlerContext* @param command*/public static void disconnect(ChannelHandlerContext channelHandlerContext, Command command) {ImServer.USERS.remove(command.getName());channelHandlerContext.disconnect();}
  1. 调用离线方法
    @Overrideprotected 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("未知指令"));}}
  1. 前端代码,关闭页面的时候,断开连接
    // 监听窗口关闭事件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 {@Beanpublic WebMvcConfigurer corsConfigurer() {return new WebMvcConfigurer() {@Overridepublic 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 {@Beanpublic 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;}@Overridepublic 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<>();// 遍历外层MapchatSessions.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 {@Resourceprivate 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 IDpublic static Map<String, Map<String, WebSocketSession>> chatSessions = new HashMap<>();@Resource  // Spring依赖注入Redis工具类private RedisUtil redisUtil;// WebSocket连接建立时触发@Overridepublic 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);}// 处理会话消息@Overridepublic 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传输错误@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) {log.error("WebSocket错误", exception);}// WebSocket连接关闭时触发@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) {// 从chatSessions中移除已关闭的会话chatSessions.remove(session.getId());log.info("用户断开连接: {}", session.getId());}// 是否支持分片消息(通常返回false)@Overridepublic 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

相关文章:

Java+Html实现前后端客服聊天

文章目录 核心组件网络通信层事件调度层服务编排层 Spring实现客服聊天技术方案对比WebScoket建立连接用户上线实现指定用户私聊群聊离线 SpringBootWebSocketHtmljQuery实现客服聊天1. 目录结构2. 配置类3. 实体类、service、controller4. ChatWebSocketHandler消息处理5.前端…...

实用工具-Another Redis Desktop Manager介绍

GitHub&#xff1a;https://github.com/qishibo/AnotherRedisDesktopManager/releases Gitee&#xff1a;AnotherRedisDesktopManager 发行版 - Gitee.com Another Redis Desktop Manager 是一款免费的 Redis 可视化管理工具&#xff0c;具有以下特点和功能&#xff1a; 特…...

MySQL如何存储表情符号?

存储表情符号 默认mysql的字符集是utf8&#xff0c;排序规则为 utf8_general_ci INSERT INTO department (name) VALUES (&#x1f604;)在存储表情的时候会报 1366 - Incorrect string value: \xF0\x9F\x98\x84 for column name at row 1, Time: 0.007000s 这时需要修改字符…...

解锁 DeepSeek 安全接入、稳定运行新路径

背景 目前&#xff0c;和 DeepSeek 相关的需求总结为两类&#xff1a; 因官方 APP/Web 服务经常无法返回结果&#xff0c;各类云厂商、硬件或软件企业提供满血版或蒸馏版的 API 算力服务&#xff0c;还有不少基于开源家用计算和存储设备的本地部署方案&#xff0c;以分担 De…...

Spring Boot 配置属性 (Configuration Properties) 详解:优雅地管理应用配置

引言 Spring Boot 的 配置属性 (Configuration Properties) 是其另一个核心特性&#xff0c;它提供了一种 类型安全、结构化 的方式来管理应用的配置信息。 与自动配置相辅相成&#xff0c;配置属性允许开发者 以声明式的方式将外部配置 (如 properties 文件、YAML 文件、环境…...

【LangChain入门 1】安装

文章目录 一、安装LangChain二、安装Ollama三、Ollama下载DeepSeekR1-7b模型 本学习系列以Ollama推理后端作为大语言模型&#xff0c;展开对LangChain框架的入门学习。 模型采用deepseek-r1:7b。 毕竟是免费开源的&#xff0c;下载过程耐心等待即可。 如果可以连接外网&#x…...

HTML中required与aria required区别

在HTML中&#xff0c;required和aria-required"true"都用于标识表单字段为必填项&#xff0c;但它们的作用和适用场景有所不同&#xff1a; 1. required 属性 • 功能属性&#xff1a;属于HTML5原生属性&#xff0c;直接控制表单验证逻辑。 • 作用&#xff1a; • …...

IvorySQL 增量备份与合并增量备份功能解析

1. 概述 IvorySQL v4 引入了块级增量备份和增量备份合并功能&#xff0c;旨在优化数据库备份与恢复流程。通过 pg_basebackup 工具支持增量备份&#xff0c;显著降低了存储需求和备份时间。同时&#xff0c;pg_combinebackup 工具能够将多个增量备份合并为单个完整备份&#x…...

【css酷炫效果】纯CSS实现故障文字特效

【css酷炫效果】纯CSS实现故障文字特效 缘创作背景html结构css样式完整代码基础版进阶版(3D效果) 效果图 想直接拿走的老板&#xff0c;链接放在这里&#xff1a;https://download.csdn.net/download/u011561335/90492053 缘 创作随缘&#xff0c;不定时更新。 创作背景 刚…...

SpringSecurity配置(自定义认证过滤器)

文末有本篇文章的项目源码文件可供下载学习 在这个案例中,我们已经实现了自定义登录URI的操作,登录成功之后,我们再次访问后端中的API的时候要在请求头中携带token,此时的token是jwt字符串,我们需要将该jwt字符串进行解析,查看解析后的User对象是否处于登录状态.登录状态下,将…...

设计模式(行为型)-备忘录模式

目录 定义 类图 角色 角色详解 &#xff08;一&#xff09;发起人角色&#xff08;Originator&#xff09;​ &#xff08;二&#xff09;备忘录角色&#xff08;Memento&#xff09;​ &#xff08;三&#xff09;备忘录管理员角色&#xff08;Caretaker&#xff09;​…...

WebAssembly 技术在逆向爬虫中的应用研究

一、引言 1.1 Web 技术发展与性能需求 在当今数字化浪潮中,Web 应用已成为人们生活和工作中不可或缺的一部分。从简单的静态网页到功能复杂的单页面应用(SPA),Web 技术的发展日新月异。随着用户对 Web 应用交互性、实时性和复杂性的要求不断提高,传统的 Web 开发技术面临着…...

Advanced Intelligent Systems 软体机器手助力截肢者玩转鼠标

随着科技的不断进步&#xff0c;假肢技术在改善截肢者生活质量方面取得了显著成就。然而&#xff0c;截肢群体在就业方面仍面临巨大困难&#xff0c;适龄截肢群体的就业率仅为健全群体的一半。现有的肌电控制假肢手在与计算机交互时存在诸多挑战&#xff0c;特别是截肢者在使用…...

pyhton中 字典 元组 列表 集合之间的互相转换

在 Python 中,集合(set)、字典(dict)、元组(tuple)、列表(list)和序列(如字符串 str)之间可以互相转换。以下是它们之间转换的详细方法,涵盖从基础到高级的用法。 1. 列表(list)与其他类型的转换 1.1 列表 → 集合 my_list = [1, 2, 2, 3...

每日Attention学习27——Patch-based Graph Reasoning

模块出处 [NC 25] [link] Graph-based context learning network for infrared small target detection 模块名称 Patch-based Graph Reasoning (PGR) 模块结构 模块特点 使用图结构更好的捕捉特征的全局上下文将图结构与特征切片(Patching)相结合&#xff0c;从而促进全局/…...

理一理Mysql日期

在 MySQL 数据库中&#xff0c;关于日期和时间的类型主要有以下几种&#xff1a; 1. **DATE**: 仅存储日期部分&#xff0c;格式为 YYYY-MM-DD&#xff0c;例如 2023-10-31。 2. **TIME**: 仅存储时间部分&#xff0c;格式为 HH:MM:SS&#xff0c;例如 14:30:00。 3. **DATE…...

数据结构:栈的应用举例——括号匹配的检验

2. 括号匹配的检验 如果表达式中包含括号&#xff0c;当程序中含有这类表达式时&#xff0c;在代码编译过程中&#xff0c;必然会检查括号是否匹配&#xff0c;这是一项必需的语法检查环节。 &#xff08;1&#xff09;迭代版 此处假设表达式中只含有左、右圆括号&#xff0…...

DeepSeek成功的秘诀:谈谈DeepSeek的算法创新

李升伟 整理 DeepSeek 是一家专注于人工智能技术研发的公司&#xff0c;其算法创新在业界引起了广泛关注。以下是 DeepSeek 使用的核心算法及其特点的详细解析&#xff1a; 1. 原生稀疏注意力&#xff08;NSA&#xff09;算法 DeepSeek 提出的 原生稀疏注意力&#xff08;Na…...

初始OpenCV

OpenCV 是一个功能强大、应用广泛的计算机视觉库,它为开发人员提供了丰富的工具和算法,可以帮助他们快速构建各种视觉应用。随着计算机视觉技术的不断发展,OpenCV 也将会继续发挥重要的作用。 OpenCV 提供了大量的计算机视觉算法和图像处理工具,广泛应用于图像和视频的处理…...

深圳南柯电子|医疗设备EMC检测测试整改:保障患者安全的第一步

在医疗设备领域&#xff0c;电磁兼容性&#xff08;EMC&#xff09;是确保设备安全、有效运行的关键指标。随着医疗技术的飞速发展&#xff0c;医疗设备日益复杂&#xff0c;其电磁环境也愈发复杂多变。EMC检测测试及整改因此成为医疗设备研发、生产、销售过程中不可或缺的一环…...

【笔记】计算机网络——数据链路层

概述 链路是从一个结点到相邻结点的物理路线&#xff0c;数据链路则是在链路的基础上增加了一些必要的硬件和软件实现 数据链路层位于物理层和网络层之间&#xff0c;它的核心任务是在直接相连的节点&#xff08;如相邻的交换机&#xff0c;路由器&#xff09;之间提供可靠且…...

Rust语言介绍和猜数字游戏的实现

文章目录 Rust语言介绍和猜数字游戏的实现cargo是什么使用Rust编写猜数字 Rust语言介绍和猜数字游戏的实现 Rust语言是一种系统编程语言&#xff0c;核心强调安全性、并发性以及高性能&#xff0c;由类似于C/C的底层控制能力&#xff0c;性能也非常接近&#xff0c;Rust有一些…...

STM32-汇编

学习arm汇编的主要目的是为了编写arm启动代码&#xff0c;启动代码启动以后&#xff0c;引导程序到c语言环境下运行。换句话说启动代码的目的是为了在处理器复位以后搭建c语言最基本的需求。因此启动代码的主要任务有&#xff1a; 初始化异常向量表&#xff1b; 初始化各工作模…...

利用通义灵码AI在VS Code中快速开发扫雷游戏:Qwen2.5-Max模型的应用实例

引言 随着人工智能技术的不断进步&#xff0c;开发过程中的自动化程度也在逐步提高。阿里云推出的通义灵码AI程序员&#xff0c;作为一款创新型的智能编程助手&#xff0c;现已全面上线并兼容VS Code、JetBrains IDEs等多种开发环境。本文将介绍如何利用最新的Qwen2.5-Max模型…...

202503执行jmeter压测数据库(ScyllaDB,redis,lindorm,Mysql)

一、Mysql 1 、 准备MySQL 连接内容 2 、 下载连接jar包 准备 mysql-connector-java-5.1.49.jar 放到 D:\apache-jmeter-5.6.3\lib\ext 目录下面; 3 、 启动jmeter ,配置脚本 添加线程组---》JDBC Connection Configuration---》JDBC Request---》查看结果树。 1)测…...

【QT 多线程示例】两种多线程实现方式

文章目录 多线程实现方式一&#xff1a;继承QThread类方式二&#xff1a; 使用QObject::moveToThread()方法 多线程实现 在Qt中&#xff0c;实现多线程编程有两种常见的方式&#xff0c;它们分别是通过继承QThread类和使用QObject::moveToThread()方法。 方式一&#xff1a;继…...

excel文件有两列,循环读取文件两列赋值到字典列表。字典的有两个key,分别为question和answer。将最终结果追加到json文件

import pandas as pd import json import osdef excel_to_json_append(excel_path, json_path):# 1. 读取Excel数据到字典列表df pd.read_excel(excel_path, usecols["question", "answer"])new_data [{"question": str(row["question&qu…...

以太网 MAC 帧格式

文章目录 以太网 MAC 帧格式以太网帧间隔参考 本文为笔者学习以太网对网上资料归纳整理所做的笔记&#xff0c;文末均附有参考链接&#xff0c;如侵权&#xff0c;请联系删除。 以太网 MAC 帧格式 以太网技术的正式标准是 IEEE 802.3&#xff0c;它规定了以太网传输数据的帧结…...

【PCB工艺】基础:电子元器件

电子原理图&#xff08;Schematic Diagram&#xff09;是电路设计的基础&#xff0c;理解电子元器件和集成电路&#xff08;IC&#xff09;的作用&#xff0c;是画好原理图的关键。 本专栏将系统讲解 电子元器件分类、常见 IC、电路设计技巧&#xff0c;帮助你快速掌握电子电路…...

docker 部署elk 设置账号密码

1. 先把 kibana 停掉 2.进入es 容器 docker exec -it 75895a078cbc /bin/bash 找到 bin 目录 执行 ./elasticsearch-setup-passwords interactive 全部设置一样的密码 ,不一样自己要记住&#xff0c;设置成功会输出如下内容 Changed password for user [apm_system] Chang…...