Netty应用(四) ----聊天室

1. 功能设计

  1. 实现客户端的登录功能。登录成功后,记录会话信息,主要是登录用户和channel的绑定关系,我们后续通信时,需要向对应的channel中发送消息。
  2. 实现两个客户端之间的通信。通过对方名称,查询到对应的channel,向其channel中发送消息。
  3. 实现聊天群的创建、群成员的查看、退群、加群功能。通过群session来记录群成员的channel信息。
  4. 客户端的退出,包括直接关闭客户端以及异常关闭。采用空闲检测+心跳机制实现。

2. 相关代码

代码网盘下载链接
链接:https://pan.baidu.com/s/11iySpSqv6D_lZ4gyYSvBEw?pwd=d79n
提取码:d79n

2.1 message消息

2.1.1 Message 消息父类

@Data
public abstract class Message implements Serializable {

    public static Class<?> getMessageClass(int messageType) {
        return messageClasses.get(messageType);
    }

    private int sequenceId;

    private int messageType;

    public abstract int getMessageType();

    public static final int LoginRequestMessage = 0;
    public static final int LoginResponseMessage = 1;
    public static final int ChatRequestMessage = 2;
    public static final int ChatResponseMessage = 3;
    public static final int GroupCreateRequestMessage = 4;
    public static final int GroupCreateResponseMessage = 5;
    public static final int GroupJoinRequestMessage = 6;
    public static final int GroupJoinResponseMessage = 7;
    public static final int GroupQuitRequestMessage = 8;
    public static final int GroupQuitResponseMessage = 9;
    public static final int GroupChatRequestMessage = 10;
    public static final int GroupChatResponseMessage = 11;
    public static final int GroupMembersRequestMessage = 12;
    public static final int GroupMembersResponseMessage = 13;
    public static final int PingMessage = 14;
    public static final int PongMessage = 15;
    private static final Map<Integer, Class<?>> messageClasses = new HashMap<>();

    static {
        messageClasses.put(LoginRequestMessage, LoginRequestMessage.class);
        messageClasses.put(LoginResponseMessage, LoginResponseMessage.class);
        messageClasses.put(ChatRequestMessage, ChatRequestMessage.class);
        messageClasses.put(ChatResponseMessage, ChatResponseMessage.class);
        messageClasses.put(GroupCreateRequestMessage, GroupCreateRequestMessage.class);
        messageClasses.put(GroupCreateResponseMessage, GroupCreateResponseMessage.class);
        messageClasses.put(GroupJoinRequestMessage, GroupJoinRequestMessage.class);
        messageClasses.put(GroupJoinResponseMessage, GroupJoinResponseMessage.class);
        messageClasses.put(GroupQuitRequestMessage, GroupQuitRequestMessage.class);
        messageClasses.put(GroupQuitResponseMessage, GroupQuitResponseMessage.class);
        messageClasses.put(GroupChatRequestMessage, GroupChatRequestMessage.class);
        messageClasses.put(GroupChatResponseMessage, GroupChatResponseMessage.class);
        messageClasses.put(GroupMembersRequestMessage, GroupMembersRequestMessage.class);
        messageClasses.put(GroupMembersResponseMessage, GroupMembersResponseMessage.class);
        messageClasses.put(PingMessage, GroupMembersResponseMessage.class);
        messageClasses.put(PongMessage, GroupMembersResponseMessage.class);
    }
}

2.1.2 AbstractResponseMessage

@Data
@ToString(callSuper = true)
public abstract class AbstractResponseMessage extends Message {
    private boolean success;
    private String reason;

    public AbstractResponseMessage() {
    }

    public AbstractResponseMessage(boolean success, String reason) {
        this.success = success;
        this.reason = reason;
    }
}

2.1.3 登录请求和响应

@Data
@ToString(callSuper = true)
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;
    }

    @Override
    public int getMessageType() {
        return LoginRequestMessage;
    }
}

@Data
@ToString(callSuper = true)
public class LoginResponseMessage extends AbstractResponseMessage {

    public LoginResponseMessage(boolean success, String reason) {
        super(success, reason);
    }

    @Override
    public int getMessageType() {
        return LoginResponseMessage;
    }
}

2.1.4 聊天请求和响应

@Data
@ToString(callSuper = true)
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;
    }

    @Override
    public int getMessageType() {
        return ChatRequestMessage;
    }
}
@Data
@ToString(callSuper = true)
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;
    }

    @Override
    public int getMessageType() {
        return ChatResponseMessage;
    }
}

2.1.5 群组聊天请求和响应

@Data
@ToString(callSuper = true)
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;
    }

    @Override
    public int getMessageType() {
        return GroupChatRequestMessage;
    }
}

@Data
@ToString(callSuper = true)
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;
    }
    @Override
    public int getMessageType() {
        return GroupChatResponseMessage;
    }
}

2.1.6 群组创建请求和响应

@Data
@ToString(callSuper = true)
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;
    }

    @Override
    public int getMessageType() {
        return GroupCreateRequestMessage;
    }
}
@Data
@ToString(callSuper = true)
public class GroupCreateResponseMessage extends AbstractResponseMessage {

    public GroupCreateResponseMessage(boolean success, String reason) {
        super(success, reason);
    }

    @Override
    public int getMessageType() {
        return GroupCreateResponseMessage;
    }
}

2.1.7 群组成员请求和响应

@Data
@ToString(callSuper = true)
public class GroupMembersRequestMessage extends Message {
    private String groupName;

    public GroupMembersRequestMessage(String groupName) {
        this.groupName = groupName;
    }

    @Override
    public int getMessageType() {
        return GroupMembersRequestMessage;
    }
}
@Data
@ToString(callSuper = true)
public class GroupMembersResponseMessage extends Message {

    private Set<String> members;

    public GroupMembersResponseMessage(Set<String> members) {
        this.members = members;
    }

    @Override
    public int getMessageType() {
        return GroupMembersResponseMessage;
    }
}

2.1.8 加群请求和响应

@Data
@ToString(callSuper = true)
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;
    }
}
@Data
@ToString(callSuper = true)
public class GroupJoinResponseMessage extends AbstractResponseMessage {

    public GroupJoinResponseMessage(boolean success, String reason) {
        super(success, reason);
    }

    @Override
    public int getMessageType() {
        return GroupJoinResponseMessage;
    }
}

2.1.9 退群请求和响应

@Data
@ToString(callSuper = true)
public class GroupQuitRequestMessage extends Message {
    private String groupName;

    private String username;

    public GroupQuitRequestMessage(String username, String groupName) {
        this.groupName = groupName;
        this.username = username;
    }

    @Override
    public int getMessageType() {
        return GroupQuitRequestMessage;
    }
}
@Data
@ToString(callSuper = true)
public class GroupQuitResponseMessage extends AbstractResponseMessage {
    public GroupQuitResponseMessage(boolean success, String reason) {
        super(success, reason);
    }

    @Override
    public int getMessageType() {
        return GroupQuitResponseMessage;
    }
}

2.1.10 ping/pong心跳消息

public class PingMessage extends Message{

    @Override
    public int getMessageType() {
        return PingMessage;
    }
}
public class PongMessage extends Message{

    @Override
    public int getMessageType() {
        return PongMessage;
    }
}

2.2 codec编解码器

2.2.1 帧编解码器,二次封装

public class ProtocolFrameDecoder extends LengthFieldBasedFrameDecoder {

    public ProtocolFrameDecoder() {
        this(1024, 12, 4, 0, 0);
    }

    public ProtocolFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment,
        int initialBytesToStrip) {
        super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip);
    }
}

2.2.2 自定义的消息编解码器

@Slf4j
@ChannelHandler.Sharable
public class MessageSharableCodec extends MessageToMessageCodec<ByteBuf,Message> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> outList) throws Exception {
        ByteBuf out = ctx.alloc().buffer();
        // 1. 4个字节的魔数
        out.writeBytes(new byte[] {1, 2, 3, 4});
        // 2. 1字节的版本
        out.writeByte(1);
        // 3. 1字节的序列化方式,0-jdk 1-json
        out.writeByte(0);
        // 4. 1字节表示消息类型,比如登录、退出..
        out.writeByte(msg.getMessageType());
        // 5. 4字节表示消息请求序号
        out.writeInt(msg.getSequenceId());
        // 由于4+1+1+1+4+4=15,我们再加一字节用于补齐,无意义
        out.writeByte(0XFF);
        // 6. 4字节表示消息的长度
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(os);
        oos.writeObject(msg);
        byte[] bytes = os.toByteArray();
        out.writeInt(bytes.length);

        // 7.消息正文
        out.writeBytes(bytes);
        outList.add(out);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        int magicNum = in.readInt();
        byte version = in.readByte();
        byte serializerType = in.readByte();
        byte type = in.readByte();
        int sequenceId = in.readInt();
        in.readByte();

        int length = in.readInt();
        byte[] bytes = new byte[length];
        in.readBytes(bytes, 0, length);
        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Message message = (Message) objectInputStream.readObject();
        out.add(message);
    }
}

2.3 handler处理器

2.3.1 登录处理器

@ChannelHandler.Sharable
public class LoginRequestHandler extends SimpleChannelInboundHandler<LoginRequestMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage msg) throws Exception {
        boolean login = UserServiceFactory.getUserService().login(msg.getUsername(), msg.getPassword());
        LoginResponseMessage loginResponseMessage;
        if (login) {
            loginResponseMessage = new LoginResponseMessage(true, "登录成功");
            // 登录成功,记录session,即账号与channel的绑定关系
            SessionFactory.getSession().bind(ctx.channel(), msg.getUsername());
        } else {
            loginResponseMessage = new LoginResponseMessage(false, "账号或密码不正确");
        }
        ctx.writeAndFlush(loginResponseMessage);
    }
}

2.3.2 聊天处理器

@ChannelHandler.Sharable
public class ChatRequestHandler extends SimpleChannelInboundHandler<ChatRequestMessage> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ChatRequestMessage msg) throws Exception {
        // 找到对方所在的channel
        String to = msg.getTo();
        Channel channel = SessionFactory.getSession().getChannel(to);
        if (channel != null){
            // 在线 向对方channel写入消息
            channel.writeAndFlush(new ChatResponseMessage(msg.getFrom(),msg.getContent()));
        } else {
            // 对方channel不存在,向自己的chanel写入失败的原因
            ctx.writeAndFlush(new ChatResponseMessage(false,"对方不存在或不在线"));
        }
    }
}

2.3.3 群聊处理器

@ChannelHandler.Sharable
public class GroupChatRequestHandler extends SimpleChannelInboundHandler<GroupChatRequestMessage> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, GroupChatRequestMessage msg) throws Exception {
        // 获取群成员,向每个成员的channel发送消息
        List<Channel> membersChannel = GroupSessionFactory.getGroupSession().getMembersChannel(msg.getGroupName());
        if (membersChannel == null) {
            // 不存在
            ctx.channel().writeAndFlush(new GroupChatResponseMessage(false, "群聊不存在"));
            return;
        }
        for (Channel channel : membersChannel) {
            if (channel.equals(ctx.channel())) {
                continue;
            }
            channel.writeAndFlush(new GroupChatResponseMessage(msg.getFrom(), msg.getContent()));
        }
    }
}

2.3.4 创建群处理器

@ChannelHandler.Sharable
public class GroupCreateRequestHandler extends SimpleChannelInboundHandler<GroupCreateRequestMessage> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, GroupCreateRequestMessage msg) throws Exception {

        // 根据名称创建群聊,如果已存在则返回已存在的群聊信息
        // 需要将创建人也拉入群聊,这里没处理,需要在client端输入members时加上自己
        Group group = GroupSessionFactory.getGroupSession().createGroup(msg.getGroupName(), msg.getMembers());
        if (group == null) {
            // 创建成功, 向每一个人回复进群消息,向创建人回复创建成功消息
            List<Channel> membersChannel = GroupSessionFactory.getGroupSession().getMembersChannel(msg.getGroupName());
            for (Channel channel : membersChannel) {
                if (!channel.equals(ctx.channel())) {
                    channel.writeAndFlush(new GroupCreateResponseMessage(true, "你已进入" + msg.getGroupName() + "群聊"));
                }
            }
            ctx.writeAndFlush(new GroupCreateResponseMessage(true, "群聊创建成功"));
        } else {
            ctx.writeAndFlush(new GroupCreateResponseMessage(false, "群聊已存在"));
        }
    }
}

2.3.5 查看群成员处理器

@ChannelHandler.Sharable
public class GroupMemberRequestHandler extends SimpleChannelInboundHandler<GroupMembersRequestMessage> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, GroupMembersRequestMessage msg) throws Exception {
        Set<String> members = GroupSessionFactory.getGroupSession().getMembers(msg.getGroupName());
        ctx.channel().writeAndFlush(new GroupMembersResponseMessage(members));
    }
}

2.3.6 加群处理器

@ChannelHandler.Sharable
public class GroupJoinRequestHandler 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(false, "群聊不存在"));
            return;
        }

        List<Channel> membersChannel = GroupSessionFactory.getGroupSession().getMembersChannel(msg.getGroupName());
        for (Channel channel : membersChannel) {
            if (channel.equals(ctx.channel())) {
                continue;
            }
            channel.writeAndFlush(new GroupJoinResponseMessage(true, msg.getUsername() + "进入群聊"));
        }
        ctx.writeAndFlush(new GroupJoinResponseMessage(true, "加入群聊成功"));
    }
}

2.3.7 退群处理器

@ChannelHandler.Sharable
public class GroupQuitRequestHandler 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 GroupQuitResponseMessage(false, "群聊不存在"));
            return;
        }

        List<Channel> membersChannel = GroupSessionFactory.getGroupSession().getMembersChannel(msg.getGroupName());
        for (Channel channel : membersChannel) {
            if (channel.equals(ctx.channel())) {
                continue;
            }
            channel.writeAndFlush(new GroupQuitResponseMessage(true, msg.getUsername() + "退出群聊"));
        }
        ctx.writeAndFlush(new GroupQuitResponseMessage(true, "退出群聊成功"));
    }
}

2.4 用户管理

2.4.1 UserService

public interface UserService {

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

2.4.2 UserServiceFactory

public abstract class UserServiceFactory {

    private static UserService userService = new UserServiceMemoryImpl();

    public static UserService getUserService() {
        return userService;
    }
}

2.4.3 UserServiceMemoryImp

public class UserServiceMemoryImpl implements UserService {
    private Map<String, String> allUserMap = new ConcurrentHashMap<>();
    {
        allUserMap.put("zhangsan", "123");
        allUserMap.put("lisi", "123");
        allUserMap.put("wangwu", "123");
        allUserMap.put("zhaoliu", "123");
        allUserMap.put("qianqi", "123");
    }

    @Override
    public boolean login(String username, String password) {
        String pass = allUserMap.get(username);
        if (pass == null) {
            return false;
        }
        return pass.equals(password);
    }
}

2.5 会话管理

2.5.1 用户会话管理

/**
 * 会话管理接口
 */
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 abstract class SessionFactory {

    private static Session session = new SessionMemoryImpl();

    public static Session getSession() {
        return session;
    }
}
public class SessionMemoryImpl implements Session {

    private final Map<String, Channel> usernameChannelMap = new ConcurrentHashMap<>();
    private final Map<Channel, String> channelUsernameMap = new ConcurrentHashMap<>();
    private final Map<Channel,Map<String,Object>> channelAttributesMap = new ConcurrentHashMap<>();

    @Override
    public void bind(Channel channel, String username) {
        usernameChannelMap.put(username, channel);
        channelUsernameMap.put(channel, username);
        channelAttributesMap.put(channel, new ConcurrentHashMap<>());
    }

    @Override
    public void unbind(Channel channel) {
        String username = channelUsernameMap.remove(channel);
        usernameChannelMap.remove(username);
        channelAttributesMap.remove(channel);
    }

    @Override
    public Object getAttribute(Channel channel, String name) {
        return channelAttributesMap.get(channel).get(name);
    }

    @Override
    public void setAttribute(Channel channel, String name, Object value) {
        channelAttributesMap.get(channel).put(name, value);
    }

    @Override
    public Channel getChannel(String username) {
        return usernameChannelMap.get(username);
    }

    @Override
    public String toString() {
        return usernameChannelMap.toString();
    }
}

2.5.2 群会话管理

@Data
/**
 * 聊天组,即聊天室
 */
public class Group {
    // 聊天室名称
    private String name;
    // 聊天室成员
    private Set<String> members;

    public static final Group EMPTY_GROUP = new Group("empty", Collections.emptySet());

    public Group(String name, Set<String> members) {
        this.name = name;
        this.members = members;
    }
}
/**
 * 聊天组会话管理接口
 */
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);

    /**
     * 获取组成员的名字
     * @param name 组名
     */
    List<String> getUsers(String name);
}

public abstract class GroupSessionFactory {

    private static GroupSession session = new GroupSessionMemoryImpl();

    public static GroupSession getGroupSession() {
        return session;
    }
}
public class GroupSessionMemoryImpl implements GroupSession {
    private final Map<String, Group> groupMap = new ConcurrentHashMap<>();

    @Override
    public Group createGroup(String name, Set<String> members) {
        Group group = new Group(name, members);
        return groupMap.putIfAbsent(name, group);
    }

    @Override
    public Group joinMember(String name, String member) {
        return groupMap.computeIfPresent(name, (key, value) -> {
            value.getMembers().add(member);
            return value;
        });
    }

    @Override
    public Group removeMember(String name, String member) {
        return groupMap.computeIfPresent(name, (key, value) -> {
            value.getMembers().remove(member);
            return value;
        });
    }

    @Override
    public Group removeGroup(String name) {
        return groupMap.remove(name);
    }

    @Override
    public Set<String> getMembers(String name) {
        return groupMap.getOrDefault(name, Group.EMPTY_GROUP).getMembers();
    }

    @Override
    public List<Channel> getMembersChannel(String name) {
        //判断群聊存不存在
        if(groupMap.get(name) == null){
            return null;
        }
        return getMembers(name).stream()
                .map(member -> SessionFactory.getSession().getChannel(member))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    @Override
    public List<String> getUsers(String name) {
        if(groupMap.get(name) == null){
            return null;
        }
        return new ArrayList<>(groupMap.get(name).getMembers());
    }
}

2.6 客户端和服务端

server

public class ChatServer {
    public static void main(String[] args) {
        LoggingHandler loggingHandler = new LoggingHandler(LogLevel.INFO);
        MessageSharableCodec messageSharableCodec = new MessageSharableCodec();
        LoginRequestHandler loginRequestHandler = new LoginRequestHandler();
        ChatRequestHandler chatRequestHandler = new ChatRequestHandler();
        GroupCreateRequestHandler groupCreateRequestHandler = new GroupCreateRequestHandler();
        GroupChatRequestHandler groupChatRequestHandler = new GroupChatRequestHandler();
        GroupJoinRequestHandler groupJoinRequestHandler = new GroupJoinRequestHandler();
        GroupQuitRequestHandler groupQuitRequestHandler = new GroupQuitRequestHandler();
        GroupMemberRequestHandler groupMemberRequestHandler = new GroupMemberRequestHandler();

        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup(4))
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();
                    pipeline.addLast(new ProtocolFrameDecoder());
                    pipeline.addLast(loggingHandler);
                    pipeline.addLast(messageSharableCodec);
                    pipeline.addLast(loginRequestHandler);
                    pipeline.addLast(chatRequestHandler);
                    pipeline.addLast(groupCreateRequestHandler);
                    pipeline.addLast(groupChatRequestHandler);
                    pipeline.addLast(groupJoinRequestHandler);
                    pipeline.addLast(groupQuitRequestHandler);
                    pipeline.addLast(groupMemberRequestHandler);
                }
            })
            .bind(8080);
    }
}

client

@Slf4j
public class ChatClient {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        AtomicBoolean login = new AtomicBoolean();
        ChannelFuture channelFuture = new Bootstrap().group(new NioEventLoopGroup())
            .channel(NioSocketChannel.class)
            .handler(new ChannelInitializer<NioSocketChannel>() {
                protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                    ChannelPipeline pipeline = nioSocketChannel.pipeline();
//                    pipeline.addLast(new LoggingHandler(LogLevel.INFO));
                    pipeline.addLast(new ProtocolFrameDecoder());
                    pipeline.addLast(new MessageSharableCodec());
                    pipeline.addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            log.info("msg:{}", msg);
                            if (msg instanceof LoginResponseMessage) {
                                LoginResponseMessage loginResponseMessage = (LoginResponseMessage) msg;
                                if (loginResponseMessage.isSuccess()) {
                                    login.set(true);
                                }
                                countDownLatch.countDown();
                            }
                        }
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            new Thread(() -> {
                                Scanner scanner = new Scanner(System.in);
                                System.out.println("请输入账号:");
                                String name = scanner.nextLine();
                                System.out.println("请输入密码:");
                                String password = scanner.nextLine();
                                // 将账号密码发给服务端,校验账号是否正确
                                LoginRequestMessage message = new LoginRequestMessage(name, password);
                                ctx.writeAndFlush(message);
                                System.out.println("等待后续操作....");
                                try {
                                    // 阻塞当前线程,直到收到登录响应,再放通
                                    countDownLatch.await();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                                if (!login.get()) {
                                    // 登录失败 直接关闭channel
                                    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(name, s[1], s[2]));
                                            break;
                                        case "gsend":
                                            ctx.writeAndFlush(new GroupChatRequestMessage(name, s[1], s[2]));
                                            break;
                                        case "gcreate":
                                            Set<String> numbers = new HashSet<>(Arrays.asList(s[2].split(",")));
                                            ctx.writeAndFlush(new GroupCreateRequestMessage(s[1], numbers));
                                            break;
                                        case "gmembers":
                                            ctx.writeAndFlush(new GroupMembersRequestMessage(s[1]));
                                            break;
                                        case "gjoin":
                                            ctx.writeAndFlush(new GroupJoinRequestMessage(name, s[1]));
                                            break;
                                        case "gquit":
                                            ctx.writeAndFlush(new GroupQuitRequestMessage(name, s[1]));
                                            break;
                                        case "quit":
                                            ctx.channel().close();
                                            break;
                                        default:
                                            System.out.println("指令不正确");
                                    }
                                }
                            }).start();
                        }
                    });
                }
            })
            .connect(new InetSocketAddress("localhost", 8080));
        Channel channel = channelFuture.sync().channel();
        channel.closeFuture().sync();
    }
}

3.空闲检测+心跳机制

IdleStateHandler:空闲状态处理器
在这里插入图片描述
我们可以通过构造参数,设置读、写的超时时间,如果时间超时,则会触发对应的事件。

  • readerIdleTimeSeconds -> IdleStateEvent -> IdleState#READER_IDLE
  • writerIdleTimeSeconds -> IdleStateEvent -> IdleState#WRITER_IDLE
  • allIdleTimeSeconds -> IdleStateEvent -> IdleState#ALL_IDLE

server
服务端需要添加对读超时的检测。

                    pipeline.addLast(messageSharableCodec);

                    // 设置空闲检测,读超时设置为5秒,即server端如果5秒没收到数据,则会触发READER_IDLE
                    pipeline.addLast(new IdleStateHandler(5, 0, 0));
                    // 空闲检测可能涉及到入站和出站,可以实现ChannelDuplexHandler子类实现
                    pipeline.addLast(new ChannelDuplexHandler() {
                        @Override
                        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
                            if (evt instanceof IdleStateEvent) {
                                IdleStateEvent event = (IdleStateEvent) evt;
                                if (IdleState.READER_IDLE.equals(event.state())) {
                                    // 触发读空闲超时事件
                                    ctx.channel().close();
                                }
                            }
                        }
                    });
                    pipeline.addLast(loginRequestHandler);

client
客户端向服务端发送心跳,心跳时长要比服务端空闲检测时间短。

                    pipeline.addLast(new MessageSharableCodec());

                    // 写超时设置为3秒,即客户端如果3秒没写出数据,则触发WRITER_IDLE,此刻我们需要补一条心跳数据
                    pipeline.addLast(new IdleStateHandler(0, 3, 0));
                    pipeline.addLast(new ChannelDuplexHandler() {
                        @Override
                        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
                            if (evt instanceof IdleStateEvent) {
                                IdleStateEvent event = (IdleStateEvent) evt;
                                if (IdleState.WRITER_IDLE.equals(event.state())) {
                                    // 触发写空闲超时事件, 向服务端发送ping,服务端可以回复pong(这块没加)
                                    ctx.channel().writeAndFlush(new PingMessage());
                                }
                            }
                        }
                    });

                    pipeline.addLast(new ChannelInboundHandlerAdapter() {
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值