Netty进阶 -- 非阻塞网络编程 实现群聊+私聊+心跳检测系统_netty和非阻塞网络编程

复制代码
/\*\*

* 客户端在指定时间内未触发相应操作执行此方法,即认为与客户端断开连接

* @param ctx 全局上下文对象

* @param evt 事件

* @throws Exception 发生异常时抛出

*/

@Override

public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

//判断当前事件是否为IdleStateEvent

if (evt instanceof IdleStateEvent) {

//将evt强转为IdleStateEvent

IdleStateEvent event = (IdleStateEvent) evt;

//判断到底发生的事件是什么

String eventType = null;

//由于IdleStateEvent底层判断事件是根据枚举类型来的,所以直接判断即可

switch (event.state()) {

case READER_IDLE:

eventType = "读空闲";

break;

case WRITER_IDLE:

eventType = "写空闲";

break;

case ALL_IDLE:

eventType = "读写空闲";

break;

}

复制代码
        System.out.println(ctx.channel().remoteAddress() + "发生超时事件,事件类型为:" + eventType);
        System.out.println("服务器做相应处理");
    }
}


**心跳检测机制就是这样,简单来说,就是每隔一段时间去检测客户端是否与服务器连接,如果无连接,那么就断开,从而节省服务器的资源**


## 三、需求分析


### 🚝多人群聊


利用map集合,Map<String, Channel> 里面存入当前在线的所有用户,继承 SimpleChannelInboundHandler 处理器 并在对应的处理器进行添加通道到map


然后实现处理器的channelRead0方法进行转发数据,这就简单的实现了多人群聊  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/0658854366a041e78a02feaa0c421eb6.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQnVnIOe7iOe7k-iAhQ==,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)


### 🚝单人私聊


单人私聊与多人群聊类似,也是在channelRead0方法内进行判断是否为私聊用户,私聊用户输入#端口号#要发送的内容,即可简单检测到本次消息为私聊,并从map中取出对应的key,拿出key对应的channel,进行转发,即可完成私聊


![在这里插入图片描述](https://img-blog.csdnimg.cn/5f1fa89fecd5403eb9f3a2fc836deb50.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQnVnIOe7iOe7k-iAhQ==,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)


**接受消息,其它用户不会看到此私聊消息**


### 🚝服务器检测用户上线、离线


**服务器端检测用户当前的状态,实现对应的方法进行相应的提示即可**


* 实现handlerAdded检测某个用户加入聊天,
* 实现channelActive表示channel处于活跃状态,即上线
* 实现channelInactive表示channel处于非活跃状态,即离线,
* 实现 handlerRemoved 表示离线


![在这里插入图片描述](https://img-blog.csdnimg.cn/c76d3bbf1ff74f069254c61b568ae2a0.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQnVnIOe7iOe7k-iAhQ==,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)


## 四、效果图


![在这里插入图片描述](https://img-blog.csdnimg.cn/16c97b6f38e6442faa71e7fb59eb4956.gif#pic_center)


## 五、核心源码


**GroupChatServer服务器端代码**

package com.wanshi.netty.groupchat;

import com.wanshi.netty.heartbeat.HeartbeatServerHandler;

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.string.StringDecoder;

import io.netty.handler.codec.string.StringEncoder;

import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;

public class GroupChatServer {

复制代码
// 监听端口
private int port;

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

//编写run方法,处理客户端的请求
public void run() {

    //创建两个线程组
    EventLoopGroup bossGroup = new NioEventLoopGroup(1);
    //Nio核数 \* 2
    EventLoopGroup workerGroup = new NioEventLoopGroup();

    ServerBootstrap bootstrap = new ServerBootstrap();

    try {
        bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 128)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childHandler(new ChannelInitializer<SocketChannel>() {

                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        //获取pipeline
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        //向pipeline加入解码器
                        pipeline.addLast("decoder", new StringDecoder());
                        //向pipeline加入编码器
                        pipeline.addLast("encoder", new StringEncoder());
                        //加入自己的业务处理handler
                        pipeline.addLast(new GroupChatServerHandler());
                        //加入心跳检测机制
                        pipeline.addLast(new IdleStateHandler(3, 5, 7, TimeUnit.SECONDS));
                        pipeline.addLast(new HeartbeatServerHandler());
                    }
                });

        System.out.println("netty 服务器启动");
        ChannelFuture future = bootstrap.bind(port).sync();
        //监听关闭事件
        future.channel().closeFuture().sync();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }
}

public static void main(String[] args) {
    GroupChatServer groupChatServer = new GroupChatServer(7000);
    groupChatServer.run();
}

}

复制代码
**GroupChatServerHandler 服务器自定义handler**

package com.wanshi.netty.groupchat;

import io.netty.channel.Channel;

import io.netty.channel.ChannelHandlerContext;

import io.netty.channel.SimpleChannelInboundHandler;

import java.text.SimpleDateFormat;

import java.util.Date;

import java.util.HashMap;

import java.util.Map;

public class GroupChatServerHandler extends SimpleChannelInboundHandler {

复制代码
//所有的channel存入map集合中,目的是为了私聊好获取用户
private static Map<String,Channel> allChannels = new HashMap<String,Channel>();

//格式化所有日期时间
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

//转化日期
private String currentDate = sdf.format(new Date());

/\*\*

* handlerAdded 表示连接建立,一旦连接建立,第一个被执行

* 将当前channel加入到map集合

*/

@Override

public void handlerAdded(ChannelHandlerContext ctx) throws Exception {

//获取当前channel

Channel channel = ctx.channel();

//推送客户加入聊天的信息推送给其它在线的客户端

//该方法会将channelGroup中所有的channel遍历并发送消息

allChannels.forEach((k, ch) ->{

ch.writeAndFlush(currentDate+" \n [客户端]" + channel.remoteAddress() + "加入聊天\n");

});

//获取端口号

String key = channel.remoteAddress().toString().split(":")[1];

allChannels.put(key, channel);

}

复制代码
/\*\*

* 表示断开连接了,将xx客户离开信息推送给当前在线的客户

* @param ctx

* @throws Exception

*/

@Override

public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

//获取当前channel

Channel channel = ctx.channel();

//推送客户加入聊天的信息推送给其它在线的客户端

//该方法会将map中所有的channel遍历并发送消息

allChannels.forEach((k, ch) ->{

ch.writeAndFlush(currentDate+" \n [客户端]" + channel.remoteAddress() + "离线\n");

});

System.out.println("当前在线人数:" + allChannels.size());

}

复制代码
/\*\*

* 读取数据并将数据转发给在线的客户端

* @param channelHandlerContext

* @param s

* @throws Exception

*/

@Override

protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {

//获取到当前channel

Channel channel = channelHandlerContext.channel();

复制代码
    //私聊用户发送消息
    if(s.contains("#")){
        String id = s.split("#")[1];
        String body = s.split("#")[2];
        Channel userChannel = allChannels.get(id);
        String key = channel.remoteAddress().toString().split(":")[1];
        userChannel.writeAndFlush(currentDate+"\n "+key+"【私聊】 [用户] "+id+" 说 : "+body);
        return;
    }

    //循环遍历hashmap集合进行转发消息
    allChannels.forEach((k, ch) -> {
        if (channel != ch) {
            ch.writeAndFlush(currentDate + " \n [客户端]" + channel.remoteAddress() + ":" + s + "\n");
        } else { // 发送消息给自己,回显自己发送的消息
            channel.writeAndFlush(currentDate + " \n [我]:" + s + "\n");
        }
    });

}

/\*\*

* 表示channel处于活动状态

* @param ctx

* @throws Exception

*/

@Override

public void channelActive(ChannelHandlerContext ctx) throws Exception {

System.out.println(currentDate + " -- " + ctx.channel().remoteAddress() + "上线~");

}

复制代码
/\*\*

* 失去连接时会触发此方法

* @param ctx 全局上下文对象

* @throws Exception

*/

@Override

public void channelInactive(ChannelHandlerContext ctx) throws Exception {

Channel channel = ctx.channel();

String key = channel.remoteAddress().toString().split(":")[1];

allChannels.remove(key);

System.out.println(currentDate + " -- " + ctx.channel().remoteAddress() + "离线");

}

复制代码
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    //关闭
    ctx.close();
}

}

复制代码
**自定义心跳处理器 -- HeartbeatServerHandler**

package com.wanshi.netty.heartbeat;

import io.netty.channel.ChannelHandlerContext;

import io.netty.channel.ChannelInboundHandlerAdapter;

import io.netty.handler.timeout.IdleStateEvent;

public class HeartbeatServerHandler extends ChannelInboundHandlerAdapter {

复制代码
/\*\*

* 客户端在指定时间内未触发相应操作执行此方法,即认为与客户端断开连接

* @param ctx 全局上下文对象

* @param evt 事件

* @throws Exception 发生异常时抛出

*/

@Override

public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

//判断当前事件是否为IdleStateEvent

if (evt instanceof IdleStateEvent) {

//将evt强转为IdleStateEvent

IdleStateEvent event = (IdleStateEvent) evt;

//判断到底发生的事件是什么

String eventType = null;

//由于IdleStateEvent底层判断事件是根据枚举类型来的,所以直接判断即可

switch (event.state()) {

case READER_IDLE:

eventType = "读空闲";

break;

case WRITER_IDLE:

eventType = "写空闲";

break;

case ALL_IDLE:

eventType = "读写空闲";

break;

}

复制代码
        System.out.println(ctx.channel().remoteAddress() + "发生超时事件,事件类型为:" + eventType);
        System.out.println("服务器做相应处理");
    }
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    System.out.println("发生异常!");
}

}

复制代码
**GroupChatClient 客户端**

package com.wanshi.netty.groupchat;

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.string.StringDecoder;

import io.netty.handler.codec.string.StringEncoder;

import java.util.Scanner;

public class GroupChatClient {

复制代码
//定义属性
private final String host;
public final int port;


public GroupChatClient(String host, int port) {
    this.host = host;
    this.port = port;
}

public void run () {
    EventLoopGroup eventExecutors = new NioEventLoopGroup();

    Bootstrap bootstrap = new Bootstrap();

    try {
        bootstrap.group(eventExecutors)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        //得到pipeline
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        //加入相关的handler
                        pipeline.addLast("decoder", new StringDecoder());
                        pipeline.addLast("encoder", new StringEncoder());
                        //加入自定义handler
                        pipeline.addLast(new GroupChatClientHandler());
                    }
                });
        ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
        //得到channel
        Channel channel = channelFuture.channel();
        System.out.println("-----" + channel.localAddress() + "----");
        //客户端需要输入信息,创建一个扫描器
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String msg = scanner.nextLine();
            //通过channel发送到服务器端
            channel.writeAndFlush(msg+"\r\n");
        }
        channelFuture.channel().closeFuture().sync();
    } catch (Exception e) {

    } finally {
        eventExecutors.shutdownGracefully();
    }
}

public static void main(String[] args) {
    GroupChatClient groupChatClient = new GroupChatClient("127.0.0.1", 7000);
    groupChatClient.run();
}

}

复制代码
**GroupChatClientHandler 客户端自定义处理器Handler**

package com.wanshi.netty.groupchat;

import io.netty.channel.Channel;

import io.netty.channel.ChannelHandlerContext;

import io.netty.channel.SimpleChannelInboundHandler;

import java.text.SimpleDateFormat;

import java.util.Date;

import java.util.HashMap;

import java.util.Map;

public class GroupChatServerHandler extends SimpleChannelInboundHandler {

复制代码
//所有的channel存入map集合中,目的是为了私聊好获取用户
private static Map<String,Channel> allChannels = new HashMap<String,Channel>();

//格式化所有日期时间
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

//转化日期
private String currentDate = sdf.format(new Date());

/\*\*

* handlerAdded 表示连接建立,一旦连接建立,第一个被执行

* 将当前channel加入到map集合

*/

@Override

public void handlerAdded(ChannelHandlerContext ctx) throws Exception {

//获取当前channel

Channel channel = ctx.channel();

//推送客户加入聊天的信息推送给其它在线的客户端

//该方法会将channelGroup中所有的channel遍历并发送消息

allChannels.forEach((k, ch) ->{

ch.writeAndFlush(currentDate+" \n [客户端]" + channel.remoteAddress() + "加入聊天\n");

});

//获取端口号

String key = channel.remoteAddress().toString().split(":")[1];

allChannels.put(key, channel);

}

复制代码
/\*\*

* 表示断开连接了,将xx客户离开信息推送给当前在线的客户

* @param ctx

* @throws Exception

*/

@Override

public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

//获取当前channel

Channel channel = ctx.channel();

//推送客户加入聊天的信息推送给其它在线的客户端

//该方法会将map中所有的channel遍历并发送消息

allChannels.forEach((k, ch) ->{

ch.writeAndFlush(currentDate+" \n [客户端]" + channel.remoteAddress() + "离线\n");

});

System.out.println("当前在线人数:" + allChannels.size());

}

复制代码
/\*\*

* 读取数据并将数据转发给在线的客户端

* @param channelHandlerContext

* @param s

* @throws Exception

*/

@Override

protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {

//获取到当前channel

Channel channel = channelHandlerContext.channel();

复制代码
    //私聊用户发送消息
    if(s.contains("#")){
        String id = s.split("#")[1];
        String body = s.split("#")[2];
        Channel userChannel = allChannels.get(id);
        String key = channel.remoteAddress().toString().split(":")[1];
        userChannel.writeAndFlush(currentDate+"\n "+key+"【私聊】 [用户] "+id+" 说 : "+body);
        return;
    }

    //循环遍历hashmap集合进行转发消息
    allChannels.forEach((k, ch) -> {
        if (channel != ch) {
            ch.writeAndFlush(currentDate + " \n [客户端]" + channel.remoteAddress() + ":" + s + "\n");
        } else { // 发送消息给自己,回显自己发送的消息
            channel.writeAndFlush(currentDate + " \n [我]:" + s + "\n");
        }
    });

}

/\*\*

* 表示channel处于活动状态

* @param ctx

* @throws Exception

*/

@Override

public void channelActive(ChannelHandlerContext ctx) throws Exception {

System.out.println(currentDate + " -- " + ctx.channel().remoteAddress() + "上线~");

}

复制代码
/\*\*

* 失去连接时会触发此方法

* @param ctx 全局上下文对象

* @throws Exception

*/

@Override

public void channelInactive(ChannelHandlerContext ctx) throws Exception {

Channel channel = ctx.channel();

String key = channel.remoteAddress().toString().split(":")[1];

allChannels.remove(key);

System.out.println(currentDate + " -- " + ctx.channel().remoteAddress() + "离线");

}

复制代码
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    //关闭
    ctx.close();
}

}

复制代码
## 往期精彩热文回顾



> 
> 🚀 **[Netty入门 -- 什么是Netty?]( )**   
>  🚀 **[如何免费使用阿里云服务器?【一篇文章教会你,真香】]( )**  
>  🚀 **[如何使用Git SVN工具 -- TortoiseGit(小乌龟)将本地项目上传至GitEE?【超详细教程】]( )**  
>  🚀 **[前后端分离系列 -- SpringBoot + Spring Security + Vue 实现用户认证 SpringSecurity如此简单]( )**
> 
> 
> 🚀 [Postman测试工具调试接口详细教程【向后端发送Json数据并接收返回的Json结果】]( )
> 
> 
> 🚀 [Java面向对象 --- 吃货联盟订餐系统(完整版)]( )  
> 
> 
> 
相关推荐
BingoGo15 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack15 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
Sinclair2 天前
简单几步,安卓手机秒变服务器,安装 CMS 程序
android·服务器
JaguarJack3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
Rockbean3 天前
用40行代码搭建自己的无服务器OCR
服务器·python·deepseek
茶杯梦轩3 天前
CompletableFuture 在 项目实战 中 创建异步任务 的核心优势及使用场景
服务器·后端·面试
JaguarJack4 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel