基于Springboot+netty 的IM聊天室&弹幕

代码地址

仓库地址
image.png

聊天室

创建springboot项目

image.png

image.png

因为我的NettyServer 是8080 所以我把服务端口改为了8081

image.png

引入netty 依赖

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.92.Final</version>
        </dependency>

IMServerHandler 处理用户连接以及发送消息等功能的逻辑

package site.zhourui.netty.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;

import java.util.HashMap;
import java.util.Map;

/**
 * @author zr 2024/8/13
 */
public class IMServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    private static final Map<Channel, String> userNicknames = new HashMap<>();
    private static final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception {
        String text = frame.text();
        Map<String, String> data = objectMapper.readValue(text, HashMap.class);
        String type = data.get("type");

        if ("set_nickname".equals(type)) {
            String nickname = data.get("nickname");
            userNicknames.put(ctx.channel(), nickname);
            System.out.println("User set nickname: " + nickname);

            // 广播用户加入消息
            broadcastMessage("系统", "用户 " + nickname + " 加入聊天");
        } else if ("chat_message".equals(type)) {
            String message = data.get("message");
            String nickname = userNicknames.get(ctx.channel());

            // 避免 nickname 未设置的情况
            if (nickname == null) {
                nickname = "Anonymous";
                userNicknames.put(ctx.channel(), nickname);
            }

            // 广播聊天消息
            broadcastMessage(nickname, message);
        }
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        System.out.println("新用户连接: " + ctx.channel().id().asShortText());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        String nickname = userNicknames.remove(ctx.channel());
        if (nickname != null) {
            System.out.println("User disconnected: " + nickname);
            // 广播用户离开消息
            broadcastMessage("系统", "用户 " + nickname + " 离开聊天");
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

    private void broadcastMessage(String senderNickname, String message) {
        for (Channel channel : userNicknames.keySet()) {
            try {
                Map<String, String> response = new HashMap<>();
                response.put("nickname", senderNickname);
                response.put("message", message);
                channel.writeAndFlush(new TextWebSocketFrame(objectMapper.writeValueAsString(response)));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}


NettyServer 聊天室服务端

package site.zhourui.netty.server;


import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
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.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import site.zhourui.netty.handler.IMServerHandler;

import java.util.concurrent.ConcurrentHashMap;

/**
 * @author zr 2024/8/13
 */
public class NettyServer {

    private final int port;

    // 存储所有连接的用户
    public static final ConcurrentHashMap<String, Channel> userChannels = new ConcurrentHashMap<>();

    public NettyServer(int port) {
        this.port = port;
    }

    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new HttpServerCodec());
                            ch.pipeline().addLast(new ChunkedWriteHandler());
                            ch.pipeline().addLast(new HttpObjectAggregator(8192));
                            ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws"));
                            ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast(new StringEncoder());
                            ch.pipeline().addLast(new IMServerHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture f = b.bind(port).sync();
            System.out.println("Netty IM server started on port " + port);
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    // 记录用户连接
    public static void addUser(String userId, Channel channel) {
        userChannels.put(userId, channel);
    }

    // 获取用户连接
    public static Channel getUserChannel(String userId) {
        return userChannels.get(userId);
    }

    // 移除用户连接
    public static void removeUser(String userId) {
        userChannels.remove(userId);
    }
}

将NettyServer 标记为组件,随springboot启动

package site.zhourui.netty.component;


import org.springframework.stereotype.Component;
import site.zhourui.netty.server.NettyServer;

import javax.annotation.PostConstruct;

/**
 * @author zr 2024/8/13
 */
@Component
public class NettyServerComponent {
    @PostConstruct
    public void startNettyServer() {
        Thread nettyThread = new Thread(() -> {
            try {
                new NettyServer(8080).run();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        nettyThread.setDaemon(true);  // 设置为守护线程
        nettyThread.start();
    }
}

chatClient.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket IM Client</title>
    <style>
      #chat-container {
        width: 50%;
        margin: auto;
        margin-top: 50px;
        border: 1px solid #ccc;
        padding: 20px;
        border-radius: 5px;
      }

      #messages {
        height: 300px;
        overflow-y: scroll;
        border: 1px solid #ccc;
        padding: 10px;
        margin-bottom: 20px;
      }

      #message-input {
        width: 80%;
        padding: 10px;
        border-radius: 5px;
        border: 1px solid #ccc;
      }

      #send-button {
        padding: 10px 20px;
        background-color: #4CAF50;
        color: white;
        border: none;
        border-radius: 5px;
        cursor: pointer;
      }

      #send-button:hover {
        background-color: #45a049;
      }

      /* 样式定义弹窗 */
      #config-modal {
        display: none;
        position: fixed;
        z-index: 1;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.5);
      }

      #config-form {
        background-color: #fff;
        margin: 15% auto;
        padding: 20px;
        border: 1px solid #ccc;
        width: 300px;
        border-radius: 5px;
      }

      #config-form input {
        width: 100%;
        padding: 10px;
        margin-bottom: 10px;
        border: 1px solid #ccc;
        border-radius: 5px;
      }

      #config-form button {
        width: 100%;
        padding: 10px;
        background-color: #4CAF50;
        color: white;
        border: none;
        border-radius: 5px;
        cursor: pointer;
      }

      #config-form button:hover {
        background-color: #45a049;
      }
    </style>
  </head>
  <body>
    <div id="config-modal">
      <div id="config-form">
        <input type="text" id="server-address" placeholder="Server Address" value="ws://localhost">
        <input type="text" id="server-port" placeholder="Port" value="8080">
        <input type="text" id="nickname" placeholder="Nickname">
        <button id="connect-button">Connect</button>
      </div>
    </div>

    <div id="chat-container">
      <h1>WebSocket IM Client</h1>
      <div id="messages"></div>
      <input type="text" id="message-input" placeholder="Type your message here..." />
      <button id="send-button">Send</button>
    </div>

    <script>
      let socket;
      let nickname = null;

      // 显示配置对话框
      const modal = document.getElementById('config-modal');
      modal.style.display = 'block';

      document.getElementById('connect-button').addEventListener('click', function() {
                                                                 const serverAddress = document.getElementById('server-address').value;
            const serverPort = document.getElementById('server-port').value;
            nickname = document.getElementById('nickname').value;

            if (!serverAddress || !serverPort || !nickname) {
                alert("All fields are required!");
                return;
            }

            // 创建 WebSocket 连接
            socket = new WebSocket(`${serverAddress}:${serverPort}/ws`);

            // 连接打开事件
            socket.onopen = function() {
                console.log('Connected to IM Server');
                socket.send(JSON.stringify({ type: 'set_nickname', nickname: nickname }));
                modal.style.display = 'none'; // 连接成功后隐藏配置对话框
            };

            // 处理收到的消息
            socket.onmessage = function(event) {
                const data = JSON.parse(event.data);
                const senderNickname = data.nickname;
                const message = data.message;

                const messageDiv = document.createElement('div');
                if (senderNickname === nickname) {
                    messageDiv.textContent = `me: ${message}`;
                    messageDiv.style.color = 'blue'; // 给自己的消息加一个样式
                } else {
                    messageDiv.textContent = `${senderNickname}: ${message}`;
                }
                document.getElementById('messages').appendChild(messageDiv);
                document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight; // 自动滚动到底部
            };

            // 连接关闭事件
            socket.onclose = function() {
                console.log('Disconnected from IM Server');
            };
        });

        // 发送消息
        document.getElementById('send-button').addEventListener('click', function() {
            const message = document.getElementById('message-input').value.trim();
            if (message) {
                socket.send(JSON.stringify({ type: 'chat_message', message: message }));
                document.getElementById('message-input').value = ''; // 清空输入框
            }
        });

        // 支持按 Enter 键发送消息
        document.getElementById('message-input').addEventListener('keypress', function(event) {
            if (event.key === 'Enter') {
                document.getElementById('send-button').click();
            }
        });
    </script>
</body>
</html>

项目结构

image.png

  1. 浏览器打开chatClient.html 第一个用户设置昵称为111

image.png

  1. 加入聊天室后即可接收到消息

image.png

  1. 浏览器再次打开chatClient.html 第二个用户设置昵称为222

image.png

  1. 222用户显示

image.png

  1. 111用户显示:222用户也加入聊天

image.png

  1. 111发送消息

image.png

  1. 222收到消息

image.png

弹幕

弹幕和聊天室服务器目前是一样的,只是客户端有部分不同

barrageClient.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket IM Client with Stylish Danmaku</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            background-image: url('https://example.com/your-background.jpg'); /* 替换为你想要的背景图 */
            background-size: cover;
            background-position: center;
            height: 100vh;
            font-family: Arial, sans-serif;
            color: white;
        }

        #chat-container {
            width: 70%;
            height: 80%;
            margin: auto;
            margin-top: 5%;
            padding: 20px;
            border-radius: 10px;
            position: relative;
            overflow: hidden; /* 确保弹幕不会超出容器 */
            background: rgba(0, 0, 0, 0.6); /* 半透明黑色背景 */
        }

        .danmaku-message {
            position: absolute;
            white-space: nowrap;
            font-size: 18px;
            padding: 5px 10px;
            border-radius: 5px;
            color: #fff;
            text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
            animation: danmaku-move 7s linear infinite;
            background: rgba(0, 0, 0, 0.5); /* 半透明背景 */
        }

        @keyframes danmaku-move {
            from {
                left: 100%; /* 从屏幕右侧外开始 */
            }
            to {
                left: -100%; /* 移动到屏幕左侧外 */
            }
        }

        #message-input {
            width: 70%;
            padding: 15px;
            border-radius: 25px;
            border: 1px solid #ccc;
            background-color: rgba(255, 255, 255, 0.8);
            color: #333;
        }

        #send-button {
            padding: 15px 25px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 25px;
            cursor: pointer;
            margin-left: 10px;
            font-size: 16px;
        }

        #send-button:hover {
            background-color: #45a049;
        }

        #config-modal {
            display: none;
            position: fixed;
            z-index: 1;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
        }

        #config-form {
            background-color: #fff;
            margin: 15% auto;
            padding: 20px;
            border: 1px solid #ccc;
            width: 300px;
            border-radius: 10px;
        }

        #config-form input {
            width: 100%;
            padding: 10px;
            margin-bottom: 10px;
            border: 1px solid #ccc;
            border-radius: 25px;
        }

        #config-form button {
            width: 100%;
            padding: 10px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 25px;
            cursor: pointer;
        }

        #config-form button:hover {
            background-color: #45a049;
        }
    </style>
</head>
<body>
    <div id="config-modal">
        <div id="config-form">
            <input type="text" id="server-address" placeholder="Server Address" value="ws://localhost">
            <input type="text" id="server-port" placeholder="Port" value="8080">
            <input type="text" id="nickname" placeholder="Nickname">
            <button id="connect-button">Connect</button>
        </div>
    </div>

    <div id="chat-container">
        <h1>WebSocket IM Client</h1>
        <input type="text" id="message-input" placeholder="Type your message here..." />
        <button id="send-button">Send</button>
    </div>

    <script>
        let socket;
        let nickname = null;

        // 显示配置对话框
        const modal = document.getElementById('config-modal');
        modal.style.display = 'block';

        document.getElementById('connect-button').addEventListener('click', function() {
            const serverAddress = document.getElementById('server-address').value;
            const serverPort = document.getElementById('server-port').value;
            nickname = document.getElementById('nickname').value;

            if (!serverAddress || !serverPort || !nickname) {
                alert("All fields are required!");
                return;
            }

            // 创建 WebSocket 连接
            socket = new WebSocket(`${serverAddress}:${serverPort}/ws`);

            // 连接打开事件
            socket.onopen = function() {
                console.log('Connected to IM Server');
                socket.send(JSON.stringify({ type: 'set_nickname', nickname: nickname }));
                modal.style.display = 'none'; // 连接成功后隐藏配置对话框
            };

            // 处理收到的消息
            socket.onmessage = function(event) {
                const data = JSON.parse(event.data);
                const senderNickname = data.nickname;
                const message = data.message;

                const messageDiv = document.createElement('div');
                messageDiv.classList.add('danmaku-message');
                if (senderNickname === nickname) {
                    messageDiv.textContent = `me: ${message}`;
                    messageDiv.style.color = 'lightblue'; // 给自己的消息加一个样式
                } else {
                    messageDiv.textContent = `${senderNickname}: ${message}`;
                }

                const chatContainer = document.getElementById('chat-container');
                messageDiv.style.top = `${Math.random() * (chatContainer.clientHeight - 20)}px`; // 随机设置弹幕的高度
                chatContainer.appendChild(messageDiv);

                // 删除弹幕元素以避免占用内存
                setTimeout(() => {
                    chatContainer.removeChild(messageDiv);
                }, 7000); // 弹幕持续7秒
            };

            // 连接关闭事件
            socket.onclose = function() {
                console.log('Disconnected from IM Server');
            };
        });

        // 发送消息
        document.getElementById('send-button').addEventListener('click', function() {
            const message = document.getElementById('message-input').value.trim();
            if (message) {
                socket.send(JSON.stringify({ type: 'chat_message', message: message }));
                document.getElementById('message-input').value = ''; // 清空输入框
            }
        });

        // 支持按 Enter 键发送消息
        document.getElementById('message-input').addEventListener('keypress', function(event) {
            if (event.key === 'Enter') {
                document.getElementById('send-button').click();
            }
        });
    </script>
</body>
</html>

  1. 浏览器打开chatClient.html 第一个用户设置昵称为1,第二个用户设置昵称为2

image.png

  1. 加入时用户提醒

image.png

  1. 用户1发送消息

image.png

  1. 用户2发送消息

image.png

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很抱歉,作为一名AI语言模型,我无法生成代码。不过我可以给你提供一些参考资料,希望能对你有所帮助。 首先,你需要在Spring Boot项目中引入Netty和WebSocket的依赖,例如: ```xml <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.22.Final</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` 然后,你可以编写一个WebSocket客户端类,例如: ```java @Component public class WebSocketClient { private final WebSocketClientHandshaker handshaker; private WebSocketChannel channel; public WebSocketClient(@Value("${websocket.url}") String url) throws Exception { URI uri = new URI(url); handshaker = WebSocketClientHandshakerFactory.newHandshaker(uri, WebSocketVersion.V13, null, true, new DefaultHttpHeaders()); } public void connect() throws Exception { Bootstrap bootstrap = new Bootstrap(); EventLoopGroup group = new NioEventLoopGroup(); try { bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new WebSocketClientInitializer(handshaker)); ChannelFuture future = bootstrap.connect(handshaker.uri().getHost(), handshaker.uri().getPort()).sync(); channel = ((WebSocketClientHandler) future.channel().pipeline().last()).getChannel(); handshaker.handshake(channel).sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { group.shutdownGracefully(); } } public void send(String message) { WebSocketFrame frame = new TextWebSocketFrame(message); channel.writeAndFlush(frame); } public void close() { channel.close(); } } ``` 其中,WebSocketClientInitializer是用于初始化Netty的WebSocket客户端的,例如: ```java public class WebSocketClientInitializer extends ChannelInitializer<SocketChannel> { private final WebSocketClientHandshaker handshaker; public WebSocketClientInitializer(WebSocketClientHandshaker handshaker) { this.handshaker = handshaker; } @Override protected void initChannel(SocketChannel channel) throws Exception { channel.pipeline() .addLast(new HttpClientCodec()) .addLast(new HttpObjectAggregator(8192)) .addLast(new WebSocketClientHandler(handshaker)); } } ``` 最后,你可以在Spring Boot的控制器中使用WebSocketClient来与WebSocket服务器进行通信,例如: ```java @RestController public class WebSocketController { @Autowired private WebSocketClient webSocketClient; @PostMapping("/send") public void send(@RequestBody String message) { webSocketClient.send(message); } } ``` 这样你就可以使用Spring BootNetty来实现WebSocket客户端了。希望对你有所帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值