【netty实战】从零构建一个带GUI的简易聊天室
一、背景
以前研究过netty框架,本次继续玩一下netty,于是基于netty写了一个带GUI的简易聊天室。对于对IM感兴趣的或者正在学习netty的小伙伴可以参考下。这里记录下并且为了分享给各位小伙伴~,不喜勿喷,多谢~。
二、概述
Ricky-Chat 是一个基于 Netty 高性能网络框架构建的即时通讯演示项目。该项目完整实现了服务端与客户端的核心功能,旨在展示 Netty 在构建实时通信应用时的强大能力。
- 服务端: 采用 Netty 的多线程模型,高效管理并维护所有客户端的连接,实现消息的可靠路由与广播。
- 客户端: 提供了直观的图形用户界面(GUI),模拟了群聊场景。用户可以方便地发送消息,所有在线成员将即时收到广播,体验流畅的实时交互。
三、正文
(一)、功能演示
1、聊天室登录:ricky用户
2、聊天室页面:ricky聊天页面
3、聊天室登录:Nicky用户
4、聊天室页面:Nicky聊天页面
4、聊天室页面:共同聊天界面
(二)、实现思路
1、服务端实现:
1>、服务端类RickyServer
/*** @Auther:ricky* @Date: 2025-10-02 10:16* @Description*/
public class RickyServer {private final static Logger LOGGER = LoggerFactory.getLogger(RickyServer.class);public static final int SERVER_PORT = 8001;public static final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);public static final Map<String, Channel> userChannelMap = new ConcurrentHashMap<>();public static void main(String[] args) {ServerBootstrap b = new ServerBootstrap();NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);NioEventLoopGroup workGroup = new NioEventLoopGroup();try {b.group(bossGroup, workGroup);b.channel(NioServerSocketChannel.class);b.localAddress(SERVER_PORT);b.option(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT);b.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);b.option(ChannelOption.SO_KEEPALIVE, true);b.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) {socketChannel.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));socketChannel.pipeline().addLast(new LengthFieldPrepender(4, false));socketChannel.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));// 添加 StringEncoder 来处理 String 到 ByteBuf 的转换socketChannel.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));// 需要添加 JsonMsgEncoder 来处理 JsonMsgDto 对象socketChannel.pipeline().addLast(new JsonMsgEncoder());socketChannel.pipeline().addLast(new JsonMsgDecoder());socketChannel.pipeline().addLast(new ServerHandler());}});ChannelFuture channelFuture = b.bind();channelFuture.addListener((future) -> {if (future.isSuccess()) {LOGGER.info(" ========》反应器线程 回调 Json服务器启动成功,监听端口: {}", channelFuture.channel().localAddress());}});channelFuture.sync();LOGGER.info(" 调用线程执行的,Json服务器启动成功,监听端口: {}", channelFuture.channel().localAddress());ChannelFuture closeFuture = channelFuture.channel().closeFuture();closeFuture.sync();} catch (Exception ex) {ExceptionUtil.stacktraceToString(ex);} finally {workGroup.shutdownGracefully();bossGroup.shutdownGracefully();}}
}
2>、服务端类ServerHandler
/*** 假设你有一个自定义的 Handler 来处理业务逻辑*/
public class ServerHandler extends SimpleChannelInboundHandler<JsonMsgDto> {private final static Logger LOGGER = LoggerFactory.getLogger(ServerHandler.class);@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {// 客户端连上来了,将它的 Channel 加入到全局 Group 中channels.add(ctx.channel());LOGGER.info("客户端已连接: {}", ctx.channel().remoteAddress());super.channelActive(ctx);}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, JsonMsgDto msg) {// ... 处理接收到的消息 ...LOGGER.info("用户是{},收到来自 {} 的消息: {}", msg.getUserName(),ctx.channel().remoteAddress(), msg.getContent());if(!userChannelMap.containsKey(msg.getUserName())) {userChannelMap.put(msg.getUserName(), ctx.channel());LOGGER.info("用户 {} 注册成功,关联到 Channel: {}", msg.getUserName(), ctx.channel().remoteAddress());}else{LOGGER.info("用户 {} 已经注册,Channel: {}", msg.getUserName(), ctx.channel().remoteAddress());}Set<String> strings = userChannelMap.keySet();for (String string : strings) {LOGGER.info("userChannelMap里面的数据 {} ", string);}// 示例:将收到的消息广播给所有客户端JsonMsgDto broadcastMsg = new JsonMsgDto();//broadcastMsg.setContent("广播: " + msg.getContent());broadcastMsg.setContent(msg.getContent());broadcastMsg.setUserName(msg.getUserName());LOGGER.info("准备广播消息给 {} 个客户端", channels.size());// 添加更完善的错误处理channels.writeAndFlush(broadcastMsg).addListener(future -> {if (future.isSuccess()) {LOGGER.info("广播消息发送成功");} else {if (future.cause() instanceof io.netty.channel.group.ChannelGroupException) {ChannelGroupException groupException = (ChannelGroupException) future.cause();for (Map.Entry<Channel, Throwable> entry : groupException) {LOGGER.error("向 Channel {} 发送消息失败: {}",entry.getKey().remoteAddress(),entry.getValue().getMessage());}} else {LOGGER.error("广播消息发送失败", future.cause());}}});}@Overridepublic void channelInactive(ChannelHandlerContext ctx) throws Exception {// 客户端断开了,Channel 会自动从 ChannelGroup 中移除LOGGER.info("客户端已断开: {}", ctx.channel().remoteAddress());super.channelInactive(ctx);}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {// ... 异常处理 ...ctx.close();}
}
2、客户端实现:
1>、客户端类RickyClient
/*** @Auther:ricky* @Date: 2023-08-02 10:46* @Description*/
public class RickyClient {private final static Logger LOGGER = LoggerFactory.getLogger(RickyClient.class);public static final String SERVER_IP = "127.0.0.1";public static final int SERVER_PORT = 8001;/*** 连接服务器并返回Channel* @return Channel 连接通道*/public static Channel connectAndGetChannel() throws InterruptedException {Bootstrap b = new Bootstrap();NioEventLoopGroup workGroup = new NioEventLoopGroup();try {b.group(workGroup);b.channel(NioSocketChannel.class);b.remoteAddress(SERVER_IP, SERVER_PORT);b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);b.option(ChannelOption.SO_KEEPALIVE, true);b.handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {// 添加解码器(与服务端对应)socketChannel.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));socketChannel.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));socketChannel.pipeline().addLast(new JsonMsgDecoder()); // 添加解码器// 保留原有的编码器socketChannel.pipeline().addLast(new LengthFieldPrepender(4, false));socketChannel.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));socketChannel.pipeline().addLast(new JsonMsgEncoder());socketChannel.pipeline().addLast(new ClientHandler());}});ChannelFuture channelFuture = b.connect();channelFuture.sync();return channelFuture.channel();} catch (Exception ex) {workGroup.shutdownGracefully();throw ex;}}/*** 启动GUI聊天客户端*/public static void startGuiClient() {SwingUtilities.invokeLater(() -> {try {UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());} catch (Exception e) {LOGGER.warn("无法设置系统外观", e);}ChatWindow chatWindow = new ChatWindow();chatWindow.setVisible(true);});}public static void main(String[] args) {startGuiClient();}
}
2>、客户端类ClientHandler
public class ClientHandler extends SimpleChannelInboundHandler<JsonMsgDto> {private final static Logger LOGGER = LoggerFactory.getLogger(ClientHandler.class);@Overrideprotected void channelRead0(ChannelHandlerContext ctx, JsonMsgDto msg) {ChatWindow.appendMessage(msg.getUserName(),msg.getContent());LOGGER.info("======> 收到服务端推送的消息: {}", msg.getContent());}@Overridepublic void channelActive(ChannelHandlerContext ctx) {LOGGER.info("客户端与服务端连接建立成功!");// super.channelActive(ctx); // 可以不调用}@Overridepublic void channelInactive(ChannelHandlerContext ctx) throws Exception {LOGGER.info("客户端与服务端连接断开!");super.channelInactive(ctx); // 可以不调用}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {LOGGER.error("客户端发生异常", cause);ctx.close();}
}
3>、客户端界面ChatWindow
可以直接查看开源地址代码
3、公共信息实现:
1>、JsonMsgDecoder
public class JsonMsgDecoder extends MessageToMessageDecoder<String> {private final static Logger LOGGER = LoggerFactory.getLogger(JsonMsgDecoder.class);private final ObjectMapper objectMapper = new ObjectMapper();@Overrideprotected void decode(ChannelHandlerContext ctx, String msg, List<Object> out) throws Exception {LOGGER.info("收到服务端原始数据: {}", msg);JsonMsgDto jsonMsgDto = objectMapper.readValue(msg, JsonMsgDto.class);out.add(jsonMsgDto);}
}
2>、JsonMsgEncoder
/*** @Auther:ricky* @Date: 2025-10-02 10:16* @Description*/
public class JsonMsgEncoder extends MessageToMessageEncoder<Object> {@Overrideprotected void encode(ChannelHandlerContext channelHandlerContext, Object obj, List<Object> list) {String json = JsonMsgDto.format(obj);System.out.println("发送报文:" + json);list.add(json);}
}
3>、JsonMsgDto
/*** @Auther:ricky* @Date: 2025-10-02 10:16* @Description*/
@Data
public class JsonMsgDto {private int id;private String userName;private String content;public JsonMsgDto() {this.id = RandomUtil.randomInt(100);}public static JsonMsgDto parse(String jsonStr) {return JSONUtil.toBean(jsonStr, JsonMsgDto.class);}public static String format(JsonMsgDto jsonMsgDto) {return JSONUtil.toJsonStr(jsonMsgDto);}public static String format(Object obj) {return JSONUtil.toJsonStr(obj);}}
四、开源
https://gitee.com/ricky_kai/ricky-chat.git