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面向对象 --- 吃货联盟订餐系统(完整版)]( )  
> 
> 
> 
相关推荐
web1828548251237 分钟前
【PHP】部署和发布PHP网站到IIS服务器
android·服务器·php
大小科圣38 分钟前
Linux网卡配置方法
linux·运维·服务器
A1-2941 分钟前
TCP三次握手全方面详解
网络·网络协议·tcp/ip
前程似锦蝈蝈42 分钟前
ollama linux下载
linux·运维·服务器·ollama
罗狮粉 9944 分钟前
Ansible在多台服务器上运行python脚本
服务器·python·ansible
网络安全筑盾者-燕子3 小时前
【入门篇】2025年黑客技术之网络安全简介
网络·安全·web安全·网络安全
9527华安3 小时前
FPGA高端项目:实时视频缩放+UltraScale GTH光编码+UDP图传架构,高速接口转网络视频传输,提供工程源码和技术支持
网络·fpga开发·图像缩放·高速接口·ultrascale gth·8b10b
charlie1145141913 小时前
我的年度写作计划
网络·c++·算法·操作系统·嵌入式·计算机架构·博客开发计划
wanhengidc3 小时前
物理服务器的硬件配置都是由哪些构成的?
运维·服务器