Netty案例:群聊天室

目录

1、系统设计

2、代码实现

2.1 服务端代码

2.2 客户端代码

2.3 启动说明

3、关键技术解析

3.1 编解码器使用

3.2 通道管理

3.3 消息协议设计

3.4 用户管理


1、系统设计

核心功能

  • 用户加入/离开聊天室通知

  • 群发聊天消息

  • 在线用户列表管理

  • 用户昵称设置

通信协议设计

  • 使用简单的文本协议,消息格式:[类型]:[内容]

  • 消息类型:JOIN(改昵称), MSG(消息),  LIST(用户列表), SYS(系统消息)

关键技术组件

  • LineBasedFrameDecoder:解决TCP粘包/拆包问题

  • StringEncoder/StringDecoder:字符串编解码

  • ChannelGroup:管理所有连接的客户端通道

2、代码实现

2.1 服务端代码

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;

public class ChatServer {

    private final int port;
    private final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); // 存储所有连接的客户端
    private final Map users = new HashMap<>(); // 在线的客户端连接及用户

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

    public void start() throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        
        try {
            ServerBootstrap bootstrap = new ServerBootstrap()
                .group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .localAddress(new InetSocketAddress(port))
                .childHandler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ch.pipeline()
                            .addLast(new LineBasedFrameDecoder(1024)) // 行分隔解码器
                            .addLast(new StringDecoder(CharsetUtil.UTF_8)) // 字符串解码器
                            .addLast(new StringEncoder(CharsetUtil.UTF_8)) // 字符串编码器
                            .addLast(new ChatServerHandler());// 自定义业务处理器
                    }
                })
                .option(ChannelOption.SO_BACKLOG, 128)
                .childOption(ChannelOption.SO_KEEPALIVE, true);
            // 启动服务器(同步)
            ChannelFuture future = bootstrap.bind().sync();
            System.out.println("聊天服务器已启动,监听端口: " + port);
            future.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully().sync();
            workerGroup.shutdownGracefully().sync();
        }
    }
    // 自定义服务端处理器
    private class ChatServerHandler extends SimpleChannelInboundHandler {
        // 新客户端连接
        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            channels.add(ctx.channel());
            users.put(ctx.channel(), "用户" + ctx.channel().id().asShortText());
            broadcastSystemMessage(users.get(ctx.channel()) + " 加入了聊天室");
            sendUserList(); // 输出用户列表
        }
        // 客户端断开
        @Override
        public void channelInactive(ChannelHandlerContext ctx) {
            String username = users.get(ctx.channel());
            channels.remove(ctx.channel());
            users.remove(ctx.channel());
            broadcastSystemMessage(username + " 离开了聊天室");
            sendUserList(); // 输出用户列表
        }
        //接收消息
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) {
            if (msg.startsWith("JOIN:")) {
                // 设置用户名
                String newName = msg.substring(5).trim();
                String oldName = users.get(ctx.channel());
                users.put(ctx.channel(), newName);
                broadcastSystemMessage(oldName + " 更名为: " + newName);
                sendUserList();
            } else if (msg.startsWith("LIST")) {
                // 请求用户列表
                ctx.channel().writeAndFlush("LIST:" + String.join(",", users.values()));
            } else {
                // 普通消息
                String username = users.get(ctx.channel());
                broadcastMessage(username, msg);
            }
        }
        // 异常处理
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            System.err.println("客户端连接异常: " + cause.getMessage());
            cause.printStackTrace();
            ctx.close();
        }

        private void broadcastMessage(String username, String message) {
            String formatted = String.format("[%s]: %s", username, message);
            channels.writeAndFlush("MSG:" + formatted + "\n", channel -> 
                channel != ctx.channel()); // 广播给除了发送者之外的所有人
        }

        private void broadcastSystemMessage(String message) {
            channels.writeAndFlush("SYS:" + message + "\n");
        }

        private void sendUserList() {
            channels.writeAndFlush("LIST:" + String.join(",", users.values()) + "\n");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ChatServer(8080).start();
    }
}

2.2 客户端代码

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

public class ChatClient {

    private final String host;
    private final int port;
    private String username;

    public ChatClient(String host, int port) {
        this.host = host;
        this.port = port;
        this.username = "匿名用户";
    }

    public void start() throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap()
                .group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ch.pipeline()
                            .addLast(new LineBasedFrameDecoder(1024))
                            .addLast(new StringDecoder(StandardCharsets.UTF_8))
                            .addLast(new StringEncoder(StandardCharsets.UTF_8))
                            .addLast(new ChatClientHandler());
                    }
                });

            Channel channel = bootstrap.connect(host, port).sync().channel();
            System.out.println("已连接到聊天服务器 " + host + ":" + port);
            
            // 读取控制台输入
            System.out.println("输入 '/name 新名字' 更改昵称");
            System.out.println("输入 '/list' 查看在线用户");
            System.out.println("输入 '/exit' 退出");
            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
            

            while (true) {
                String input = in.readLine();
                if (input == null || "/exit".equalsIgnoreCase(input)) {
                    channel.close();
                    break;
                } else if (input.startsWith("/name ")) {
                    String newName = input.substring(6).trim();
                    channel.writeAndFlush("JOIN:" + newName + "\n");
                } else if ("/list".equalsIgnoreCase(input)) {
                    channel.writeAndFlush("LIST\n");
                } else {
                    channel.writeAndFlush(input + "\n");
                }
            }
        } finally {
            group.shutdownGracefully();
        }
    }

    private class ChatClientHandler extends SimpleChannelInboundHandler {
        // 接收服务端消息
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) {
            if (msg.startsWith("SYS:")) {
                System.out.println("\u001B[33m[系统] " + msg.substring(4) + "\u001B[0m");
            } else if (msg.startsWith("MSG:")) {
                System.out.println(msg.substring(4));
            } else if (msg.startsWith("LIST:")) {
                System.out.println("\u001B[34m==== 在线用户 ====");
                String[] users = msg.substring(5).split(",");
                for (String user : users) {
                    System.out.println(user);
                }
                System.out.println("================\u001B[0m");
            }
        }
        //异常处理
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            System.err.println("服务器连接异常: " + cause.getMessage());
            cause.printStackTrace();
            ctx.close();
        }
    }

    public static void main(String[] args) throws Exception {
        new ChatClient("localhost", 8080).start();
    }
}

2.3 启动说明

# 启动服务器:
java ChatServer

# 启动多个客户端:
java ChatClient

客户端命令:

  • /name 新名字:更改昵称

  • /list:查看在线用户

  • /exit:退出聊天室

3、关键技术解析

3.1 编解码器使用

ch.pipeline()
    .addLast(new LineBasedFrameDecoder(1024)) // 解决TCP粘包/拆包
    .addLast(new StringDecoder(CharsetUtil.UTF_8)) // 字节转字符串
    .addLast(new StringEncoder(CharsetUtil.UTF_8)); // 字符串转字节
  • LineBasedFrameDecoder:基于换行符的消息分割器

  • StringDecoder:将接收到的ByteBuf转换为字符串

  • StringEncoder:将字符串转换为ByteBuf发送

3.2 通道管理

// 服务器端管理所有连接的通道
private final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

// 添加新连接
channels.add(ctx.channel());

// 广播消息
channels.writeAndFlush("广播内容");

3.3 消息协议设计

消息类型 格式 说明
JOIN JOIN:新用户名 设置或更改用户名
MSG MSG:[用户名]:内容 聊天消息
SYS SYS:系统消息 系统通知
LIST LIST:用户1,用户2,... 在线用户列表

3.4 用户管理

// 存储用户信息
private final Map users = new HashMap<>();

// 用户加入
users.put(ctx.channel(), "默认用户名");

// 用户离开
users.remove(ctx.channel());

你可能感兴趣的:(12_计算机网络,网络,java,分布式)