使用 netty实现一个简单的聊天室

具体原理:

登录之后服务端把channel和用户名绑定放到集合中,

之后聊天等请求都根据用户名发送到对应的chaael

以下仅展示部分源码,详情请下载以上文件

这里写目录标题

      • 登录接口
      • 会话管理接口
      • 聊天组会话管理接口
      • 服务端和客户端
      • 聊天室业务-单聊
        • 登录 handler
        • 单聊 handler
      • 聊天室业务-群聊
        • 创建群聊 handler
        • 群聊 handler
        • 加入群聊 handler
        • 退出群聊 handler
        • 查看成员 handler
      • 聊天室业务-退出
      • 聊天室业务-空闲检测
        • 连接假死
        • 服务器端解决
        • 客户端定时心跳
      • 效果图

登录接口

/**
 * 用户管理接口
 */
public interface UserService {

    /**
     * 登录
     * @param username 用户名
     * @param password 密码
     * @return 登录成功返回 true, 否则返回 false
     */
    boolean login(String username, String password);
}

会话管理接口

/**
 * 会话管理接口
 */
public interface Session {

    /**
     * 绑定会话
     * @param channel 哪个 channel 要绑定会话
     * @param username 会话绑定用户
     */
    void bind(Channel channel, String username);

    /**
     * 解绑会话
     * @param channel 哪个 channel 要解绑会话
     */
    void unbind(Channel channel);

    /**
     * 获取属性
     * @param channel 哪个 channel
     * @param name 属性名
     * @return 属性值
     */
    Object getAttribute(Channel channel, String name);

    /**
     * 设置属性
     * @param channel 哪个 channel
     * @param name 属性名
     * @param value 属性值
     */
    void setAttribute(Channel channel, String name, Object value);

    /**
     * 根据用户名获取 channel
     * @param username 用户名
     * @return channel
     */
    Channel getChannel(String username);
}

聊天组会话管理接口

/**
 * 聊天组会话管理接口
 */
public interface GroupSession {

    /**
     * 创建一个聊天组, 如果不存在才能创建成功, 否则返回 null
     * @param name 组名
     * @param members 成员
     * @return 成功时返回组对象, 失败返回 null
     */
    Group createGroup(String name, Set<String> members);

    /**
     * 加入聊天组
     * @param name 组名
     * @param member 成员名
     * @return 如果组不存在返回 null, 否则返回组对象
     */
    Group joinMember(String name, String member);

    /**
     * 移除组成员
     * @param name 组名
     * @param member 成员名
     * @return 如果组不存在返回 null, 否则返回组对象
     */
    Group removeMember(String name, String member);

    /**
     * 移除聊天组
     * @param name 组名
     * @return 如果组不存在返回 null, 否则返回组对象
     */
    Group removeGroup(String name);

    /**
     * 获取组成员
     * @param name 组名
     * @return 成员集合, 没有成员会返回 empty set
     */
    Set<String> getMembers(String name);

    /**
     * 获取组成员的 channel 集合, 只有在线的 channel 才会返回
     * @param name 组名
     * @return 成员 channel 集合
     */
    List<Channel> getMembersChannel(String name);
}

服务端和客户端

@Slf4j
public class ChatServer {
    public static void main(String[] args) {
        //利用boss、worker两个线程组来处理网络事件
        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();
        GroupChatRequestMessageHandler GROUPCHAT_HANDLER = new GroupChatRequestMessageHandler();
        GroupCreateRequestMessageHandler GROUPCREATE_HANDLER = new GroupCreateRequestMessageHandler();
        GroupJoinRequestMessageHandler GROUPJOIN_HANDLER = new GroupJoinRequestMessageHandler();
        GroupMembersRequestMessageHandler GROUPMEMBERS_HANDLER = new GroupMembersRequestMessageHandler();
        GroupQuitRequestMessageHandler GROUPQUIT_HANDLER = new GroupQuitRequestMessageHandler();
        QuitHandler QUIT_HANDLER = new QuitHandler();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap()
                    .channel(NioServerSocketChannel.class)
                    .group(boss, worker)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline()
                                    .addLast(new ProcotolFrameDecoder())
                                    .addLast(LOGGING_HANDLER)
                                    .addLast(MESSAGE_CODEC)
                                    .addLast(LOGIN_HANDLER)
                                    .addLast(CHAT_HANDLER)
                                    .addLast(GROUPCHAT_HANDLER)
                                    .addLast(GROUPCREATE_HANDLER)
                                    .addLast(GROUPJOIN_HANDLER)
                                    .addLast(GROUPMEMBERS_HANDLER)
                                    .addLast(GROUPQUIT_HANDLER)
                                    .addLast(QUIT_HANDLER);


                        }
                    });
            Channel channel = serverBootstrap.bind(8888).sync().channel();
            channel.closeFuture().sync(); //主线程阻塞在这里,如果不阻塞,则服务器主线程执行完毕会直接关闭服务器
        } catch (InterruptedException e) {
            log.error("server error", e);
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}
@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()
                    .channel(NioSocketChannel.class)
                    .group(group)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline()
                                    .addLast(new ProcotolFrameDecoder())
                                    .addLast(LOGGING_HANDLER)
                                    .addLast(MESSAGE_CODEC)
                                    .addLast("client handler", new ChannelInboundHandlerAdapter() {
                                        // 在连接建立后触发 active 事件
                                        @Override
                                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                            new Thread(() -> {
                                                Scanner scanner = new Scanner(System.in);
                                                System.out.println("请输入用户名:");
                                                String username = scanner.nextLine();
                                                System.out.println("请输入密码:");
                                                String password = scanner.nextLine();
                                                // 构造消息对象
                                                LoginRequestMessage message = new LoginRequestMessage(username, password);
                                                ctx.writeAndFlush(message);
                                                System.out.println("等待后续操作...");
                                                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("==================================");
                                                    String command = scanner.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 channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//                                            log.debug("msg: {}", msg);
                                            System.out.println("msg:"+msg);
                                            if ((msg instanceof LoginResponseMessage)) {
                                                LoginResponseMessage response = (LoginResponseMessage) msg;
                                                if (response.isSuccess()) {
                                                    // 如果登录成功
                                                    LOGIN.set(true);
                                                }
                                                // CountDownLatch减为0,唤醒阻塞的线程
                                                WAIT_FOR_LOGIN.countDown();
                                            }
                                        }
                                    });
                        }
                    });
            Channel channel = bootstrap.connect("127.0.0.1", 8888)
                    .sync().channel();
            channel.closeFuture().sync(); //主线程阻塞在这里,如果不阻塞,则服务器主线程执行完毕会直接关闭服务器
        } catch (Exception e) {
            log.error("client error", e);
        } finally {
            group.shutdownGracefully();
        }

    }
}

聊天室业务-单聊

服务器端将 handler 独立出来

登录 handler
@ChannelHandler.Sharable
public class LoginRequestMessageHandler extends SimpleChannelInboundHandler<LoginRequestMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, LoginRequestMessage loginRequestMessage) throws Exception {
        String username = loginRequestMessage.getUsername();
        String password = loginRequestMessage.getPassword();

        boolean login = UserServiceFactory.getUserService().login(username, password);
        LoginResponseMessage message;
        if(login){
            SessionFactory.getSession().bind(channelHandlerContext.channel(),username);
            message = new LoginResponseMessage(true,"登录成功");
        }else{
            message = new LoginResponseMessage(false,"用户名或密码不正确");
        }
        channelHandlerContext.writeAndFlush(message);
    }
}
单聊 handler
@ChannelHandler.Sharable
public class ChatRequestMessageHandler extends SimpleChannelInboundHandler<ChatRequestMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ChatRequestMessage chatRequestMessage) throws Exception {
        String chatRequestMessageTo = chatRequestMessage.getTo();
        Channel channel = SessionFactory.getSession().getChannel(chatRequestMessageTo);
        ChatResponseMessage chatResponseMessage;
        if(channel == null){
            chatResponseMessage = new ChatResponseMessage(false,"对方用户不存在或者不在线");
            channelHandlerContext.writeAndFlush(chatResponseMessage);
        }else{
            chatResponseMessage = new ChatResponseMessage(true,chatRequestMessage.getFrom(),chatRequestMessage.getContent());
            channel.writeAndFlush(chatResponseMessage);
        }
    }
}

聊天室业务-群聊

创建群聊 handler
@ChannelHandler.Sharable
public class GroupCreateRequestMessageHandler extends SimpleChannelInboundHandler<GroupCreateRequestMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, GroupCreateRequestMessage groupCreateRequestMessage) throws Exception {
        String groupName = groupCreateRequestMessage.getGroupName();
        Set<String> members = groupCreateRequestMessage.getMembers();
        GroupSession groupSession = GroupSessionFactory.getGroupSession();
        Group group = groupSession.createGroup(groupName, members);
        if(group == null) {
            channelHandlerContext.writeAndFlush(new GroupCreateResponseMessage(false,"群组已存在,创建失败"));
        }else{
            channelHandlerContext.writeAndFlush(new GroupCreateResponseMessage(false,groupName+"群组创建成功"));
            members.forEach(member -> {
                Channel channel = SessionFactory.getSession().getChannel(member);
                channel.writeAndFlush(new GroupCreateResponseMessage(true,"您已被拉入" + groupName+"群组"));

            });
        }
    }
}
群聊 handler
@ChannelHandler.Sharable
public class GroupChatRequestMessageHandler extends SimpleChannelInboundHandler<GroupChatRequestMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, GroupChatRequestMessage groupChatRequestMessage) throws Exception {
        List<Channel> membersChannel = GroupSessionFactory.getGroupSession().getMembersChannel(groupChatRequestMessage.getGroupName());
        membersChannel.forEach(channel -> {
            channel.writeAndFlush(new GroupChatResponseMessage(groupChatRequestMessage.getFrom(),groupChatRequestMessage.getContent()));
        });
    }
}
加入群聊 handler
@ChannelHandler.Sharable
public class GroupJoinRequestMessageHandler extends SimpleChannelInboundHandler<GroupJoinRequestMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, GroupJoinRequestMessage groupJoinRequestMessage) throws Exception {
        Group group = GroupSessionFactory.getGroupSession().joinMember(groupJoinRequestMessage.getGroupName(), groupJoinRequestMessage.getUsername());
        if(group!=null){
            channelHandlerContext.writeAndFlush(new GroupJoinResponseMessage(true,groupJoinRequestMessage.getGroupName() + "群加入成功"));
        }else{
            channelHandlerContext.writeAndFlush(new GroupJoinResponseMessage(false,groupJoinRequestMessage.getGroupName()+ "群不存在"));
        }
    }
}
退出群聊 handler
@ChannelHandler.Sharable
public class GroupQuitRequestMessageHandler extends SimpleChannelInboundHandler<GroupQuitRequestMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, GroupQuitRequestMessage groupQuitRequestMessage) throws Exception {
        Group group = GroupSessionFactory.getGroupSession().removeMember(groupQuitRequestMessage.getGroupName(), groupQuitRequestMessage.getUsername());
        if (group != null) {
            channelHandlerContext.writeAndFlush(new GroupJoinResponseMessage(true, "已退出群" + groupQuitRequestMessage.getGroupName()));
        }else{
            channelHandlerContext.writeAndFlush(new GroupJoinResponseMessage(false, groupQuitRequestMessage.getGroupName() + "群不存在"));
        }

    }
}
查看成员 handler
@ChannelHandler.Sharable
public class GroupMembersRequestMessageHandler extends SimpleChannelInboundHandler<GroupMembersRequestMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, GroupMembersRequestMessage groupMembersRequestMessage) throws Exception {
        Set<String> members = GroupSessionFactory.getGroupSession().getMembers(groupMembersRequestMessage.getGroupName());
        channelHandlerContext.writeAndFlush(new GroupMembersResponseMessage(members));
    }
}

聊天室业务-退出

@Slf4j
@ChannelHandler.Sharable
public class QuitHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        SessionFactory.getSession().unbind(ctx.channel());
        log.debug("{} 已经断开", ctx.channel());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        SessionFactory.getSession().unbind(ctx.channel());
        log.debug("{} 已经异常断开 异常是{}", ctx.channel(), cause.getMessage());
    }
}

聊天室业务-空闲检测

连接假死

原因

  • 网络设备出现故障,例如网卡,机房等,底层的 TCP 连接已经断开了,但应用程序没有感知到,仍然占用着资源。
  • 公网网络不稳定,出现丢包。如果连续出现丢包,这时现象就是客户端数据发不出去,服务端也一直收不到数据,就这么一直耗着
  • 应用程序线程阻塞,无法进行数据读写

问题

  • 假死的连接占用的资源不能自动释放
  • 向假死的连接发送数据,得到的反馈是发送超时
服务器端解决
  • 怎么判断客户端连接是否假死呢?如果能收到客户端数据,说明没有假死。因此策略就可以定为,每隔一段时间就检查这段时间内是否接收到客户端数据,没有就可以判定为连接假死
// 用来判断是不是 读空闲时间过长,或 写空闲时间过长
// 5s 内如果没有收到 channel 的数据,会触发一个 IdleState#READER_IDLE 事件
ch.pipeline().addLast(new IdleStateHandler(5, 0, 0));
// 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 没有读到数据了");
            ctx.channel().close();
        }
    }
});
客户端定时心跳
  • 客户端可以定时向服务器端发送数据,只要这个时间间隔小于服务器定义的空闲检测的时间间隔,那么就能防止前面提到的误判,客户端可以定义如下心跳处理器
// 用来判断是不是 读空闲时间过长,或 写空闲时间过长
// 3s 内如果没有向服务器写数据,会触发一个 IdleState#WRITER_IDLE 事件
ch.pipeline().addLast(new IdleStateHandler(0, 3, 0));
// 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());
        }
    }
});

效果图

使用 netty实现一个简单的聊天室_第1张图片
使用 netty实现一个简单的聊天室_第2张图片

你可能感兴趣的:(netty,java,网络,后端)