聊天室案例

用户登录后实现单聊和群聊功能。

1. 定义消息

按照业务功能的不同:分为登录消息,退出消息,单聊请求消息,单聊回复消息,建群消息,发群消息,回应群消息,进群消息和退群消息,心跳消息:

java
public class LoginRequestMessage extends Message {

    private String username;

    private String password;



    public LoginRequestMessage() {

    }



    public LoginRequestMessage(String username, String password) {

        this.username = username;

        this.password = password;

    }



    public String getUsername() {

        return username;

    }



    public void setUsername(String username) {

        this.username = username;

    }



    public String getPassword() {

        return password;

    }



    public void setPassword(String password) {

        this.password = password;

    }



    @Override

    public int getMessageType() {

        return LoginRequestMessage;

    }

}
java
public class LoginResponseMessage extends AbstractResponseMessage {



    public LoginResponseMessage(boolean success, String reason) {

        super(success, reason);

    }



    @Override

    public int getMessageType() {

        return LoginResponseMessage;

    }

}
java
public class ChatRequestMessage extends Message {



    private String content;

    private String to;

    private String from;



    public ChatRequestMessage() {

    }



    public ChatRequestMessage(String from, String to, String content) {

        this.from = from;

        this.to = to;

        this.content = content;

    }



    public String getContent() {

        return content;

    }



    public void setContent(String content) {

        this.content = content;

    }



    public String getTo() {

        return to;

    }



    public void setTo(String to) {

        this.to = to;

    }



    public String getFrom() {

        return from;

    }



    public void setFrom(String from) {

        this.from = from;

    }



    @Override

    public int getMessageType() {

        return ChatRequestMessage;

    }

}
java
public class ChatResponseMessage extends AbstractResponseMessage {



    private String from;

    private String content;



    public ChatResponseMessage(boolean success, String reason) {

        super(success, reason);

    }



    public ChatResponseMessage(String from, String content) {

        this.from = from;

        this.content = content;

    }



    public String getFrom() {

        return from;

    }



    public void setFrom(String from) {

        this.from = from;

    }



    public String getContent() {

        return content;

    }



    public void setContent(String content) {

        this.content = content;

    }



    @Override

    public int getMessageType() {

        return ChatResponseMessage;

    }

}
java
public class GroupCreateRequestMessage extends Message {

    private String groupName;

    private Set<String> members;



    public GroupCreateRequestMessage(String groupName, Set<String> members) {

        this.groupName = groupName;

        this.members = members;

    }



    public String getGroupName() {

        return groupName;

    }



    public void setGroupName(String groupName) {

        this.groupName = groupName;

    }



    public Set<String> getMembers() {

        return members;

    }



    public void setMembers(Set<String> members) {

        this.members = members;

    }



    @Override

    public int getMessageType() {

        return GroupCreateRequestMessage;

    }

}
java
public class GroupCreateResponseMessage extends AbstractResponseMessage {



    public GroupCreateResponseMessage(boolean success, String reason) {

        super(success, reason);

    }



    @Override

    public int getMessageType() {

        return GroupCreateResponseMessage;

    }

}
java
public class GroupChatRequestMessage extends Message {

    private String content;

    private String groupName;

    private String from;



    public GroupChatRequestMessage(String from, String groupName, String content) {

        this.content = content;

        this.groupName = groupName;

        this.from = from;

    }



    public String getContent() {

        return content;

    }



    public void setContent(String content) {

        this.content = content;

    }



    public String getGroupName() {

        return groupName;

    }



    public void setGroupName(String groupName) {

        this.groupName = groupName;

    }



    public String getFrom() {

        return from;

    }



    public void setFrom(String from) {

        this.from = from;

    }



    @Override

    public int getMessageType() {

        return GroupChatRequestMessage;

    }

}
java
public class GroupChatResponseMessage extends AbstractResponseMessage {

    private String from;

    private String content;



    public GroupChatResponseMessage(boolean success, String reason) {

        super(success, reason);

    }



    public GroupChatResponseMessage(String from, String content) {

        this.from = from;

        this.content = content;

    }



    public String getFrom() {

        return from;

    }



    public void setFrom(String from) {

        this.from = from;

    }



    public String getContent() {

        return content;

    }



    public void setContent(String content) {

        this.content = content;

    }



    @Override

    public int getMessageType() {

        return GroupChatResponseMessage;

    }

}
java
public class GroupJoinRequestMessage extends Message {

    private String groupName;



    private String username;



    public GroupJoinRequestMessage(String username, String groupName) {

        this.groupName = groupName;

        this.username = username;

    }



    @Override

    public int getMessageType() {

        return GroupJoinRequestMessage;

    }



    public String getGroupName() {

        return groupName;

    }



    public void setGroupName(String groupName) {

        this.groupName = groupName;

    }



    public String getUsername() {

        return username;

    }



    public void setUsername(String username) {

        this.username = username;

    }

}
java
public class GroupJoinResponseMessage extends AbstractResponseMessage {



    public GroupJoinResponseMessage(boolean success, String reason) {

        super(success, reason);

    }

    @Override

    public int getMessageType() {

        return GroupJoinResponseMessage;

    }

}
java
public class GroupQuitRequestMessage extends Message {

    private String groupName;



    private String username;



    public GroupQuitRequestMessage(String username, String groupName) {

        this.groupName = groupName;

        this.username = username;

    }



    public String getGroupName() {

        return groupName;

    }



    public void setGroupName(String groupName) {

        this.groupName = groupName;

    }



    public String getUsername() {

        return username;

    }



    public void setUsername(String username) {

        this.username = username;

    }

    @Override

    public int getMessageType() {

        return GroupQuitRequestMessage;

    }

}
java
public class GroupQuitResponseMessage extends AbstractResponseMessage {

    public GroupQuitResponseMessage(boolean success, String reason) {

        super(success, reason);

    }

    @Override

    public int getMessageType() {

        return GroupQuitResponseMessage;

    }

}
java
public class PingMessage extends Message {

    @Override

    public int getMessageType() {

        return PingMessage;

    }

}

2. 客户端代码

java
public static void main(String[] args) {

        NioEventLoopGroup group = new NioEventLoopGroup();
        LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);
        WXMessageCodec MESSAGE_CODEC = new WXMessageCodec();
        CountDownLatch WAIT_FOR_LOGIN = new CountDownLatch(1);
        AtomicBoolean LOGIN = new AtomicBoolean(false);
        AtomicBoolean EXIT = new AtomicBoolean(false);
        Scanner scanner = new Scanner(System.in);
        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 LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0));
                    ch.pipeline().addLast(MESSAGE_CODEC);
                    // 用来判断是不是 读空闲时间过长,或 写空闲时间过长
                    // 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());
                            }
                        }
                    });
                    ch.pipeline().addLast("client handler", new ChannelInboundHandlerAdapter() {
                        // 接收响应消息
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            log.debug("msg: {}", msg);
                            if ((msg instanceof LoginResponseMessage)) {
                                LoginResponseMessage response = (LoginResponseMessage) msg;
                                if (response.isSuccess()) {
                                    // 如果登录成功
                                    LOGIN.set(true);
                                }
                                // 唤醒 system in 线程
                                WAIT_FOR_LOGIN.countDown();
                            }
                        }

                        // 在连接建立后触发 active 事件
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            // 负责接收用户在控制台的输入,负责向服务器发送各种消息
                            new Thread(() -> {
                                System.out.println("请输入用户名:");
                                String username = scanner.nextLine();
                                if (EXIT.get()) {
                                    return;
                                }
                                System.out.println("请输入密码:");
                                String password = scanner.nextLine();
                                if (EXIT.get()) {
                                    return;
                                }
                                // 构造消息对象
                                LoginRequestMessage message = new LoginRequestMessage(username, password);
                                System.out.println(message);
                                // 发送消息
                                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 = null;
                                    try {
                                        command = scanner.nextLine();
                                    } catch (Exception e) {
                                        break;
                                    }
                                    if (EXIT.get()) {
                                        return;
                                    }
                                    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();
        }
    }

3. 服务端代码

服务器端将handler独立出来

java
public static void main(String[] args) throws InterruptedException {
    NioEventLoopGroup group = new NioEventLoopGroup();
    ServerBootstrap serverBootstrap = new ServerBootstrap();
    LoggingHandler loggingHandler = new LoggingHandler(LogLevel.INFO);
    try {
        serverBootstrap.group(group);
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                
                ch.pipeline().addLast(loggingHandler);
                /**
                 * TODO 添加业务handler
                */
            }
        });
        ChannelFuture channelFuture = serverBootstrap.bind("192.168.101.1", 8080);
        channelFuture.channel().closeFuture().sync();
    } finally {
        group.shutdownGracefully();
    }
}

4. 聊天室业务-单聊

java
@ChannelHandler.Sharable

public class LoginRequestMessageHandler extends SimpleChannelInboundHandler<LoginRequestMessage> {

    @Override

    protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage msg) throws Exception {

        String username = msg.getUsername();

        String password = msg.getPassword();

        boolean login = UserServiceFactory.getUserService().login(username, password);

        LoginResponseMessage message;

        if(login) {

            SessionFactory.getSession().bind(ctx.channel(), username);

            message = new LoginResponseMessage(true, "登录成功");

        } else {

            message = new LoginResponseMessage(false, "用户名或密码不正确");

        }

        ctx.writeAndFlush(message);

    }

}
java
@ChannelHandler.Sharable

public class ChatRequestMessageHandler extends SimpleChannelInboundHandler<ChatRequestMessage> {

    @Override

    protected void channelRead0(ChannelHandlerContext ctx, ChatRequestMessage msg) throws Exception {

        String to = msg.getTo();

        Channel channel = SessionFactory.getSession().getChannel(to);

        // 在线

        if(channel != null) {

            channel.writeAndFlush(new ChatResponseMessage(msg.getFrom(), msg.getContent()));

        }

        // 不在线

        else {

            ctx.writeAndFlush(new ChatResponseMessage(false, "对方用户不存在或者不在线"));

        }

    }

}

5. 聊天室业务-群聊

java
@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();

        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 + "已经存在"));

        }

    }

}
java
@ChannelHandler.Sharable

public class GroupChatRequestMessageHandler extends SimpleChannelInboundHandler<GroupChatRequestMessage> {

    @Override

    protected void channelRead0(ChannelHandlerContext ctx, GroupChatRequestMessage msg) throws Exception {

        List<Channel> channels = GroupSessionFactory.getGroupSession()

                .getMembersChannel(msg.getGroupName());



        for (Channel channel : channels) {

            channel.writeAndFlush(new GroupChatResponseMessage(msg.getFrom(), msg.getContent()));

        }

    }

}
java
@ChannelHandler.Sharable

public class GroupJoinRequestMessageHandler extends SimpleChannelInboundHandler<GroupJoinRequestMessage> {

    @Override

    protected void channelRead0(ChannelHandlerContext ctx, GroupJoinRequestMessage msg) throws Exception {

        Group group = GroupSessionFactory.getGroupSession().joinMember(msg.getGroupName(), msg.getUsername());

        if (group != null) {

            ctx.writeAndFlush(new GroupJoinResponseMessage(true, msg.getGroupName() + "群加入成功"));

        } else {

            ctx.writeAndFlush(new GroupJoinResponseMessage(true, msg.getGroupName() + "群不存在"));

        }

    }

}
java
@ChannelHandler.Sharable

public class GroupQuitRequestMessageHandler extends SimpleChannelInboundHandler<GroupQuitRequestMessage> {

    @Override

    protected void channelRead0(ChannelHandlerContext ctx, GroupQuitRequestMessage msg) throws Exception {

        Group group = GroupSessionFactory.getGroupSession().removeMember(msg.getGroupName(), msg.getUsername());

        if (group != null) {

            ctx.writeAndFlush(new GroupJoinResponseMessage(true, "已退出群" + msg.getGroupName()));

        } else {

            ctx.writeAndFlush(new GroupJoinResponseMessage(true, msg.getGroupName() + "群不存在"));

        }

    }

}
java
@ChannelHandler.Sharable

public class GroupMembersRequestMessageHandler extends SimpleChannelInboundHandler<GroupMembersRequestMessage> {

    @Override

    protected void channelRead0(ChannelHandlerContext ctx, GroupMembersRequestMessage msg) throws Exception {

        Set<String> members = GroupSessionFactory.getGroupSession()

                .getMembers(msg.getGroupName());

        ctx.writeAndFlush(new GroupMembersResponseMessage(members));

    }

}

6. 聊天室业务-退出

java
@ChannelHandler.Sharable
public class QuitHandler extends ChannelInboundHandlerAdapter {
    // 当连接断开时触发 inactive 事件
    @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());
    }
}

7. 聊天室业务-空闲检测

在实际使用中需要处理连接假死的情况,假死的原因有:

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

导致的问题:

  1. 假死的连接占用的资源不能自动释放。
  2. 向假死的连接发送数据,得到的反馈是发送超时。

服务器端解决方案:如果能收到客户端数据,说明没有假死。因此策略就可以定为,每隔一段时间就检查这段时间内是否接收到客户端数据,没有就可以判定为连接假死。但如果用户在登录的过程中,或者用户在线并没有聊天的情形下并不能统一认为客户端就是假死。因此在客户端设置一个定时周期任务发送心跳检测消息给服务器。

java
// 用来判断是不是 读空闲时间过长,或 写空闲时间过长

// 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();

        }

    }

});
java
// 用来判断是不是 读空闲时间过长,或 写空闲时间过长

// 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());

        }

    }

});
客户端定时心跳:客户端可以定时向服务器端发送数据,只要这个时间间隔小于服务器定义的空闲检测的时间间隔,那么就能防止前面提到的误判,客户端可以定义如下心跳处理器。

评论