李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
07.Netty进阶之聊天室案例
Leefs
2022-06-20 PM
1546℃
0条
[TOC] ### 一、聊天室业务介绍 + 用户管理接口 ```java /** * 用户管理接口 */ public interface UserService { /** * 登录 * @param username 用户名 * @param password 密码 * @return 登录成功返回 true, 否则返回 false */ boolean login(String username, String password); } ``` + 会话管理接口 ```java import io.netty.channel.Channel; /** * 会话管理接口 */ public interface Session { /** * 绑定会话 * @param channel 哪个 channel 要绑定会话 * @param username 会话绑定用户 */ void bind(Channel channel, String username); /** * 解绑会话 * @param channel 哪个 channel 要解绑会话 */ void unbind(Channel channel); /** * 获取属性 * @param channel 哪个 channel * @param name 属性名 * @return 属性值 */ Object getAttribute(Channel channel, String name); /** * 设置属性 * @param channel 哪个 channel * @param name 属性名 * @param value 属性值 */ void setAttribute(Channel channel, String name, Object value); /** * 根据用户名获取 channel * @param username 用户名 * @return channel */ Channel getChannel(String username); } ``` + 聊天组会话管理接口 ```java import io.netty.channel.Channel; import java.util.List; import java.util.Set; /** * 聊天组会话管理接口 */ public interface GroupSession { /** * 创建一个聊天组, 如果不存在才能创建成功, 否则返回 null * @param name 组名 * @param members 成员 * @return 成功时返回组对象, 失败返回 null */ Group createGroup(String name, Set
members); /** * 加入聊天组 * @param name 组名 * @param member 成员名 * @return 如果组不存在返回 null, 否则返回组对象 */ Group joinMember(String name, String member); /** * 移除组成员 * @param name 组名 * @param member 成员名 * @return 如果组不存在返回 null, 否则返回组对象 */ Group removeMember(String name, String member); /** * 移除聊天组 * @param name 组名 * @return 如果组不存在返回 null, 否则返回组对象 */ Group removeGroup(String name); /** * 获取组成员 * @param name 组名 * @return 成员集合, 没有成员会返回 empty set */ Set
getMembers(String name); /** * 获取组成员的 channel 集合, 只有在线的 channel 才会返回 * @param name 组名 * @return 成员 channel 集合 */ List
getMembersChannel(String name); /** * 判断群聊是否一被创建 * @param name 群聊名称 * @return 是否存在 */ boolean isCreated(String name); } ``` ### 二、聊天室业务-登录 + **服务端** ```java import cn.itcast.protocol.MessageCodecSharable; import cn.itcast.protocol.ProcotolFrameDecoder; import cn.itcast.server.handler.*; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; import io.netty.handler.timeout.IdleStateHandler; import lombok.extern.slf4j.Slf4j; /** * 服务端 */ @Slf4j public class ChatServer { public static void main(String[] args) { NioEventLoopGroup boss = new NioEventLoopGroup(); NioEventLoopGroup worker = new NioEventLoopGroup(); LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG); MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable(); LoginRequestMessageHandler LOGIN_HANDLER = new LoginRequestMessageHandler(); ChatRequestMessageHandler CHAT_HANDLER = new ChatRequestMessageHandler(); GroupCreateRequestMessageHandler GROUP_CREATE_HANDLER = new GroupCreateRequestMessageHandler(); GroupJoinRequestMessageHandler GROUP_JOIN_HANDLER = new GroupJoinRequestMessageHandler(); GroupMembersRequestMessageHandler GROUP_MEMBERS_HANDLER = new GroupMembersRequestMessageHandler(); GroupQuitRequestMessageHandler GROUP_QUIT_HANDLER = new GroupQuitRequestMessageHandler(); GroupChatRequestMessageHandler GROUP_CHAT_HANDLER = new GroupChatRequestMessageHandler(); QuitHandler QUIT_HANDLER = new QuitHandler(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.channel(NioServerSocketChannel.class); serverBootstrap.group(boss, worker); serverBootstrap.childHandler(new ChannelInitializer
() { @Override protected void initChannel(SocketChannel ch) throws Exception { //半包处理器 ch.pipeline().addLast(new ProcotolFrameDecoder()); ch.pipeline().addLast(LOGGING_HANDLER); //对发送来的消息进行解码操作 ch.pipeline().addLast(MESSAGE_CODEC); //空闲检测 //用来判断是不是 读空闲时间过长或写空闲时间过长 //5s内如果没有收到channel的数据,会触发一个 IdleState#READER_IDLE 事件 ch.pipeline().addLast(new IdleStateHandler(5,0,0)); //ChannelDuplexHandler 可以同时作为入站和出站处理器 ch.pipeline().addLast(new ChannelDuplexHandler(){ //用来触发特殊事件 @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { IdleStateEvent event = (IdleStateEvent)evt; //触发了读空闲事件 if(event.state() == IdleState.READER_IDLE){ log.debug("已经 5s 没有读到数据了"); ctx.channel().close(); } } }); //业务处理器 //SimpleChannelInboundHandler:针对不同类型对象进行处理 //对LoginRequestMessage对象的消息感兴趣 //接收LoginRequestMessage对象发送过来的消息进行业务处理 ch.pipeline().addLast(LOGIN_HANDLER); ch.pipeline().addLast(CHAT_HANDLER); ch.pipeline().addLast(GROUP_CREATE_HANDLER); ch.pipeline().addLast(GROUP_JOIN_HANDLER); ch.pipeline().addLast(GROUP_MEMBERS_HANDLER); ch.pipeline().addLast(GROUP_QUIT_HANDLER); ch.pipeline().addLast(GROUP_CHAT_HANDLER); ch.pipeline().addLast(QUIT_HANDLER); } }); Channel channel = serverBootstrap.bind(8080).sync().channel(); channel.closeFuture().sync(); } catch (InterruptedException e) { log.error("server error", e); } finally { boss.shutdownGracefully(); worker.shutdownGracefully(); } } } ``` + **客户端** ```java import cn.itcast.message.*; import cn.itcast.protocol.MessageCodecSharable; import cn.itcast.protocol.ProcotolFrameDecoder; 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.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; import io.netty.handler.timeout.IdleStateHandler; import lombok.extern.slf4j.Slf4j; import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; /** * 客户端 */ /** * 1.客户端创建一个新的线程接收控制台输入 * 2.将输入信息通过channel发送到服务端进行校验,同时客户端通过倒计数锁进入阻塞状态 * 3.服务端获得客户端发送过来的登录消息进行判断登录信息是否正确 * 4.将判断结果返回给客户端 * 5.客户端通过channelRead方法接收服务端返回的消息 * 6.在channelRead方法中对校验结果进行判断,通过一个boolean参数来记录状态 * 7.唤醒system in线程 * 8.在system in线程对登录状态进行判断 * 如果登录失败则关闭channel通道并退出 * 如果登录成功则继续向下执行 */ @Slf4j public class ChatClient { public static void main(String[] args) { NioEventLoopGroup group = new NioEventLoopGroup(); LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG); MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable(); //倒计数锁,初始值为1 CountDownLatch WAIT_FOR_LOGIN = new CountDownLatch(1); //记录登录状态,默认未登录 AtomicBoolean LOGIN = new AtomicBoolean(false); AtomicBoolean EXIT = new AtomicBoolean(false); Scanner scanner = new Scanner(System.in); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.channel(NioSocketChannel.class); bootstrap.group(group); //建立Scoket会话连接 bootstrap.handler(new ChannelInitializer
() { @Override protected void initChannel(SocketChannel ch) throws Exception { //引入长度域解码器,解决数据中的粘包,半包问题 ch.pipeline().addLast(new ProcotolFrameDecoder()); //进行日志记录,输出日志格式 // ch.pipeline().addLast(LOGGING_HANDLER); //作用:将入站消息进行编码转换成为符合协议的ByteBuf ch.pipeline().addLast(MESSAGE_CODEC); //客户端向服务端发送心跳包 // 用来判断是不是 读空闲时间过长,或 写空闲时间过长 // 3s 内如果没有向服务器写数据,会触发一个 IdleState#WRITER_IDLE 事件 ch.pipeline().addLast(new IdleStateHandler(0, 3, 0)); // ChannelDuplexHandler 可以同时作为入站和出站处理器 ch.pipeline().addLast(new ChannelDuplexHandler() { // 用来触发特殊事件 @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{ IdleStateEvent event = (IdleStateEvent) evt; // 触发了写空闲事件 if (event.state() == IdleState.WRITER_IDLE) { // log.debug("3s 没有写数据了,发送一个心跳包"); ctx.writeAndFlush(new PingMessage()); } } }); ch.pipeline().addLast("client handler",new ChannelInboundHandlerAdapter(){ //接收服务器端向客户端返回的响应信息 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { log.debug("msg:{}",msg); if((msg instanceof LoginResponseMessage)){ LoginResponseMessage response = (LoginResponseMessage)msg; if(response.isSuccess()){ //如果登录成功 LOGIN.set(true); } //唤醒system in 线程 //让引用计数减一 WAIT_FOR_LOGIN.countDown(); } } //在连接建立后触发active事件 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { //在创建一个线程 //负责接收用户在控制台的输入,负责向服务器发送各种消息 new Thread(() -> { System.out.println("请输入用户名:"); if(EXIT.get()){ return; } String userName = scanner.nextLine(); System.out.println("请输入密码:"); String password = scanner.nextLine(); if(EXIT.get()){ return; } //构造消息对象 LoginRequestMessage message = new LoginRequestMessage(userName,password); //发送消息 ctx.writeAndFlush(message); System.out.println("等待后续操作..."); try { WAIT_FOR_LOGIN.await(); } catch (InterruptedException e) { e.printStackTrace(); } //如果登录失败 if(!LOGIN.get()){ //关闭连接通道 ctx.channel().close(); return; } while (true){ System.out.println("=================================="); System.out.println("send [username] [content]"); System.out.println("gsend [group name] [content]"); System.out.println("gcreate [group name] [m1,m2,m3...]"); System.out.println("gmembers [group name]"); System.out.println("gjoin [group name]"); System.out.println("gquit [group name]"); System.out.println("quit"); System.out.println("=================================="); //处理业务消息 String command = scanner.nextLine(); if(EXIT.get()){ return; } //通过空格分割,获取到控制台输入数据 String[] s = command.split(" "); switch (s[0]){ case "send": //将消息发送到服务端 ctx.writeAndFlush(new ChatRequestMessage(userName,s[1],s[2])); break; case "gsend": ctx.writeAndFlush(new GroupChatRequestMessage(userName,s[1],s[2])); break; case "gcreate": String[] m = s[2].split(","); List
asList = Arrays.asList(m); Set
set = new HashSet<>(asList); set.add(userName); //加入自己 ctx.writeAndFlush(new GroupCreateRequestMessage(s[1],set)); break; case "gmembers": ctx.writeAndFlush(new GroupMembersRequestMessage(s[1])); break; case "gjoin": ctx.writeAndFlush(new GroupJoinRequestMessage(userName, s[1])); break; case "gquit": ctx.writeAndFlush(new GroupQuitRequestMessage(userName, s[1])); break; case "quit": ctx.channel().close(); break; } } },"system in").start(); } // 在连接断开时触发 @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { log.debug("连接已经断开,按任意键退出.."); EXIT.set(true); } // 在出现异常时触发 @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { log.debug("连接已经断开,按任意键退出..{}", cause.getMessage()); EXIT.set(true); } }); } }); //sync:Channel方法,同步等待Channel关闭,在向下执行 Channel channel = bootstrap.connect("localhost", 8080).sync().channel(); //closeFuture:处理 Channel 的关闭 channel.closeFuture().sync(); } catch (Exception e) { log.error("client error", e); } finally { group.shutdownGracefully(); } } } ``` ### 三、聊天室业务-单聊 服务器端将 handler 独立出来 + 登录 handler ```java import cn.itcast.message.LoginRequestMessage; import cn.itcast.message.LoginResponseMessage; import cn.itcast.server.service.UserServiceFactory; import cn.itcast.server.session.SessionFactory; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; /** * Created by lilinchao * Date 2022/6/19 * Description 登录消息处理Handler */ @ChannelHandler.Sharable public class LoginRequestMessageHandler extends SimpleChannelInboundHandler
{ @Override protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage msg) throws Exception { String username = msg.getUsername(); String password = msg.getPassword(); //通过用户名和密码进行登录校验 boolean login = UserServiceFactory.getUserService().login(username, password); LoginResponseMessage message; //向客户端返回消息 if (login) { //登录成功 //bind方法将用户名和channel对应起来,可以根据用户名找到对应的channel SessionFactory.getSession().bind(ctx.channel(), username); message = new LoginResponseMessage(true, "登录成功"); } else { message = new LoginResponseMessage(false, "用户名或密码不正确"); } ctx.writeAndFlush(message); } } ``` + 单聊handler ```java import cn.itcast.message.ChatRequestMessage; import cn.itcast.message.ChatResponseMessage; import cn.itcast.server.session.SessionFactory; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; /** * Created by lilinchao * Date 2022/6/19 * Description 聊天信息处理Handler */ @ChannelHandler.Sharable public class ChatRequestMessageHandler extends SimpleChannelInboundHandler
{ @Override protected void channelRead0(ChannelHandlerContext ctx, ChatRequestMessage msg) throws Exception { String to = msg.getTo(); //根据用户名找到对应的channel Channel channel = SessionFactory.getSession().getChannel(to); //在线 if(channel != null){ channel.writeAndFlush(new ChatResponseMessage(msg.getFrom(),msg.getContent())); }else { //不在线 channel.writeAndFlush(new ChatResponseMessage(false,"对方用户不存在或不在线")); } } } ``` ### 四、聊天室业务-群聊 + 创建群聊 ```java import cn.itcast.message.GroupCreateRequestMessage; import cn.itcast.message.GroupCreateResponseMessage; import cn.itcast.server.session.Group; import cn.itcast.server.session.GroupSession; import cn.itcast.server.session.GroupSessionFactory; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import java.util.List; import java.util.Set; /** * Created by lilinchao * Date 2022/6/19 * Description 创建群聊Handler */ @ChannelHandler.Sharable public class GroupCreateRequestMessageHandler extends SimpleChannelInboundHandler
{ @Override protected void channelRead0(ChannelHandlerContext ctx, GroupCreateRequestMessage msg) throws Exception { String groupName = msg.getGroupName(); Set
members = msg.getMembers(); //群管理 GroupSession groupSession = GroupSessionFactory.getGroupSession(); Group group = groupSession.createGroup(groupName, members); if(group == null){ //发送成功消息 ctx.writeAndFlush(new GroupCreateResponseMessage(true,groupName + " 创建成功")); //向每一个被拉入群的用户发送拉群消息 List
channels = groupSession.getMembersChannel(groupName); for (Channel channel:channels){ channel.writeAndFlush(new GroupCreateResponseMessage(true, "您已被拉入" + groupName)); } }else { ctx.writeAndFlush(new GroupCreateResponseMessage(false, groupName + "已经存在")); } } } ``` + 加入群聊 ```java import cn.itcast.message.GroupJoinRequestMessage; import cn.itcast.message.GroupJoinResponseMessage; import cn.itcast.server.session.GroupSession; import cn.itcast.server.session.GroupSessionFactory; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import java.util.Set; /** * Created by lilinchao * Date 2022/6/19 * Description 加入群聊Handler */ @ChannelHandler.Sharable public class GroupJoinRequestMessageHandler extends SimpleChannelInboundHandler
{ @Override protected void channelRead0(ChannelHandlerContext ctx, GroupJoinRequestMessage msg) throws Exception { GroupSession groupSession = GroupSessionFactory.getGroupSession(); // 判断该用户是否在群聊中 Set
members = groupSession.getMembers(msg.getGroupName()); boolean joinFlag = false; // 群聊存在且用户未加入,才能加入 if (!members.contains(msg.getUsername()) && groupSession.isCreated(msg.getGroupName())) { joinFlag = true; } if (joinFlag) { // 加入群聊 groupSession.joinMember(msg.getGroupName(), msg.getUsername()); ctx.writeAndFlush(new GroupJoinResponseMessage(true,"加入"+msg.getGroupName()+"成功")); } else { ctx.writeAndFlush(new GroupJoinResponseMessage(false, "加入失败,群聊未存在或您已加入该群聊")); } } } ``` + 退出群聊 ```java import cn.itcast.message.GroupQuitRequestMessage; import cn.itcast.message.GroupQuitResponseMessage; import cn.itcast.server.session.GroupSession; import cn.itcast.server.session.GroupSessionFactory; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import java.util.Set; /** * Created by lilinchao * Date 2022/6/19 * Description 退出群聊Handler */ @ChannelHandler.Sharable public class GroupQuitRequestMessageHandler extends SimpleChannelInboundHandler
{ @Override protected void channelRead0(ChannelHandlerContext ctx, GroupQuitRequestMessage msg) throws Exception { GroupSession groupSession = GroupSessionFactory.getGroupSession(); String groupName = msg.getGroupName(); Set
members = groupSession.getMembers(groupName); String username = msg.getUsername(); // 判断用户是否在群聊中以及群聊是否存在 boolean joinFlag = false; if (groupSession.isCreated(groupName) && members.contains(username)) { // 可以退出 joinFlag = true; } if (joinFlag) { // 退出成功 groupSession.removeMember(groupName, username); ctx.writeAndFlush(new GroupQuitResponseMessage(true, "退出"+groupName+"成功")); } else { // 退出失败 ctx.writeAndFlush(new GroupQuitResponseMessage(false, "群聊不存在或您未加入该群,退出"+groupName+"失败")); } } } ``` + 查看成员 ```java import cn.itcast.message.GroupMembersRequestMessage; import cn.itcast.message.GroupMembersResponseMessage; import cn.itcast.server.session.GroupSessionFactory; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; /** * Created by lilinchao * Date 2022/6/19 * Description 查看成员Handler */ @ChannelHandler.Sharable public class GroupMembersRequestMessageHandler extends SimpleChannelInboundHandler
{ @Override protected void channelRead0(ChannelHandlerContext ctx, GroupMembersRequestMessage msg) throws Exception { ctx.writeAndFlush(new GroupMembersResponseMessage(GroupSessionFactory.getGroupSession().getMembers(msg.getGroupName()))); } } ``` ### 五、聊天室业务-退出 ```java import cn.itcast.server.session.SessionFactory; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import lombok.extern.slf4j.Slf4j; /** * 退出聊天 */ @Slf4j @ChannelHandler.Sharable public class QuitHandler extends ChannelInboundHandlerAdapter { // 当连接断开时触发 inactive 事件 @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { SessionFactory.getSession().unbind(ctx.channel()); log.debug("{} 已经断开", ctx.channel()); } // 当出现异常时触发 @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { SessionFactory.getSession().unbind(ctx.channel()); log.debug("{} 已经异常断开 异常是{}", ctx.channel(), cause.getMessage()); } } ``` ### 六、聊天室业务-空闲检测 #### 连接假死 **原因** * 网络设备出现故障,例如网卡,机房等,底层的 TCP 连接已经断开了,但应用程序没有感知到,仍然占用着资源。 * 公网网络不稳定,出现丢包。如果连续出现丢包,这时现象就是客户端数据发不出去,服务端也一直收不到数据,就这么一直耗着 * 应用程序线程阻塞,无法进行数据读写 **问题** * 假死的连接占用的资源不能自动释放 * 向假死的连接发送数据,得到的反馈是发送超时 **服务器端解决** * 可以添加`IdleStateHandler`对空闲时间进行检测,通过构造函数可以传入三个参数 - readerIdleTimeSeconds 读空闲经过的秒数 - writerIdleTimeSeconds 写空闲经过的秒数 - allIdleTimeSeconds 读和写空闲经过的秒数 当指定时间内未发生读或写事件时,**会触发特定事件** - 读空闲会触发`READER_IDLE` - 写空闲会触发`WRITE_IDLE` - 读和写空闲会触发`ALL_IDEL` 想要处理这些事件,**需要自定义事件处理函数** ```java // 用来判断是不是 读空闲时间过长,或 写空闲时间过长 // 5s 内如果没有收到 channel 的数据,会触发一个 IdleState#READER_IDLE 事件 ch.pipeline().addLast(new IdleStateHandler(5, 0, 0)); // ChannelDuplexHandler 可以同时作为入站和出站处理器 ch.pipeline().addLast(new ChannelDuplexHandler() { // 用来触发特殊事件 @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{ IdleStateEvent event = (IdleStateEvent) evt; // 触发了读空闲事件 if (event.state() == IdleState.READER_IDLE) { log.debug("已经 5s 没有读到数据了"); ctx.channel().close(); } } }); ``` - 使用`IdleStateHandler`进行空闲检测 - 使用双向处理器`ChannelDuplexHandler`对入站与出站事件进行处理 - `IdleStateHandler`中的事件为特殊事件,需要实现`ChannelDuplexHandler`的`userEventTriggered`方法,判断事件类型并自定义处理方式,来对事件进行处理 为**避免因非网络等原因引发的READ_IDLE事件**,比如网络情况良好,只是用户本身没有输入数据,这时发生READ_IDLE事件,**直接让服务器断开连接是不可取的** 为避免此类情况,需要在**客户端向服务器发送心跳包**,发送频率要**小于**服务器设置的`IdleTimeSeconds`,一般设置为其值的一半 **客户端定时心跳** * 客户端可以定时向服务器端发送数据,只要这个时间间隔小于服务器定义的空闲检测的时间间隔,那么就能防止前面提到的误判,客户端可以定义如下心跳处理器 ```java // 用来判断是不是 读空闲时间过长,或 写空闲时间过长 // 3s 内如果没有向服务器写数据,会触发一个 IdleState#WRITER_IDLE 事件 ch.pipeline().addLast(new IdleStateHandler(0, 3, 0)); // ChannelDuplexHandler 可以同时作为入站和出站处理器 ch.pipeline().addLast(new ChannelDuplexHandler() { // 用来触发特殊事件 @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{ IdleStateEvent event = (IdleStateEvent) evt; // 触发了写空闲事件 if (event.state() == IdleState.WRITER_IDLE) { // log.debug("3s 没有写数据了,发送一个心跳包"); ctx.writeAndFlush(new PingMessage()); } } }); ``` ### 目录结构 ![07.Netty进阶之聊天室案例01.jpg](https://lilinchao.com/usr/uploads/2022/06/2740581719.jpg) ![07.Netty进阶之聊天室案例02.jpg](https://lilinchao.com/usr/uploads/2022/06/3436607235.jpg) - client包:存放客户端相关类 - message包:存放各种类型的消息 - protocol包:存放自定义协议 - server包:存放服务器相关类 - service包:存放用户相关类 - session包:单聊及群聊相关会话类 *附参考文章链接* *《黑马程序员Netty教程》* [*《Netty在线聊天室》*](https://nyimac.gitee.io/2021/04/25/Netty%E5%9F%BA%E7%A1%80/#3%E3%80%81%E5%9C%A8%E7%BA%BF%E8%81%8A%E5%A4%A9%E5%AE%A4)
标签:
Netty
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2178.html
上一篇
06.Netty进阶之Sharable注解
下一篇
01.Netty优化之扩展序列化算法
评论已关闭
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
NLP
4
标签云
Java编程思想
Filter
Elastisearch
线程池
Python
LeetCode刷题
Spark Streaming
MyBatis-Plus
Spring
JVM
pytorch
NIO
DataX
Golang基础
高并发
Flume
Java工具类
Quartz
工具
Java
SpringCloud
Redis
序列化和反序列化
Spark Core
稀疏数组
Linux
Docker
哈希表
随笔
正则表达式
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭