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实现客服聊天
- 新建一个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();// 设置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();}}
}
- 启动
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();// 设置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();}}
}
- 消息处理的实现
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());}
}
- 前端页面
<!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*/@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("未知指令"));}}
}
- 前端代码
<!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;}// 获取目标用户的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("消息格式错误"));}}
}
- 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("未知指令"));}}
- 前端代码
<!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加入处理加入群聊
@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("未知指令"));}}
- 前端代码
<!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();}
- 调用离线方法
@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("未知指令"));}}
- 前端代码,关闭页面的时候,断开连接
// 监听窗口关闭事件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.前端…...
基于Spring Boot的冷链物流系统的设计与实现的设计与实现(LW+源码+讲解)
专注于大学生项目实战开发,讲解,毕业答疑辅导,欢迎高校老师/同行前辈交流合作✌。 技术范围:SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:…...
《线程池:Linux平台编译线程池动态库发生的死锁问题》
关于如何编译动态库可以移步《Linux:动态库动态链接与静态库静态链接》-CSDN博客 我们写的线程池代码是闭源的,未来想提供给别人使用,只需要提供so库和头文件即可。 系统默认库文件路径为: usr/lib usr/loacl/lib 系统默认头文件…...
鸿蒙NEXT项目实战-百得知识库03
代码仓地址,大家记得点个star IbestKnowTeach: 百得知识库基于鸿蒙NEXT稳定版实现的一款企业级开发项目案例。 本案例涉及到多个鸿蒙相关技术知识点: 1、布局 2、配置文件 3、组件的封装和使用 4、路由的使用 5、请求响应拦截器的封装 6、位置服务 7、三…...
sql server数据迁移,springboot搭建开发环境遇到的问题及解决方案
最近搭建springboot项目开发环境,数据库连的是sql server,遇到许多问题在此记录一下。 1、sql server安装教程 参考:https://www.bilibili.com/opus/944736210624970769 2、sql server导出、导入数据库 参考:https://blog.csd…...
Sensodrive机器人力控关节模组SensoJoint在海洋垃圾清理机器人中的拓展应用
海洋污染已成为全球性的环境挑战,其中海底垃圾的清理尤为困难。据研究,海洋中约有2600万至6600万吨垃圾,超过90%沉积在海底。传统上,潜水员收集海底垃圾不仅成本高昂,而且充满风险。为解决这一问题,欧盟资助…...
matrix-breakout-2-morpheus 靶机----练习攻略 【仅获取shell】
【此练习仅做到反弹shell】 1.靶机下载地址 https://download.vulnhub.com/matrix-breakout/matrix-breakout-2-morpheus.ova 2. 打开靶机,kali使用nmap扫描同C段的主机 找到靶机ip 确保靶机和kali网卡均为NAT模式 先查看kali的ip nmap 192.168.182.1/24 …...
吴恩达机器学习笔记复盘(八)多元线性回归的梯度下降
简介 梯度下降是多元线性回归的主流优化方法,具有普适性和可扩展性,而标准方程法适用于特定场景。实际应用中需结合特征工程和参数调优提升模型性能。本篇不复盘参数调优。 1.多元线性回归模型 多元线性回归模型假设因变量 与多个自变量 之间存在线性…...
SAP-ABAP: 采购申请创建(PR)BAPI_PR_CREATE 技术指南-详解
BAPI_PR_CREATE 技术指南 用途:通过 RFC 接口创建 SAP 采购申请(PR),支持自动化集成与批量处理。 一、功能概览 类别说明核心功能创建标准采购申请、预留转采购申请,支持多行项目及账户分配。集成场景与 MRP 系统、外…...
Python:单继承方法的重写
继承:让类和类之间转变为父子关系,子类默认继承父类的属性和方法 单继承: class Person:def eat(self):print("eat")def sing(self):print("sing") class Girl(Person):pass#占位符,代码里面类下面不写任何东…...
Cursor解锁Claude Max,助力AI编程新突破!
Cursor 最新推出的 Claude Max 模型,以其卓越的性能和创新的能力,正在重新定义我们对 AI 辅助编程的认知。这款搭载 Claude3.7 大脑的超级模型,不仅具备超强智能,还凭借一系列技术突破,向传统 AI 编程工具发起了挑战。…...
Datawhale coze-ai-assistant 笔记4
课程地址: 第 6 章 应用 - 飞书云文档https://zxdwhda-share.feishu.cn/wiki/Gi9aw4EDTiXxcekUWebcEtmUnb4 应用 AI…...
【基于深度学习的验证码识别】---- part3数据加载、模型等API介绍(2)
四、模型 模型的定义 在机器学习和深度学习中,模型 可以定义为: 一个数学函数或算法,能够从输入数据中提取特征并生成输出。通过训练过程,模型能够学习数据中的规律(如分类、回归、聚类等)。训练完成后&…...
留 言 板
书单 作者:郦波 《五百年来王阳明》 《郦波评说曾国藩家训》《最是人间留不住》《一天一生》 作者:曾仕强 《论语的生活智慧》 《曾仕强详解道德经 道经》 作者: [何勇,陈晓峰著] 《Greenplum企业应用实战 》 作者 Mark G. Sobel…...
3.18练习
BUUCTF练习day2 [GXYCTF2019]Ping Ping Ping ping题有点熟悉的命令注入,使用 127.0.0.1;ls可以得到当前目录的文件名 cat一下flag.php,发现不行 过滤了空格,那我们有什么方法绕过呢 <(重定向符号)${IFS}$IFS$9%09(tab键) 试一试发现过…...
Redis哨兵模式-黑马学习笔记
redis哨兵模式 1.哨兵主要的作用 用于监控主节点的健康情况 如果主节点挂掉立马选择一个从节点顶上去 2.监控的机制 如何监控呢? sentinel基于心跳机制检测主节点的状态 每个1秒向集群发送ping (三体中逻辑用来威胁三体人的心跳检测) 如…...
Linkreate wordpressAI插件 24小时自动生成原创图文,新增从百度、必应搜索引擎自动获取相关下拉关键词
Linkreate wordpressAI插件核心功能亮点 文章生成与优化 自动化文章生成:利用 AI 技术,根据关键词生成高质量文章。 支持指定长度和要求,异步生成不阻塞操作。 且 AI 可自动生成精准的 tag 标签,利于 SEO 优化。 批量生成文章…...
《AI赋能云原生区块链,引领供应链溯源革新》
在数字化浪潮席卷全球的当下,供应链管理领域正经历着深刻变革。云原生区块链凭借其去中心化、不可篡改等特性,为供应链溯源带来了前所未有的透明度与可靠性。而AI的融入,更如虎添翼,以强大的智能分析和决策能力,为云原…...
基于单片机的多功能热水器设计(论文+源码)
1系统方案设计 基于单片机的多功能热水器系统,其系统框图如图2.1所示。主要采用了DS18B20温度传感器,HC-SR04超声波模块,STC89C52单片机,液晶,继电器等来构成整个系统。硬件上主要通过温度传感器进行水温的检测&am…...
Java面试黄金宝典4
1. 什么是泛型 ?与 T 的区别 原理 泛型是 Java 编程语言中的一个强大特性,它提供了编译时类型安全检查机制,允许在定义类、接口和方法时使用类型参数。这些类型参数在使用时会被具体的类型所替代,从而实现代码的复用和类型安全。泛…...
vim在连续多行行首插入相同的字符
工作中经常需要用vim注释掉一段代码或者json文件中的一部分,需要在多行前面插入//或者#符号。在 Vim 中,在连续多行行首插入相同字符主要有以下两种方法: Visual Block 模式插入 将光标移到要插入相同内容的第一行的行首24。按下Ctrl v进入…...
路径问题(greedy):地下城游戏
题目描述: 恶魔们抓住了公主并将她关在了地下城 dungeon 的 右下角 。地下城是由 m x n 个房间组成的二维网格。我们英勇的骑士最初被安置在 左上角 的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。 骑士的初始健康点数为一个正整数。如果他的健…...
【论文阅读】CARES:医学视觉语言模型可信度的综合基准
CARES:医学视觉语言模型可信度的综合基准 1. 研究背景与动机2. 方法论创新3. 核心实验结果4. 贡献与意义5.总结 CARES: A Comprehensive Benchmark of Trustworthiness in Medical Vision Language Models CARES:医学视觉语言模型可信度的综合基准 Accep…...
Qt窗口坐标体系
坐标系:以左上角为原点(0,0),X向右增加,Y向下增加 对于嵌套窗口,其坐标是相对于父窗口来说的 例如: 通过move方法实现...
英伟达消费级RTX显卡配置表
显卡型号显存大小显存频率显存位宽显存带宽CUDA核心数TDP(功耗)上市年份RTX 409024GB21 Gbps384-bit1,008 GB/s16,384450W2022RTX 4080 (16GB)16GB22.4 Gbps256-bit716.8 GB/s9,728320W2022RTX 4080 (12GB)12GB21 Gbps192-bit504 GB/s7,680285W2023RTX 4…...
1.5.3 掌握Scala内建控制结构 - for循环
Scala的for循环功能强大,支持单重和嵌套循环。单重for循环语法为for (变量 <- 集合或数组 (条件)) {语句组},可选筛选条件,循环变量依次取集合值。支持多种任务,如输出指定范围整数(使用Range、to、until࿰…...
10、STL中的unordered_map使用方法
一、了解 1、unordered_map(哈希) unordered_map是借用哈希表实现的关联容器。 访问键值对O(1),最坏情况O(n),例如哈希冲突严重时。【n是一个哈希桶的元素数量】 unordered_map特性 键值对存储ÿ…...
pycharm-python國際象棋遊戲代碼
嗯,用户的问题是“pycharm寫關於python國際象棋遊戲代碼”,也就是要用PyCharm来写一个Python的国际象棋游戏代码。我需要先整理一下用户提供的搜索结果,看看有什么相关的信息可以利用。 首先看搜索结果中的各个网页内容。网页1主要讲的是象棋…...
Linux下Redis哨兵集群模式搭建(1主2从+3哨兵)
Linux下Redis哨兵集群模式搭建(1主2从3哨兵) 一、Redis哨兵模式搭建 1.安装包下载 链接: https://pan.baidu.com/s/1_n2rCMi5MHX-mVkkyMo4LA 提取码: gbra 2.新建redis目录 mkdir -p /app/redis3.解压到/app/redis目录下 tar -zxvf redis-6.2.16.ta…...
二.使用ffmpeg对原始音频数据重采样并进行AAC编码
重采样:将音频三元组【采样率 采样格式 通道数】之中的任何一个或者多个值改变。 一.为什么要进行重采样? 1.原始音频数据和编码器的数据格式不一致 2.播放器要求的和获取的数据不一致 3.方便运算 二.本次编码流程 1.了解自己本机麦克风参数&#x…...
【初学者】请介绍一下指针分析(Pointer Analysis)?
李升伟 整理 指针分析(Pointer Analysis) 指针分析(Pointer Analysis)是一种静态程序分析技术,用于确定程序中指针可能指向的内存位置或对象。它是编译器优化、程序验证、漏洞检测和并行化等领域的重要基础。 1. 指…...
【程序人生】成功人生架构图(分层模型)
文章目录 ⭐前言⭐一、根基层——价值观与使命⭐二、支柱层——健康与能量⭐三、驱动层——学习与进化⭐四、网络层——关系系统⭐五、目标层——成就与财富⭐六、顶层——意义与传承⭐外层:调节环——平衡与抗风险⭐思维导图 标题详情作者JosieBook头衔CSDN博客专家…...
目标检测20年(一)
今天看的文献是《Object Detection in 20 Years: A Survey》,非常经典的一篇目标检测文献,希望通过这篇文章学习到目标检测的基础方法并提供一些创新思想。 论文链接:1905.05055 目录 一、摘要 1.1 原文 1.2 翻译 二、介绍 三、目标检测…...
SQLMesh系列教程:SQLMesh虚拟数据环境
各种工具都已将软件工程实践引入到数据工程中,但仍有差距存在,尤其是在测试和工作流等领域。SQLMesh 的目标是在这些领域开辟新的天地,解决像 dbt 这样的竞争产品尚未提供强大解决方案的难题。在这篇文章中,我将对 SQLMesh 进行简…...
【python小游戏】扫雷
扫雷小游戏代码。供消遣娱乐: import tkinter as tk from tkinter import messagebox import random# 游戏参数(中等难度:15x15 网格,40 颗雷) ROWS 15 COLS 15 MINES 40 CELL_SIZE 30 COLORS {default: #CCCCCC…...
【Linux】learning notes(4)cat、more、less、head、tail、vi、vim
文章目录 catmore 查看整个文件less 查看整个文件head 查看部分文件tail 查看部分文件vim / vi cat cat 命令在 Linux 和 Unix 系统中非常常用,它用于连接文件并打印到标准输出设备(通常是屏幕)。虽然 cat 的基本用法很简单,但它…...
【论文阅读】Adversarial Patch Attacks on Monocular Depth Estimation Networks
一、背景 单目深度估计是CV领域一个比较热门的研究方向,但是现有的方法过度依赖于非深度特征,使得单目深度估计的模型容易受到外界的攻击,针对这一问题该论文设计了一种攻击贴图的方法,用于攻击深度学习实现的单目深度估计模型。…...
基于Flask的自闭症患者诊断辅助系统:助力自闭症诊断
基于Flask的自闭症患者诊断辅助系统:助力自闭症诊断的创新方案 在当今社会,自闭症的早期准确诊断对于患者的治疗和康复至关重要。作为项目的第一作者,我致力于开发一款基于Web的自闭症诊断辅助系统,为这一领域贡献一份力量。 本…...
SqlServer Sql学习随笔
环境 SqlServerSSMSC# 查询 --查询来自数据库[MyTestDb]的[dbo]的表[testTable]前1000条数据--dbo 代表 数据库所有者(Database Owner),在 SQL Server 里,它是一个模式(Schema)。 --**模式(Sc…...
【6】组合计数学习笔记
前言 关于今天发现自己连快速幂都忘记怎么写这件事 这篇博客是组合计数基础,由于大部分内容都是 6 6 6 级,所以我就给整个提高级的组合数学评了 6 6 6 级。 组合计数基础 加法原理与乘法原理 加法原理(分类计数原理)&#…...
功能安全实战系列06-英飞凌Tricore系列SMU详解
本文框架 前言1.What?1.1SMU特性及架构1.1.1 SMU_core和SMU_stdby1.1.2 Flip-Flop机制1.1.3 RT Alarm (RecoveryTime)1.2 Alarm状态机1.3 FSP1.4 Alarm handing1.4.1 SMU_core Alarm handing1.4.2 SMU_Standby Alarm handing1.5 寄存器介绍2.How?2.1 如何排查SMU问题前言 在…...
Python 中的集合的中高级用法
Python 中的集合(set)是一种无序且不重复的数据结构,适用于去重、成员检测和集合运算等场景。以下是集合的中级和高级用法,涵盖从基础到高级的详细操作。 1. 集合的创建与初始化 1.1 创建集合 # 空集合 empty_set = set()# 直接初始化 my_set = {1, 2,...
opencv初步学习——图像处理2
这一部分主要讲解如何初步地创建一个图像,以及彩色图像我们的一些基本处理方法 一、创建一个灰度图像 1-1、zeros()函数 [NumPy库] 要用到这一个函数,首先我们需要调用我们的NumPy库,这一个函数的作用是可以帮助我们生成一个元素值都是0的二…...
传统服务部署、虚拟化部署与云原生部署资源消耗对比与优化指南
1. 三种部署方式概述 1.1 传统服务部署 定义:直接运行于物理服务器或基础Linux操作系统环境,无虚拟化层隔离 特点: 资源独占(CPU/内存/磁盘) 部署流程简单但扩展困难 典型场景:单一业务高负载场景&…...
使用htool工具导出和导入Excel表
htool官网 代码中用到的hool包里面的excel工具ExcelUtil 1. 引入依赖 <!-- Java的工具类 --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.25</version></dependency>&l…...
【Linux内核】从文件层面理解socket建立的方式(优雅的C风格多态)
内核层面理解 Socket 的创建和连接 引言 众所周知,Linux 下一切皆文件。无论是普通文件(如 file.txt),还是特殊文件(包括网络套接字),我们都可以以处理文件的方式来访问它们。网络套接字&…...
WebSocket:开启实时通信的新篇章
在当今的互联网应用中,实时交互已经成为不可或缺的一部分。无论是实时的在线聊天、股票行情更新,还是多人在线游戏,都需要一种高效的双向通信机制。而这正是 WebSocket 的用武之地。 本文将带你深入了解 WebSocket,探索其工作原理…...
只是“更轻更薄”?不!遨游三防平板还选择“更强更韧”
当消费电子领域普遍追求“更轻更薄”的设计美学时,遨游三防平板不止于此,还选择了另一条道路——“更强更韧”。在智能制造的复杂场景中,三防平板需直面高温、油污、撞击与极端气候的考验。普通消费级平板因防护性能不足,常因环境…...
C++ 各种map对比
文章目录 特点比较1. std::map2. std::unordered_map3. std::multimap4. std::unordered_multimap5. hash_map(SGI STL 扩展) C 示例代码代码解释 特点比较 1. std::map 底层实现:基于红黑树(一种自平衡的二叉搜索树)…...
《量子门与AI神经元:计算世界的奇妙碰撞》
在当今科技飞速发展的时代,量子计算和人工智能作为前沿领域,正不断颠覆我们对计算和智能的认知。量子门操作和AI中的神经元计算过程,分别作为这两大领域的核心机制,看似处于不同维度,却有着千丝万缕的联系,…...