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

news2025/3/21 3:34:52

文章目录

  • 核心组件
    • 网络通信层
    • 事件调度层
    • 服务编排层
  • 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();
            // 设置EventLoopGroup
            bootstrap.group(boss, worker)
                    // 设置通道类型为NioServerSocketChannel
                    .channel(NioServerSocketChannel.class)
                    // 设置子处理器,用于处理新连接的初始化
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            // 在这里可以添加各种ChannelHandler来处理消息
                        }
                    });

            // 绑定服务器到指定端口,并同步等待成功
            ChannelFuture future = bootstrap.bind(8001).sync();
            // 等待服务器socket关闭
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 优雅地关闭EventLoopGroup
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

  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();
            // 设置EventLoopGroup
            bootstrap.group(boss, worker)
                    // 设置通道类型为NioServerSocketChannel
                    .channel(NioServerSocketChannel.class)
                    // 设置子处理器,用于处理新连接的初始化
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            //用于HTTP请求和响应的编解码
                            pipeline.addLast(new HttpServerCodec())
                                    //支持分块写入大文件或流式数据
                                    .addLast(new ChunkedWriteHandler())
                                    //将多个HTTP消息片段合并成一个完整的HTTP消息,最大聚合大小为64KB。
                                    .addLast(new HttpObjectAggregator(1024*64))
                                    // 添加WebSocket协议处理器,用于处理WebSocket握手和协议升级
                                    .addLast(new WebSocketServerProtocolHandler("/"))
                                    // 添加自定义的WebSocket业务逻辑处理器,用于处理WebSocket消息的接收和发送。
                                    .addLast(new WebSocketHandler());

                        }
                    });

            // 绑定服务器到指定端口,并同步等待成功
            ChannelFuture future = bootstrap.bind(8001).sync();
            // 等待服务器socket关闭
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 优雅地关闭EventLoopGroup
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}
  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
     */
    @Override
    protected 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
     */
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
        try {
        	//收到的消息转为Command对象
            Command command = JSON.parseObject(textWebSocketFrame.text(), Command.class);
            //判断消息是不是连接登陆
            switch (CommandType.getCommandType(command.getCode())){
                case CONNECTION -> ConnectionHandler.execute(channelHandlerContext, command);
                default -> channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
            }
        } catch (Exception e) {
            channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
        }
    }
}
  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;
                    }

                    // 获取目标用户的Channel
                    Channel channel = USERS.get(chatMessage.getTarget());

                    // 如果目标用户不在线,发送失败消息
                    if (channel == null) {
                        channelHandlerContext.channel().writeAndFlush(Result.fail("消息发送失败,用户" + chatMessage.getTarget() + "不在线"));
                    } else {
                        // 目标用户在线,发送私聊消息
                        channel.writeAndFlush(Result.success("私聊消息(" + chatMessage.getName() + "):" + chatMessage.getContent()));
                    }
                }
                default -> {
                    // 如果消息类型不支持,发送失败消息
                    channelHandlerContext.channel().writeAndFlush(Result.fail("消息发送失败,不支持的消息类型"));
                }
            }
        } catch (Exception e) {
            // 捕获并处理解析消息时的异常,发送格式错误消息
            channelHandlerContext.channel().writeAndFlush(Result.fail("消息格式错误"));
        }
    }
}
  1. WebSocketHandler中新增一个聊天消息处理调用
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
        try {
            Command command = JSON.parseObject(textWebSocketFrame.text(), Command.class);
            switch (CommandType.getCommandType(command.getCode())){
                case CONNECTION -> ConnectionHandler.execute(channelHandlerContext, command);
                //聊天消息处理
                case CHAT -> ChatHandler.execute(channelHandlerContext, textWebSocketFrame);
                default -> channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
            }
        } catch (Exception e) {
            channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
        }
    }
  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加入处理加入群聊
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
        try {
            Command command = JSON.parseObject(textWebSocketFrame.text(), Command.class);
            switch (CommandType.getCommandType(command.getCode())){
                case CONNECTION -> ConnectionHandler.execute(channelHandlerContext, command);
                case CHAT -> ChatHandler.execute(channelHandlerContext, textWebSocketFrame);
                //加入群聊处理
                case JOIN_GROUP -> JoinFGroupHandler.execute(channelHandlerContext);
                case DISCONNECT -> ConnectionHandler.disconnect(channelHandlerContext,command);
                default -> channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
            }
        } catch (Exception e) {
            channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
        }
    }
  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. 调用离线方法
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
        try {
            Command command = JSON.parseObject(textWebSocketFrame.text(), Command.class);
            switch (CommandType.getCommandType(command.getCode())){
                case CONNECTION -> ConnectionHandler.execute(channelHandlerContext, command);
                case CHAT -> ChatHandler.execute(channelHandlerContext, textWebSocketFrame);
                //离线
                case DISCONNECT -> ConnectionHandler.disconnect(channelHandlerContext,command);
                default -> channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
            }
        } catch (Exception e) {
            channelHandlerContext.channel().writeAndFlush(Result.fail("未知指令"));
        }
    }
  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 {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")  // 允许所有路径
                        .allowedOrigins("*")  // 允许所有域
                        .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")  // 允许的请求方法
                        .allowedHeaders("*")  // 允许的请求头
                        .allowCredentials(false);  // 不允许携带 Cookie
            }
        };
    }
}

  • RedisConfig
package com.wzw.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {


    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        // 配置 key 的序列化方式
        template.setKeySerializer(new StringRedisSerializer());

        // 配置 value 的序列化方式,这里使用 Jackson 来序列化对象为 JSON 格式
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        // 同样地配置 Hash 类型的 key 和 value 的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        // 初始化序列化配置
        template.afterPropertiesSet();

        return template;
    }

}
  • WebSocketConfig
package com.wzw.config;  // 包声明,定义配置类所在的包路径

import com.wzw.handler.ChatWebSocketHandler;  // 导入处理WebSocket消息的处理器类
import org.springframework.context.annotation.Configuration;  // Spring配置类注解
import org.springframework.web.socket.config.annotation.EnableWebSocket;  // 启用WebSocket支持
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;  // WebSocket配置接口
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;  // WebSocket处理器注册类

@Configuration  // 声明这是一个Spring配置类
@EnableWebSocket  // 启用WebSocket功能
public class WebSocketConfig implements WebSocketConfigurer {  // 实现WebSocket配置接口

    private final ChatWebSocketHandler chatWebSocketHandler;  // 注入处理WebSocket消息的处理器实例

    // 构造函数,通过依赖注入获取ChatWebSocketHandler实例
    public WebSocketConfig(ChatWebSocketHandler chatWebSocketHandler) {
        this.chatWebSocketHandler = chatWebSocketHandler;
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 注册WebSocket处理器,配置连接路径和跨域设置
        registry
            .addHandler(chatWebSocketHandler, "/chat")  // 将处理器绑定到路径 "/chat"
            .setAllowedOrigins("*");  // 允许所有来源的跨域请求(生产环境建议限制具体域名)
    }
}

3. 实体类、service、controller

  • ChatMessage
package com.wzw.entity;

import lombok.Data;

@Data
public class ChatMessage {
    /**
     * 类型
     *      session 连接成功
     *      join    加入会话
     */
    private String type;
    private String sessionId;
    /**
     * 发送者
     */
    private String from;
    /**
     * 发送者昵称
     */
    private String fromName;
    /**
     * 接收者
     */
    private String to;
    /**
     * 消息内容
     */
    private String message;
}

  • ChatService
package com.wzw.service;

import com.alibaba.fastjson2.JSON;
import com.wzw.entity.ChatMessage;
import com.wzw.handler.ChatWebSocketHandler;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;

@Service
public class ChatService {

    /**
     * 所有会话
     * @return
     */
    public Map<String, Object> chatAll(String sessionId) {

        Map<String, Object> map = new HashMap<>();

        ChatWebSocketHandler.chatSessions.forEach(
                (key, value) -> {
                    List<String> users=new ArrayList<>();

                    if(!key.equals(sessionId)){
                        value.forEach((k, v) -> {
                            users.add(ChatWebSocketHandler.getUserIdFromSession(v));
                        });

                        map.put(key, users);
                    }
                }
        );

        return map;
    }

    /**
     * 加入会话
     * @param chatMessage
     * @return
     */
    public Map<String, Object> join(ChatMessage chatMessage) {
        Map<String, Map<String, WebSocketSession>> chatSessions = ChatWebSocketHandler.chatSessions;

        Map<String,Object> result=new HashMap<>();

        chatSessions.forEach(
                (key, value) -> {
                    if (key.equals(chatMessage.getTo())) {
                        //找到要加入的目标会话
                        Map<String, WebSocketSession> map = new HashMap<>(chatSessions.get(chatMessage.getTo()));
                        //找到来源会话信息
                        Map<String, WebSocketSession> map1 = new HashMap<>(chatSessions.get(chatMessage.getFrom()));

                        AtomicReference<String> user= new AtomicReference<>("");
                        map.forEach((k, v) -> {
                            user.set(ChatWebSocketHandler.getUserIdFromSession(v));
                            try {
                                chatMessage.setFromName("系统消息");
                                chatMessage.setSessionId(chatMessage.getTo());
                                chatMessage.setType("join");
                                chatMessage.setMessage(ChatWebSocketHandler.getUserIdFromSession(map1.get(chatMessage.getFrom()))+"加入会话");
                                v.sendMessage(new TextMessage(JSON.toJSONString(chatMessage)));
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            }
                        });
                        //将来源会话加入到要加入的目标会话中
                        map.putAll(map1);

                        chatSessions.put(key, map);
                        result.put("1","加入群聊成功");
                        result.put("to",user);

                    }
                }
        );

        return CollectionUtils.isEmpty(result)?Map.of("-1","加入群聊失败"):result;
    }

    /**
     * 断开会话
     * @param chatMessage
     * @return
     */
    public Map<String, Object> disconnect(ChatMessage chatMessage) {
        Map<String, Map<String, WebSocketSession>> chatSessions = ChatWebSocketHandler.chatSessions;
        Map<String,Object> result = new HashMap<>();

        // 遍历外层Map
        chatSessions.forEach((key, value) -> {
            if (key.equals(chatMessage.getTo())) {
                value.forEach((k, v) -> {
                    // 创建可变副本进行操作
                    Map<String, WebSocketSession> mutableMap = new HashMap<>(value);
                    mutableMap.remove(chatMessage.getFrom()); // 安全删除

                    // 更新原始Map(需同步处理)
                    chatSessions.put(key, mutableMap);
                    result.put("1", "成功断开会话: " + chatMessage.getFrom());
                });
            }
        });

        return CollectionUtils.isEmpty(result) ?
                Collections.singletonMap("-1", "断开会话失败") : result;
    }

}

  • ChatController
package com.wzw.controller;

import com.wzw.entity.ChatMessage;
import com.wzw.service.ChatService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/chat")
public class ChatController {

    @Resource
    private ChatService chatService;

    /**
     * 所有会话
     * @param sessionId
     * @return
     */
    @GetMapping("/list")
    public Map<String, Object> chat(String sessionId) {
        return chatService.chatAll(sessionId);
    }

    /**
     * 加入会话
     * @param chatMessage
     * @return
     */
    @PostMapping("/join")
    public Map<String, Object> join(@RequestBody ChatMessage chatMessage) {
        return chatService.join(chatMessage);
    }

    /**
     * 退出会话
     * @param chatMessage
     * @return
     */
    @PostMapping("/disconnect")
    public Map<String, Object> disconnect(@RequestBody ChatMessage chatMessage) {
        return chatService.disconnect(chatMessage);
    }

}

4. ChatWebSocketHandler消息处理

package com.wzw.handler;  // 定义包路径

import com.alibaba.fastjson2.JSON;  // JSON序列化/反序列化工具
import com.wzw.entity.ChatMessage;  // 聊天消息实体类
import com.wzw.util.RedisUtil;  // Redis工具类(用于缓存操作)
import io.netty.channel.group.ChannelGroup;  // Netty通道组(用于管理WebSocket连接)
import io.netty.channel.group.DefaultChannelGroup;  // 默认通道组实现
import io.netty.util.concurrent.GlobalEventExecutor;  // Netty全局事件执行器
import jakarta.annotation.Resource;  // Spring依赖注入注解
import lombok.extern.slf4j.Slf4j;  // 日志记录工具
import org.springframework.stereotype.Component;  // Spring组件注解
import org.springframework.web.socket.*;  // WebSocket相关接口和类
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Slf4j  // 启用日志记录
@Component  // 声明为Spring组件
public class ChatWebSocketHandler implements WebSocketHandler {  // 实现WebSocket处理器接口

    // 存储所有会话的静态Map:外层Key为会话ID,内层Key为用户Session ID
    public static Map<String, Map<String, WebSocketSession>> chatSessions = new HashMap<>();

    @Resource  // Spring依赖注入Redis工具类
    private RedisUtil redisUtil;

    // WebSocket连接建立时触发
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 从会话中获取用户ID(通过URL参数)
        String userId = getUserIdFromSession(session);

        // 将当前会话存入chatSessions(外层Key为session.getId())
        chatSessions.put(session.getId(), Map.of(session.getId(), session));

        // 构建系统消息通知用户连接成功
        ChatMessage chatMessage = new ChatMessage();
        chatMessage.setFromName("系统消息");
        chatMessage.setMessage("连接成功");
        chatMessage.setFrom(userId);
        chatMessage.setSessionId(session.getId());
        chatMessage.setType("session");

        // 将消息发送给当前用户
        session.sendMessage(new TextMessage(JSON.toJSONString(chatMessage)));

        // 将用户ID存入Redis(示例用途,可能用于会话关联)
        redisUtil.set("a", userId);
        log.info("用户连接: {}", userId);
    }

    // 处理会话消息
    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        // 获取原始消息内容(JSON字符串)
        String payload = message.getPayload().toString();

        // 将JSON反序列化为ChatMessage对象
        ChatMessage chatMessage = JSON.parseObject(payload, ChatMessage.class);

        log.info("收到消息: {}", payload);

        // 根据消息目标会话ID获取目标会话集合
        Map<String, WebSocketSession> map = chatSessions.get(chatMessage.getTo());

        // 遍历目标会话中的所有用户
        map.forEach((key, value) -> {
            if (value.isOpen()) {  // 检查会话是否处于打开状态
                try {
                    // 将消息广播给目标会话的所有用户
                    value.sendMessage(new TextMessage(payload));
                } catch (IOException e) {
                    log.error("发送消息失败", e);
                }
            }
        });
    }

    // 处理WebSocket传输错误
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) {
        log.error("WebSocket错误", exception);
    }

    // WebSocket连接关闭时触发
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        // 从chatSessions中移除已关闭的会话
        chatSessions.remove(session.getId());
        log.info("用户断开连接: {}", session.getId());
    }

    // 是否支持分片消息(通常返回false)
    @Override
    public boolean supportsPartialMessages() {
        return false;
    }

    // 从WebSocket会话中提取用户ID(通过URL参数)
    public static String getUserIdFromSession(WebSocketSession session) {
        String query = session.getUri().getQuery();  // 获取URL查询参数
        if (query != null && query.contains("userId=")) {
            return query.split("userId=")[1];  // 提取userId参数值
        }
        return "anonymous-" + session.getId();  // 若未找到参数,生成匿名ID
    }
}

5.前端页面

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>客服界面</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f9;
            margin: 0;
            padding: 20px;
            display: flex;
        }
        .left-container, .right-container {
            flex: 1;
            padding: 10px;
        }
        .left-container {
            margin-right: 20px;
        }
        h2 {
            color: #333;
        }
        #chatBox {
            border: 1px solid #ccc;
            width: 100%;
            height: 300px;
            overflow-y: auto;
            background-color: #fff;
            padding: 10px;
            border-radius: 8px;
            margin-bottom: 10px;
        }
        #messageInput {
            width: calc(100% - 110px);
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
            margin-right: 10px;
        }
        #sendButton {
            padding: 10px 20px;
            border: none;
            background-color: #007bff;
            color: #fff;
            border-radius: 4px;
            cursor: pointer;
        }
        #sendButton:hover {
            background-color: #0056b3;
        }
        .message {
            margin: 10px 0;
            padding: 0 6px;
            border-radius: 10px;
            max-width: 70%;
            word-wrap: break-word;
            clear: both; /* 确保每条消息独占一行 */
        }
        .message.sent {
            background-color: #a9f3a9;
            color: #000000;
            float: right;
        }
        .message.received {
            background-color: #ccc;
            color: black;
            float: left;
        }
        .center-message {
            display: block;
            text-align: center; /* 确保水平居中 */
            margin: 10px 0;
            padding: 0 6px;
            border-radius: 10px;
            max-width: 100%;
            word-wrap: break-word;
            background-color: #f0f0f0;
            color: #333;
            clear: both; /* 确保系统消息和加入聊天提示独占一行 */
        }
        .input-container {
            display: flex;
            width: 100%;
        }
        #messageInput {
            flex: 1;
        }
        #chatList {
            border: 1px solid #ccc;
            background-color: #fff;
            padding: 10px;
            border-radius: 8px;
            height: 200px;
            overflow-y: auto;
        }
        #chatList table {
            width: 100%;
            border-collapse: collapse;
        }
        #chatList th, #chatList td {
            border: 1px solid #ddd;
            padding: 8px;
            text-align: left;
        }
        #chatList th {
            background-color: #f2f2f2;
        }
        #chatList button {
            padding: 5px 10px;
            border: none;
            background-color: #007bff;
            color: #fff;
            border-radius: 4px;
            cursor: pointer;
        }
        #chatList button:hover {
            background-color: #0056b3;
        }
    </style>
    <!-- 引入jQuery库 -->
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div class="left-container">
    <button id="fetchChatListButton">获取聊天列表</button>
    <div id="chatList">
        <table>
            <thead>
            <tr>
                <th>序号</th>
                <th>描述</th>
                <th>操作</th>
            </tr>
            </thead>
            <tbody>
            <!-- 表格行将动态插入到这里 -->
            </tbody>
        </table>
    </div>
</div>
<div class="right-container">
    <h2>客服聊天</h2>
    <p>你的 ID: <span id="userId"></span></p>

    <div id="chatBox"></div>

    <div class="input-container">
        <input type="text" id="messageInput" placeholder="输入消息">
        <button id="sendButton">发送</button>
    </div>
</div>

<script>
    $(document).ready(function() {
        const userId = "agent-" + Math.random().toString(36).substring(7);
        let sessionId = null;
        let toSessionId = null;
        $("#userId").text(userId);

        const socket = new WebSocket("ws://localhost:8080/chat?userId=" + userId);

        socket.onopen = function () {
            console.log("WebSocket 已连接");
        };

        socket.onmessage = function (event) {
            const data = JSON.parse(event.data);
            if(data.type==="session"){
                sessionId=data.sessionId;
            }
            if(data.from!==sessionId){
                addMessageToChatBox(data.fromName, data.message, "received");
            }
        };

        socket.onerror = function (error) {
            console.error("WebSocket 发生错误:", error);
        };

        $("#sendButton").click(function() {
            const message = $("#messageInput").val();
            const toUserId = $("#toUserId").val();

            const chatMessage = {
                from: sessionId,
                to: toSessionId,
                fromName: userId,
                message: message
            };

            console.log(chatMessage);

            socket.send(JSON.stringify(chatMessage));

            addMessageToChatBox(userId, message, "sent");

            $("#messageInput").val("");
        });

        $("#fetchChatListButton").click(function() {
            $.ajax({
                url: 'http://127.0.0.1:8080/chat/list?sessionId='+sessionId,
                method: 'GET',
                success: function(data) {
                    const chatList = $("#chatList tbody");
                    chatList.empty(); // 清空表格
                    let index = 1;
                    for (const [key, value] of Object.entries(data)) {
                        const tr = $(`
                            <tr>
                                <td>${index}</td>
                                <td>${value}</td>
                                <td><button class="joinButton" data-key="${key}">加入</button></td>
                            </tr>
                        `);
                        toSessionId=key;
                        chatList.append(tr);
                        index++;
                    }
                },
                error: function(error) {
                    console.error("获取聊天列表失败:", error);
                }
            });
        });

        $(document).on("click", ".joinButton", function() {
            const key = $(this).data("key");
            $.ajax({
                url: 'http://127.0.0.1:8080/chat/join',
                method: 'POST',
                contentType: 'application/json',
                data: JSON.stringify({
                    to: key,
                    from: sessionId
                }),
                success: function(response) {
                    console.log("加入聊天成功:", response);
                    addMessageToChatBox("系统消息", `已成功加入 ${response.to} 聊天`, "center");
                },
                error: function(error) {
                    console.error("加入聊天失败:", error);
                    addMessageToChatBox("系统消息", "加入聊天失败,请重试", "center");
                }
            });
        });

        // 新增函数:添加消息到聊天框
        function addMessageToChatBox(fromName, message, type) {
            const messageElement = $(`<div class="message ${type}"><p><b>${fromName}:</b> ${message}</p></div>`);
            $("#chatBox").append(messageElement);
            $("#chatBox").scrollTop($("#chatBox")[0].scrollHeight);
        }
    });
</script>
</body>
</html>

6.效果

在这里插入图片描述

会话列表:只要访问页面,就会创建一个会话。目前需要手动刷新,如果有人加入,描述中会出现此会话,显示所有参与人的昵称。
加入会话:只能加入一个会话,如果已经加入过其它会话,会断开上一个加入的会话,然后加入新的会话中。
聊天框:绿色是自己发出的消息,左边灰色是收到的消息,中间是系统消息
在这里插入图片描述
如果有人加入了你当前的会话,会出现提示,直接发送消息,加入会话中的人都可以看到
在这里插入图片描述

代码链接

所有代码链接:https://gitee.com/w452339689/im.git

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2318739.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

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

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

【LangChain入门 1】安装

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

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;​…

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

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

每日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;从而促进全局/…

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

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

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

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

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)测…

以太网 MAC 帧格式

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

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

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

从WebRTC到嵌入式:EasyRTC如何借助大模型提升音视频通信体验

随着人工智能技术的快速发展&#xff0c;WebRTC与大模型的结合正在为音视频通信领域带来革命性的变革。WebRTC作为一种开源实时通信技术&#xff0c;以其低延迟、跨平台兼容性和强大的音视频处理能力&#xff0c;成为智能硬件和物联网设备的重要技术支撑。 而EasyRTC作为基于W…

前端样式库推广——TailwindCss

官方网址&#xff1a; https://tailwindcss.com/docs/installation/using-vite 中文官方文档&#xff1a;https://www.tailwindcss.cn/ github地址&#xff1a;tailwindcss 正在使用tailwindcss的网站&#xff1a;https://tailwindcss.com/showcase 一看github&#xff0c;竟然…

SpringBoot 第二课(Ⅰ) 整合springmvc(详解)

目录 一、SpringBoot对静态资源的映射规则 1. WebJars 资源访问 2. 静态资源访问 3. 欢迎页配置 二、SpringBoot整合springmvc 概述 Spring MVC组件的自动配置 中央转发器&#xff08;DispatcherServlet&#xff09; 控制器&#xff08;Controller&#xff09; 视图解…

OpenHarmony 开源鸿蒙北向开发——3.配置SDK

安装、配置完成之后我们就要配置SDK。 我们创建工程后&#xff0c;点击右上角设置 进入设置 进入OpenHarmony SDK&#xff0c;选择编辑 这里配置一下SDK安装位置 点击完成 这里我们API版本勾选第一个即可 确认安装 勾选接受 这里要等一会 安装完成后&#xff0c;点击完成

vulhub Matrix-Breakout

1.下载靶机&#xff0c;打开靶机和kali虚拟机 2.查询kali和靶机ip 3.浏览器访问 访问81端口有登陆界面 4.扫描敏感目录 kali dirb 扫描 一一访问 robot.txt提示我们继续找找&#xff0c;可能是因为我们的字典太小了&#xff0c;我们换个扫描器换个字典试下,利用kali自带的最大…