Administrator
2025-08-14 c1bb3dd69c4d68f3f0787b59881ca258707b8cea
feat(websocket): 添加 Netty WebSocket 聊天功能基础版本,URL:http://localhost:8085/febs/pages/websocket/chat.html

- 新增 Netty WebSocket 配置和服务器启动逻辑
- 实现 WebSocketServerHandler 处理消息广播- 添加静态页面实现前端聊天室界面
- 更新 application.yml 配置,添加 WebSocket 端口
- 在 pom.xml 中添加 netty-all 依赖
2 files modified
4 files added
293 ■■■■■ changed files
pom.xml 8 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/common/configure/NettyWebSocketConfig.java 54 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/common/websocket/WebSocketServer.java 45 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/common/websocket/WebSocketServerHandler.java 48 ●●●●● patch | view | raw | blame | history
src/main/resources/application.yml 4 ●●●● patch | view | raw | blame | history
src/main/resources/static/febs/pages/websocket/chat.html 134 ●●●●● patch | view | raw | blame | history
pom.xml
@@ -29,6 +29,14 @@
    <dependencies>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.92.Final</version> <!-- 示例版本 -->
        </dependency>
        <dependency>
            <groupId>com.volcengine</groupId>
            <artifactId>volcengine-java-sdk-ark-runtime</artifactId>
src/main/java/cc/mrbird/febs/common/configure/NettyWebSocketConfig.java
New file
@@ -0,0 +1,54 @@
package cc.mrbird.febs.common.configure;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
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.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class NettyWebSocketConfig {
    @Value("${netty.websocket.port:8090}")
    private int port;
    @Bean(name = "bossGroup", destroyMethod = "shutdownGracefully")
    public EventLoopGroup bossGroup() {
        return new NioEventLoopGroup(1);
    }
    @Bean(name = "workerGroup", destroyMethod = "shutdownGracefully")
    public EventLoopGroup workerGroup() {
        return new NioEventLoopGroup();
    }
    @Bean
    public ServerBootstrap serverBootstrap(EventLoopGroup bossGroup, EventLoopGroup workerGroup) {
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 128)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new io.netty.channel.ChannelInitializer<io.netty.channel.socket.SocketChannel>() {
                    @Override
                    protected void initChannel(io.netty.channel.socket.SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new HttpServerCodec());
                        ch.pipeline().addLast(new ChunkedWriteHandler());
                        ch.pipeline().addLast(new HttpObjectAggregator(65536));
                        ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws"));
                        ch.pipeline().addLast(new cc.mrbird.febs.common.websocket.WebSocketServerHandler());
                    }
                });
        return bootstrap;
    }
}
src/main/java/cc/mrbird/febs/common/websocket/WebSocketServer.java
New file
@@ -0,0 +1,45 @@
package cc.mrbird.febs.common.websocket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@Component
public class WebSocketServer {
    @Value("${netty.websocket.port:8090}")
    private int port;
    @Autowired
    private ServerBootstrap serverBootstrap;
    @Autowired
    private EventLoopGroup bossGroup;
    @Autowired
    private EventLoopGroup workerGroup;
    private ChannelFuture channelFuture;
    @PostConstruct
    public void start() throws InterruptedException {
        channelFuture = serverBootstrap.bind(port).sync();
        System.out.println("Netty WebSocket服务器启动在端口:" + port);
    }
    @PreDestroy
    public void stop() {
        if (channelFuture != null) {
            channelFuture.channel().close();
        }
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
        System.out.println("Netty WebSocket服务器已停止");
    }
}
src/main/java/cc/mrbird/febs/common/websocket/WebSocketServerHandler.java
New file
@@ -0,0 +1,48 @@
package cc.mrbird.febs.common.websocket;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.util.concurrent.GlobalEventExecutor;
public class WebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        channels.add(ctx.channel());
    }
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        channels.remove(ctx.channel());
    }
    @Override
    public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        String request = msg.text();
        System.out.println("服务端收到:" + request);
        // 广播消息给其他连接的客户端,不包括发送者自己
        for (io.netty.channel.Channel channel : channels) {
            if (channel != ctx.channel()) {
                channel.writeAndFlush(new TextWebSocketFrame("[用户] " + request));
            }
        }
    }
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
src/main/resources/application.yml
@@ -41,3 +41,7 @@
            metadata:
              TableInfoHelper: error
    cc.mrbird.febs: info
netty:
  websocket:
    port: 8090
src/main/resources/static/febs/pages/websocket/chat.html
New file
@@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>WebSocket聊天室</title>
    <link rel="stylesheet" href="/layui/css/layui.css" media="all">
    <style>
        .chat-container {
            width: 100%;
            max-width: 800px;
            margin: 20px auto;
            border: 1px solid #e6e6e6;
            border-radius: 4px;
            overflow: hidden;
        }
        .chat-messages {
            height: 400px;
            padding: 15px;
            overflow-y: auto;
            background-color: #f8f8f8;
        }
        .chat-input {
            padding: 15px;
            background-color: #fff;
        }
        .message {
            margin-bottom: 10px;
            padding: 8px 12px;
            border-radius: 4px;
            background-color: #fff;
            box-shadow: 0 1px 2px rgba(0,0,0,0.1);
        }
        .message.me {
            background-color: #1E9FFF;
            color: white;
            text-align: right;
        }
    </style>
</head>
<body>
<div class="layui-container">
    <div class="chat-container">
        <div class="chat-messages" id="chatMessages">
            <!-- 消息将在这里显示 -->
        </div>
        <div class="chat-input">
            <div class="layui-form">
                <div class="layui-form-item">
                    <div class="layui-input-block">
                        <input type="text" id="messageInput" placeholder="请输入消息..." class="layui-input">
                    </div>
                </div>
                <div class="layui-form-item">
                    <div class="layui-input-block">
                        <button class="layui-btn" id="sendBtn">发送消息</button>
                        <button class="layui-btn layui-btn-primary" id="connectBtn">连接</button>
                        <button class="layui-btn layui-btn-danger" id="disconnectBtn">断开</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
<script src="/layui/layui.js" charset="utf-8"></script>
<script>
    layui.use(['jquery'], function() {
        var $ = layui.jquery;
        var websocket;
        function connect() {
            if ('WebSocket' in window) {
                // 连接到Netty WebSocket服务器
                websocket = new WebSocket("ws://localhost:8090/ws");
                websocket.onopen = function() {
                    appendMessage("[系统] 连接成功", "system");
                };
                websocket.onmessage = function(event) {
                    appendMessage(event.data, "other");
                };
                websocket.onclose = function() {
                    appendMessage("[系统] 连接已关闭", "system");
                };
                websocket.onerror = function() {
                    appendMessage("[系统] 连接发生错误", "system");
                };
            } else {
                alert('您的浏览器不支持WebSocket');
            }
        }
        function disconnect() {
            if (websocket) {
                websocket.close();
            }
        }
        function sendMessage() {
            var message = $('#messageInput').val().trim();
            if (message && websocket) {
                websocket.send(message);
                appendMessage(message, "me");
                $('#messageInput').val('');
            }
        }
        function appendMessage(message, type) {
            var messageClass = "message";
            if (type === "me") {
                messageClass += " me";
            }
            $('#chatMessages').append('<div class="' + messageClass + '">' + message + '</div>');
            $('#chatMessages').scrollTop($('#chatMessages')[0].scrollHeight);
        }
        // 绑定事件
        $('#connectBtn').on('click', connect);
        $('#disconnectBtn').on('click', disconnect);
        $('#sendBtn').on('click', sendMessage);
        // 回车发送消息
        $('#messageInput').on('keypress', function(e) {
            if (e.which === 13) {
                sendMessage();
            }
        });
    });
</script>
</body>
</html>