Netty制作的聊天室

客户端

package com.zhao.client;

import com.zhao.message.LoginRequestMessage;
import com.zhao.protocol.MessageCodecSharable;
import com.zhao.protocol.ProcotolFrameDecoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
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 lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.Scanner;

@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();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(group);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ProcotolFrameDecoder());
                    ch.pipeline().addLast(LOGGING_HANDLER);
                    ch.pipeline().addLast(MESSAGE_CODEC);


                    ch.pipeline().addLast("client handler",new ChannelInboundHandlerAdapter(){

                        // 接收响应消息
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            log.debug("{}",msg);
                        }

                        // 在连接建立之后触发这个 Active 事件
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            // 开启新的线程发送消息
                            new Thread(()->{
                                Scanner sc = new Scanner(System.in);
                                System.out.println("情输入用户名");
                                String username = sc.nextLine();

                                System.out.println("情输入密码");
                                String password = sc.nextLine();

                                LoginRequestMessage message = new LoginRequestMessage(username,password);

                                // 发送消息
                                ctx.writeAndFlush(message);

                                System.out.println("wait...");
                                try {
                                    System.in.read();
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }

                            },"system in").start();
                        }
                    });
                }
            });
            Channel channel = bootstrap.connect("localhost", 8080).sync().channel();
            channel.closeFuture().sync();
        } catch (Exception e) {
            log.error("client error", e);
        } finally {
            group.shutdownGracefully();
        }
    }
}

服务器端

package com.zhao.server;

import com.zhao.message.LoginRequestMessage;
import com.zhao.message.LoginResponseMessage;
import com.zhao.protocol.MessageCodecSharable;
import com.zhao.protocol.ProcotolFrameDecoder;
import com.zhao.server.service.UserServiceFactory;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.SimpleChannelInboundHandler;
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 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();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ProcotolFrameDecoder());
                    ch.pipeline().addLast(LOGGING_HANDLER);
                    ch.pipeline().addLast(MESSAGE_CODEC);
                    // 只关心 LoginRequestMessage 事件
                    ch.pipeline().addLast(new SimpleChannelInboundHandler<LoginRequestMessage>() {
                        @Override
                        protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage loginRequestMessage) throws Exception {
                            String username = loginRequestMessage.getUsername();
                            String password = loginRequestMessage.getPassword();
                            // 进行用户名和密码验证
                            boolean login = UserServiceFactory.getUserService().login(username, password);
                            LoginResponseMessage message;
                            if (login) {
                                message = new LoginResponseMessage(true, "登录成功");
                            } else {
                                message = new LoginResponseMessage(false, "用户名或密码错误");
                            }

                            // 这条消息会触发出站 handler,先触发 MESSAGE_CODEC 再触发 LOGGING_HANDLER
                            ctx.writeAndFlush(message);

                        }
                    });
                }
            });
            Channel channel = serverBootstrap.bind(8080).sync().channel();
            channel.closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("server error", e);
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

服务器端打印结果

 客户端打印结果

两边接收和发送的字节数都是一致的。并且客户端也打印了服务器响应的消息

但是有一个问题?客户端用的是 nio 线程接收消息并打印的。与此同时新开启的 system in线程还处于等待状态呢。解决方式也很简单,引入 CountDownLatch 计数器,当服务端的消息返回就把计数器 减1,此时等待的线程就可以运行了。

修改客户端后代码

package com.zhao.client;

import com.zhao.message.LoginRequestMessage;
import com.zhao.message.LoginResponseMessage;
import com.zhao.protocol.MessageCodecSharable;
import com.zhao.protocol.ProcotolFrameDecoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
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 lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;

@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();
        CountDownLatch WAIT_FOR_LOGIN = new CountDownLatch(1);

        // 服务器响应状态
        AtomicBoolean LOGIN = new AtomicBoolean(false);
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(group);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ProcotolFrameDecoder());
                    // ch.pipeline().addLast(LOGGING_HANDLER);
                    ch.pipeline().addLast(MESSAGE_CODEC);


                    ch.pipeline().addLast("client handler", new ChannelInboundHandlerAdapter() {

                        // 接收响应消息
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            log.debug("{}", msg);
                            if (msg instanceof LoginResponseMessage) {
                                LoginResponseMessage response = (LoginResponseMessage) msg;

                                // 如果登录成功
                                if (response.isSuccess()) {
                                    LOGIN.set(true);
                                }

                                // 计数器 减1,唤醒 system in 线程
                                WAIT_FOR_LOGIN.countDown();
                            }

                        }

                        // 在连接建立之后触发这个 Active 事件
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            // 开启新的线程发送消息
                            new Thread(() -> {
                                Scanner sc = new Scanner(System.in);
                                System.out.println("情输入用户名");
                                String username = sc.nextLine();

                                System.out.println("情输入密码");
                                String password = sc.nextLine();

                                LoginRequestMessage message = new LoginRequestMessage(username, password);

                                // 发送消息
                                ctx.writeAndFlush(message);

                                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("==================================");

                                    System.out.println("选择菜单");
                                    String line = sc.nextLine();
                                }

                            }, "system in").start();


                        }
                    });
                }
            });
            Channel channel = bootstrap.connect("localhost", 8080).sync().channel();
            channel.closeFuture().sync();
        } catch (Exception e) {
            log.error("client error", e);
        } finally {
            group.shutdownGracefully();
        }
    }
}

客户端结果

如果登录成功,服务端返回 true,修改共享变量,通知 system in 线程继续向下运行。返回 false,system in 线程关闭客户端返回

登录成功后续操作逻辑实现

package com.zhao.client;

import com.zhao.message.*;
import com.zhao.protocol.MessageCodecSharable;
import com.zhao.protocol.ProcotolFrameDecoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
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 lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;

@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();
        CountDownLatch WAIT_FOR_LOGIN = new CountDownLatch(1);
        AtomicBoolean LOGIN = new AtomicBoolean(false);
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(group);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ProcotolFrameDecoder());
                    // ch.pipeline().addLast(LOGGING_HANDLER);
                    ch.pipeline().addLast(MESSAGE_CODEC);


                    ch.pipeline().addLast("client handler", new ChannelInboundHandlerAdapter() {

                        // 接收响应消息
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            log.debug("{}", msg);
                            if (msg instanceof LoginResponseMessage) {
                                LoginResponseMessage response = (LoginResponseMessage) msg;

                                // 如果登录成功
                                if (response.isSuccess()) {
                                    LOGIN.set(true);
                                }

                                // 计数器 减1,唤醒 system in 线程
                                WAIT_FOR_LOGIN.countDown();
                            }

                        }

                        // 在连接建立之后触发这个 Active 事件
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            // 开启新的线程发送消息
                            new Thread(() -> {
                                Scanner sc = new Scanner(System.in);
                                System.out.println("情输入用户名");
                                String username = sc.nextLine();

                                System.out.println("情输入密码");
                                String password = sc.nextLine();

                                LoginRequestMessage message = new LoginRequestMessage(username, password);

                                // 发送消息
                                ctx.writeAndFlush(message);

                                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("==================================");

                                    System.out.println("选择菜单");
                                    String command = sc.nextLine();
                                    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":
                                            Set<String> set = new HashSet<>(Arrays.asList(s[2].split(",")));
                                            ctx.writeAndFlush(new GroupCreateRequestMessage(username, 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();
                                            return;
                                    }
                                }
                            }, "system in").start();


                        }
                    });
                }
            });
            Channel channel = bootstrap.connect("localhost", 8080).sync().channel();
            channel.closeFuture().sync();
        } catch (Exception e) {
            log.error("client error", e);
        } finally {
            group.shutdownGracefully();
        }
    }
}

对服务端的改进,当有用户登录上来后,就存储 用户名和用户对应的 channel。

// 把 Channel 和 登录用户进行绑定
SessionFactory.getSession().bind(ctx.channel(), username);

此时服务器的 handler 已显得足够臃肿。为了美观,在这里来一些骚操作

我想把 SimpleChannelInboundHandler 这个 handler 单独提取出一个类

 结果

移动内部类

照猫画虎,下面的 handler 全部提取为单独类

当服务器接收到客户端选择的菜单后,handler 所做的操作

package com.zhao.server.handler;

import com.zhao.message.ChatRequestMessage;
import com.zhao.message.ChatResponseMessage;
import com.zhao.server.session.SessionFactory;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

/**
 * @Auther: HackerZhao
 * @Date: 2021/11/18 19:36
 * @Description: 只关注 ChatRequestMessage
 */
@ChannelHandler.Sharable
public class ChatRequestMessageHandler extends SimpleChannelInboundHandler<ChatRequestMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ChatRequestMessage msg) throws Exception {
        String to = msg.getTo();  // 发给谁
        // 如果对方不在线或者未注册,又该怎么办
        // 根据用户名找channel
        Channel channel = SessionFactory.getSession().getChannel(to);
        // 在线
        if (channel != null){
            ctx.writeAndFlush(new ChatResponseMessage(msg.getFrom(),msg.getContent()));
        }else {
            // 不在线
            ctx.writeAndFlush(new ChatResponseMessage(false,"对方用户不存在或不在线"));
        }


    }
}

修改过后的服务端

package com.zhao.server;

import com.zhao.protocol.MessageCodecSharable;
import com.zhao.protocol.ProcotolFrameDecoder;
import com.zhao.server.handler.ChatRequestMessageHandler;
import com.zhao.server.handler.LoginRequestMessageHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
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 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();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ProcotolFrameDecoder());
                    ch.pipeline().addLast(LOGGING_HANDLER);
                    ch.pipeline().addLast(MESSAGE_CODEC);
                    // 只关心 LoginRequestMessage 用户登录事件
                    ch.pipeline().addLast(LOGIN_HANDLER);
                    // 只关心 ChatRequestMessageHandler 发送消息事件
                    ch.pipeline().addLast(CHAT_HANDLER);
                }
            });
            Channel channel = serverBootstrap.bind(8080).sync().channel();
            channel.closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("server error", e);
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }

}

开启两个客户端,登录后发送消息

张三客户端

李四客户端

 李四回复张三

如果只是两个人聊天未免有些单调,所以可以添加 群聊的功能

群聊 handler 代码实现

package com.zhao.server.handler;

import com.zhao.message.GroupCreateRequestMessage;
import com.zhao.message.GroupCreateResponseMessage;
import com.zhao.server.session.Group;
import com.zhao.server.session.GroupSession;
import com.zhao.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;

/**
 * @Auther: HackerZhao
 * @Date: 2021/11/18 20:53
 * @Description: 群聊的 handler
 */
@ChannelHandler.Sharable
public class GroupCreateRequestMessageHandler extends SimpleChannelInboundHandler<GroupCreateRequestMessage> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, GroupCreateRequestMessage msg) throws Exception {
        // 接收客户端传来的 群聊名称
        String groupName = msg.getGroupName();
        // 被邀请的群成员
        Set<String> members = msg.getMembers();
        // 群管理器
        GroupSession groupSession = GroupSessionFactory.getGroupSession();
        // 创建群,如果存在 返回群名,不存在返回 null
        Group group = groupSession.createGroup(groupName, members);
        // 不存在,可以安全创建
        if (group == null) {
            // 向 创建这个群 的用户发送成功消息
            ctx.writeAndFlush(new GroupCreateResponseMessage(true, groupName + "创建成功"));
            // 向 被拉入 的用户发送消息
            List<Channel> channels = groupSession.getMembersChannel(groupName);
            for (Channel channel : channels) {
                channel.writeAndFlush(new GroupCreateResponseMessage(true,"您已被拉入"+groupName));
            }


        }else {
            ctx.writeAndFlush(new GroupCreateResponseMessage(false, groupName + "已经存在"));
        }


    }
}

服务器添加这个 handler

// 只关心 GroupCreateRequestMessageHandler 创建群聊事件
ch.pipeline().addLast(GROUP_CREATE_HANDLER);

启动多个客户端测试群聊功能

张三客户端

李四客户端

王五客户端

张三此时想给群聊发送一条信息,发现自己并没有在群聊里边。建群的时候首先要把 群主拉到群里边。

case "gcreate":
    Set<String> set = new HashSet<>(Arrays.asList(s[2].split(",")));
    // 加入自己
    set.add(username);
    ctx.writeAndFlush(new GroupCreateRequestMessage(s[1], set));
    break;

发起群聊

张三客户端

李四客户端

王五客户端

赵六客户端

客户端的主动退出和异常退出

添加新的 Handler,QuitHandler

测试结果,张三客户端

服务器端

李四客户端,异常退出后,服务器打印结果

大批用户连接服务器后出现 网络设备故障,出现假死状况,服务器 连接数释放不出来,新连接连接不上。该如何处理

Netty 解决假死的手段  

IdleStateHandler
// 用来判断是不是 读空闲时间过长,或写空闲时间过长
// 参数1 读空闲时间过长没收到数据会做出处理
// 参数2 写空闲时间过长没收到数据会做出处理
// 参数3 读写空闲时间过长没收到数据会做出处理
// 5s内没有收到 channel 的数据,就会触发一个 IdleState.READER_IDLE 事件
ch.pipeline().addLast(new IdleStateHandler(5,0,0));

// 处理没有收到数据的事件,处理双向(读写事件)的handler 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 没有读到数据了");
        }
    }
});

启动服务器和客户端,控制台打印

没有收到数据,可以让客户端定时向服务器发送一些无用包,证明自己还活着,俗称“心跳”。

客户端加入上面的代码,3s 向服务器发送一个心跳包

// 3s内没有向服务器写数据,就会触发一个 IdleState.WRITER_IDLE事件,写与读的的间隔大概是 1/2
ch.pipeline().addLast(new IdleStateHandler(0,3,0));
// 处理没有收到数据的事件,处理双向(读写事件)的handler 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());
        }
    }
});

 服务器显示,并没有关闭客户端

客户端

服务器和客户端完整代码

服务器

package com.zhao.server;

import com.zhao.protocol.MessageCodecSharable;
import com.zhao.protocol.ProcotolFrameDecoder;
import com.zhao.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();
        GroupChatRequestMessageHandler GROUP_CHAT_HANDLER = new GroupChatRequestMessageHandler();
        GroupJoinRequestMessageHandler GROUP_JOIN_HANDLER = new GroupJoinRequestMessageHandler();
        GroupMembersRequestMessageHandler GROUP_MEMBERS_HANDLER = new GroupMembersRequestMessageHandler();
        GroupQuitRequestMessageHandler GROUP_QUIT_HANDLER = new GroupQuitRequestMessageHandler();
        QuitHandler QUIT_HANDLER = new QuitHandler();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ProcotolFrameDecoder());
                    ch.pipeline().addLast(LOGGING_HANDLER);
                    ch.pipeline().addLast(MESSAGE_CODEC);

                    // 用来判断是不是 读空闲时间过长,或写空闲时间过长
                    // 参数1 读空闲时间过长没收到数据会做出处理
                    // 参数2 写空闲时间过长没收到数据会做出处理
                    // 参数3 读写空闲时间过长没收到数据会做出处理
                    // 5s内没有收到 channel 的数据,就会触发一个 IdleState.READER_IDLE 事件
                    ch.pipeline().addLast(new IdleStateHandler(5,0,0));
                    // 处理没有收到数据的事件,处理双向(读写事件)的handler 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 没有读到数据了");
                            }
                        }
                    });

                    // 只关心 LoginRequestMessage 用户登录事件
                    ch.pipeline().addLast(LOGIN_HANDLER);
                    // 只关心 ChatRequestMessageHandler 发送消息事件
                    ch.pipeline().addLast(CHAT_HANDLER);
                    // 只关心 GroupCreateRequestMessageHandler 创建群聊事件
                    ch.pipeline().addLast(GROUP_CREATE_HANDLER);
                    // 只关心 GroupChatRequestMessage 向群聊发送消息事件
                    ch.pipeline().addLast(GROUP_CHAT_HANDLER);
                    // 当连接断开时移除 channel
                    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();
        }
    }

}

客户端

package com.zhao.client;

import com.zhao.message.*;
import com.zhao.protocol.MessageCodecSharable;
import com.zhao.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.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;

@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();
        CountDownLatch WAIT_FOR_LOGIN = new CountDownLatch(1);
        AtomicBoolean LOGIN = new AtomicBoolean(false);
        AtomicBoolean EXIT = new AtomicBoolean(false);

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(group);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ProcotolFrameDecoder());
                    // ch.pipeline().addLast(LOGGING_HANDLER);
                    ch.pipeline().addLast(MESSAGE_CODEC);

                    // 用来判断是不是 写空闲时间过长
                    // 参数1 读空闲时间过长没收到数据会做出处理
                    // 参数2 写空闲时间过长没收到数据会做出处理
                    // 参数3 读写空闲时间过长没收到数据会做出处理
                    // 3s内没有向服务器写数据,就会触发一个 IdleState.WRITER_IDLE事件,写与读的的间隔大概是 1/2
                    ch.pipeline().addLast(new IdleStateHandler(0,3,0));
                    // 处理没有收到数据的事件,处理双向(读写事件)的handler 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);
                            System.out.println(msg);
                            if (msg instanceof LoginResponseMessage) {
                                LoginResponseMessage response = (LoginResponseMessage) msg;

                                // 如果登录成功
                                if (response.isSuccess()) {
                                    LOGIN.set(true);
                                }

                                // 计数器 减1,唤醒 system in 线程
                                WAIT_FOR_LOGIN.countDown();
                            }

                        }

                        // 在连接建立之后触发这个 Active 事件
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            // 开启新的线程发送消息
                            new Thread(() -> {
                                Scanner sc = new Scanner(System.in);
                                System.out.println("请输入用户名");
                                String username = sc.nextLine();

                                System.out.println("请输入密码");
                                String password = sc.nextLine();

                                LoginRequestMessage message = new LoginRequestMessage(username, password);

                                // 发送消息
                                ctx.writeAndFlush(message);

                                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("==================================");

                                    System.out.println("选择菜单");
                                    String command = sc.nextLine();
                                    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":
                                            Set<String> set = new HashSet<>(Arrays.asList(s[2].split(",")));
                                            // 加入自己
                                            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();
                                            return;
                                    }
                                }
                            }, "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);
                }
        });
        Channel channel = bootstrap.connect("localhost", 8080).sync().channel();
        channel.closeFuture().sync();
    } catch(
    Exception e)

    {
        log.error("client error", e);
    } finally

    {
        group.shutdownGracefully();
    }
}
}

百度网盘存放聊天室所有代码,链接在下方

提取码:qr1p​​​​​​​

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海上钢琴师_1900

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值