Netty学习笔记_11(Netty群聊系统)

15 篇文章 0 订阅

一、Netty群聊系统开发要求

  1. 编写一个Netty群聊系统,实现服务器端和客户端之间的数据简单通信(非阻塞)
  2. 实现多人群聊
  3. 服务器端:检测用户上线、离线、转发客户端消息
  4. 客户端:通过channel可以无阻塞发送消息给其他客户,同时可以接收其他客户端发送的消息(服务器转发得到)

二、设计与开发

2-1、服务器端

  1. 首先要设置监听端口,服务器将通过该端口与客户端交换信息,发送和接收数据;
  2. 初始化事件循环组EventLoopGroup,这里初始化两个事件循环组bossGroupworkerGroup。其中,bossGroup主要处理新来的客户端连接,workerGroup主要负责连接建立以后的IO事件;
  3. 初始化Netty的启动类ServerBootStrap,通过链式调用配置参数:【设置事件循环对象:bossGroup用于处理accept事件,workerGroup用于处理已建立连接的IO请求】——【构造serverSocketChannel的工厂类】——【设置serverSocketChannel允许的最大连接队列数】——【设置channel为保持活动状态】——【初始化SocketChannel的业务处理器管道pipeline>>考虑到群聊系统主要进行文字交流,故需要一个编码器,一个解码器和一个自定义转发消息的业务处理器】;
  4. 给服务器启动类绑定端口;
  5. 在main()方法中调用启动类对象的run()方法。
    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;
    
    public class groupChatServer {
        //相关属性
        private int port; //监听端口
    
        public groupChatServer(int port){
            this.port = port;
        }
    
        //编写run()方法,处理客户端请求
        public void run() throws InterruptedException {
            //创建两个eventLoopGroup
            EventLoopGroup bossGroup = new NioEventLoopGroup(1);
            EventLoopGroup workerGroup = new NioEventLoopGroup();
    
            try {
                //
                ServerBootstrap b = new ServerBootstrap();
                b.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 ch) throws Exception {
                                //获取到pipeline
                                ChannelPipeline pipeline = ch.pipeline();
                                //向pipeline里面加入解码器
                                pipeline.addLast("decoder", new StringDecoder());
                                pipeline.addLast("encoder", new StringEncoder());
                                //加入自己的业务处理 handler
                                pipeline.addLast(new groupChatServerHandler());
    
                            }
                        });
    
                System.out.println("Netty服务器已启动");
                ChannelFuture channelFuture = b.bind(port).sync();
    
                //监听关闭事件
                channelFuture.channel().closeFuture().sync();
            }finally {
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
    
            new groupChatServer(7000).run();
        }
    }
    

     

 2-2、服务器自定义业务处理器handler

  1. 1为了方便管理,需要建立一个channel的集合,每当有新的客户端连接时,新建一个channel放入该集合,因此创建一个任务队列的单线程事件执行器GlobalEventExecutor并使用ChannelGroup来管理连接进来的channel。
  2. 自定义的handler需要重写某些方法,满足:
    在新的客户端建立连接时触发,提示客户加入聊天;
    在客户端断开连接时触发,提示客户下线;
    在客户端发送消息时触发,接收消息并将消息转发给其他客户端;
    在发生异常时触发,关闭通道ChannelHandler关联的channel和ChannelFuture(监听器)
    import io.netty.channel.Channel;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    import io.netty.channel.group.ChannelGroup;
    import io.netty.channel.group.DefaultChannelGroup;
    import io.netty.util.concurrent.GlobalEventExecutor;
    
    import java.text.SimpleDateFormat;
    import java.util.HashMap;
    import java.util.Map;
    
    public class groupChatServerHandler extends SimpleChannelInboundHandler<String> {
    
        //定义一个hashmap进行管理
        //public static Map<String,Channel> channels = new HashMap<String,Channel>();
    
        //定义一个channel组管理所有的channel
        /**
         * GlobalEventExecutor.INSTANCE是一个全局的事件执行器,是一个单例
         * */
        private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-DD HH:mm:ss");
    
        //handlerAdded:表示连接建立,一旦连接建立,第一个被执行
        //将当前channel加入到 channelGroup
        @Override
        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
            Channel channel = ctx.channel();
            //将该客户加入聊天的信息推送给其他在线的客户端
            //writeAndFlush:该方法会将 channelGroup 中的所有 channel 遍历,并发送消息,无需自己遍历
            channelGroup.writeAndFlush(sdf.format(new java.util.Date()) + " [客户端]"+channel.remoteAddress()+"加入聊天\n");
            channelGroup.add(channel);
    
            //channels.put("id100",channel);
        }
        //断开连接时触发,将xx客户离开的消息推送给当前在线的客户端,自动将当前channel从channelGroup中移除
        @Override
        public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
            Channel channel = ctx.channel();
            channelGroup.writeAndFlush(sdf.format(new java.util.Date()) + " [客户端]"+channel.remoteAddress()+"离开了\n");
            System.out.println("channelGroup size = "+channelGroup.size());
        }
    
        //表示 channel 处于活动状态,提示 xx 上线
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println(ctx.channel().remoteAddress() + "上线了");
        }
    
        //表示 channel 处于非活动状态,提示 xx 离线
        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println(ctx.channel().remoteAddress() + "离线了");
        }
    
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
    
            //获取到当前的channel
            Channel channel = ctx.channel();
            //遍历 channelGroup,根据不同的情况,回送不同的消息
            channelGroup.forEach(ch -> {
                if (channel != ch){  //表示遍历到的channel不是当前的channel,转发消息
                    ch.writeAndFlush("[客户]"+channel.remoteAddress() + " 发送消息:"+ msg + "\n");
                }else {  //回显一下
                    ch.writeAndFlush("自己发送了消息:"+msg+"\n");
                }
            });
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            //关闭
            ctx.close();
        }
    }
    

     

 2-3、客户端

  1. 客户端需要初始化主机和端口两个属性;
  2. 客户端同样需要初始化线程池(EventLoopGroup),但只需要初始化一个进行具体业处理的eventExecutors即可;
  3. 新建客户端的启动类BootStrap,并设置相关参数——【设置事件循环对象:eventExecutors用于处理已建立连接的IO请求】——【构造SocketChannel的工厂类】——【初始化SocketChannel的业务处理器管道pipeline>>考虑到群聊系统主要进行文字交流,故需要一个编码器,一个解码器和一个自定义的业务处理器】
  4. 客户端需要输入消息,故创建一个扫描器scanner用来通过键盘输入消息。
    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;
        private final int port;
    
        public groupChatClient(String host,int port){
            this.host = host;
            this.port = port;
        }
    
        public void run() throws InterruptedException {
            EventLoopGroup eventExecutors = new NioEventLoopGroup();
    
            try {
                Bootstrap bootstrap = new Bootstrap();
    
                bootstrap.group(eventExecutors)
                        .channel(NioSocketChannel.class)
                        .handler(new ChannelInitializer<SocketChannel>() {
    
                            @Override
                            protected void initChannel(SocketChannel ch) throws Exception {
    
                                //得到pipeline
                                ChannelPipeline pipeline = ch.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.remoteAddress() + "-----------");
                //客户端需要输入信息,创建一个扫描器
                Scanner scanner = new Scanner(System.in);
                while (scanner.hasNext()){
                    String msg = scanner.nextLine();
                    //通过channel发送到服务器端
                    channel.writeAndFlush(msg+"\r\n");
                }
            }finally {
                eventExecutors.shutdownGracefully();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
    
            new groupChatClient("127.0.0.1",7000).run();
        }
    }
    
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    
    public class groupChatClientHandler extends SimpleChannelInboundHandler<String> {
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
            System.out.println(msg.trim());
        }
    }
    

     

 三、点对点聊天功能扩展

  1. 要实现点对点的聊天,必须很快地找到每个客户端连接时创建的channel,因此不能再使用ChannelGroup来进行管理,改用hasmap进行管理,规定每个channel对应一个id,Map<String id,Channel channel>;
  2. 依然是通过hashmap进行管理,但可以创建一个User对象,在该该对象内部初始化用户ID,登录密码等属性,可以存入数据库,使每个user实体与channel唯一对应,Map<User user,Channel channel>;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值