当前位置: 首页 > news >正文

【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

http://www.dtcms.com/a/456961.html

相关文章:

  • 阿里云无主体新增网站阿里云网站用什么做的
  • 做营销策划的上哪个网站好做房产的网站排名
  • 深入理解CSS BFC:块级格式化上下文
  • 226. 翻转二叉树 LeetCode 热题 HOT 100
  • Python人工智能编程从零开始掌握机器学习基础
  • Linux V4L2框架详解:Camera软件架构与驱动实现
  • javaweb--JavaScript
  • CachyOS:面向游戏的 Arch Linux 优化与安装配置指南
  • Encoder-Decoder架构的模型简介
  • 哪些网站适合花钱做推广房产网新房
  • OpenTiny 进阶学习指南:从全景到精微的高效成长之路
  • 制作网站电话如何在本地安装wordpress
  • 9-mysql编程
  • 十堰专业网站建设科技公司网站设计欣赏
  • [linux仓库]信号快速认识[进程信号·壹]
  • 【开题答辩实录分享】以《走失人口系统档案的设计与实现》为例进行答辩实录分享
  • 【智能体】Ch3-提升模型性能的定向学习(Enhancing model performance with targeted learning)
  • 【LLM】大模型vibe coding(cursor、copilot、comate)
  • 如何创建网站教程视频react做前台网站
  • Web 开发 24
  • 深入理解RNN及其变体:从传统RNN到LSTM、GRU(附PyTorch实战)
  • Linux 服务器常见的性能调优
  • 济南网站价格wordpress tag模板代码
  • 飞牛nas配置息屏不关机
  • 【ThreeJs】【伪VR】用 Three.js 实现伪 VR 全景看房系统:低成本实现 3D 级交互体验
  • Java Spring “Bean” 面试清单(含超通俗生活案例与深度理解)
  • 生活琐记(6)
  • Python高效数据分析从入门到实战的七个步骤
  • 长沙网站制作关键词推广在线咨询 1 网站宣传
  • 使用中sql注意点