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面向对象 --- 吃货联盟订餐系统(完整版)]( )  
> 
> 
> 
相关推荐
Excuse_lighttime18 分钟前
HTTP / HTTPS 协议
网络·网络协议·http·https
z人间防沉迷k20 分钟前
TCP核心机制
网络·网络协议·tcp/ip·http
chennalC#c.h.JA Ptho1 小时前
ubuntu studio 系统详解
linux·运维·服务器·经验分享·ubuntu·系统安全
像风一样_2 小时前
TCP首部格式及三次握手四次挥手
网络·网络协议·tcp/ip
{{uname}}7 小时前
利用WebSocket实现实时通知
网络·spring boot·websocket·网络协议
Vone_668 小时前
node.js 邮箱验证服务器搭建
运维·服务器·node.js
丢丢丢丢丢丢~8 小时前
apache2的默认html修改
linux·运维·服务器
wusam8 小时前
Linux系统管理与编程20:Apache
linux·运维·服务器·apache·shell编程
我不想当小卡拉米9 小时前
【Linux】操作系统入门:冯诺依曼体系结构
linux·开发语言·网络·c++
思科小白白9 小时前
【无标题】
网络·智能路由器