Netty聊天系统(3)登录与私聊功能实现

5 定义一个Handler实现身份校验

在客户端与服务器端之间通信时,一定要进行身份认证,即客户端必须要登录。此时可以在用户登录完成后,在channel上添加一个标志位,表示用户已经登录。在接受客户端发送的消息时,必须确保此时客户端已经登录。

在客户端登录后,在channel上添加一个标志位。

在服务器端的LoginRequestHandler中,用户登录成功后,就将channel添加一个“login”的标志位,并设置为true。

ctx.channel().attr(AttributeKey.newInstance("login")).set(true);

添加一个身份认证Handler,放在LoginRequestHandler的后面,表示后面的所有请求都必须要经过身份认证,没有经过身份认证的话就关闭连接。

public class AuthHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //如果没有登录,则关闭连接
        Boolean login =(Boolean) ctx.channel().attr(AttributeKey.newInstance("login")).get();
        if(login == null || !login){
            ctx.channel().close();
        }
        // 否则的话就直接传给下一个handler处理
        else {
            super.channelRead(ctx, msg);
        }
    }
}

以上解决方案存在的问题:这样客户端每次发送数据给服务器端时,都需要去校验,性能会受到影响。

我们可以在第一次校验之后,只要连接未断开都不再做校验!

可以通过pipeline的热插拔机制实现。

只需要在上面的代码中加入移除handler的代码即可,即校验通过后,就将AuthHandler删除。

public class AuthHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //如果没有登录,则关闭连接
        Boolean login =(Boolean) ctx.channel().attr(AttributeKey.newInstance("login")).get();
        if(login == null || !login){
            ctx.channel().close();
        }
        // 否则的话就直接传给下一个handler处理,并删除AuthHandler
        else {
            ctx.pipeline().remove(this);  //移除该handler即可。
            super.channelRead(ctx, msg);
        }
    }
}

6 实现一对一私聊功能

我们之前都是客户端跟服务器端通信,要实现客户端与客户端之间的通信,肯定是要通过服务器端进行转发的,那么我们需要告诉服务器端,我要发送消息给谁并且附带上消息内容。

这时候要解决的问题就是,服务器端收到数据包后解析,直到了要发给那个用户,但是怎么通过用户的ID找到与用户连接的channel?

这时可以将用户连接并且登录后的channel保存起来,利用Map或者其他方式将其一一对应起来,当需要发送消息给张三的时候,就去Map中找张三与服务器端建立连接的channel,通过channel将消息转发给张三。

写一个保存会话Channel的工具类

  • 使用concurrentHashMap存储userID到channel的映射
import io.netty.channel.Channel;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ChannelUtil {
    //定义一个Map结构存储userID映射到channel
    private static final Map<Long, Channel> userIdChannel = new ConcurrentHashMap<>();

    //用户登录后保存用户channel
    public static void saveChannel(Long userId, Channel channel){
        userIdChannel.put(userId, channel);
    }
    
    //当用户退出后删除用户channel
    public static void removeChannel(Long userId){
        if(!userIdChannel.containsKey(userId)){
            throw new RuntimeException("未找到当前用户的channel");
        }
        userIdChannel.remove(userId);
    }
    
    //通过userId查找channel
    public static Channel getChannel(Long userId){
        return userIdChannel.get(userId);
    }
}

写一个AttributeKeys工具类,存储所有绑定在channel上的属性。

每个channel都去新建一个AttributeKey时会报错。

public class AttributeKeys {
    public static AttributeKey<Boolean> LOGIN = AttributeKey.newInstance("login");
    public static AttributeKey<Long> USER_ID = AttributeKey.newInstance("userId");
}

改造服务器端的LoginRequestHandler,当登录成功后就保存channel,当用户断线之后记得删除channel

public class LoginRequestHandler extends SimpleChannelInboundHandler<LoginRequestPacket> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginRequestPacket packet) throws Exception {
        String username = packet.getUsername();
        System.out.println(username + "用户登录成功");
        // 登录后返回客户端响应,告诉客户端是否登录成功
        LoginResponsePacket loginResponsePacket = new LoginResponsePacket();
        loginResponsePacket.setVersion(packet.getVersion());
        loginResponsePacket.setSuccess(true);

        // 为channel添加标记位,表示已经登录成功
        ctx.channel().attr(AttributeKeys.LOGIN).set(true);
        // 为channel添加userId属性,方便在删除channel时能快速从Map中查找并删除
        ctx.channel().attr(AttributeKeys.USER_ID).set(packet.getUserId());
        // 保存channel
        ChannelUtil.saveChannel(packet.getUserId(), ctx.channel());

        ctx.channel().writeAndFlush(loginResponsePacket);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        ChannelUtil.removeChannel(ctx.channel().attr(AttributeKeys.USER_ID).get());
    }
}

改造消息传输类,此时需要包含要发送给哪个用户的相关信息。

@Data
public class MsgRequestPacket extends Packet {
    private Long toUserId;
    private String message;

    @Override
    public Byte getCommand() {
        return Command.MSG_REQUEST;
    }
}
@Data
public class MsgResponsePacket extends Packet {
    private Long fromUserId;
    private String message;
    @Override
    public Byte getCommand() {
        return Command.MSG_RESPONSE;
    }
}

服务器端处理消息的MsgRequestHandler逻辑

首先获取消息发送方信息和目的客户端信息,从ChannelUtil中获取目的客户端的channel,获取到channel后,将消息发送给目的客户端。

public class MsgRequestHandler extends SimpleChannelInboundHandler<MsgRequestPacket> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MsgRequestPacket msg) throws Exception {
        String message = msg.getMessage();
        long fromUserId = ctx.channel().attr(AttributeKeys.USER_ID).get();
        System.out.println("收到userId:"+fromUserId +"发送给"+msg.getToUserId()+"的消息:"+ message);

        //构造发送给接收方的消息
        MsgResponsePacket msgResponsePacket = new MsgResponsePacket();
        msgResponsePacket.setMessage(message);
        msgResponsePacket.setFromUserId(fromUserId);

        //获取接收方与服务器端连接的channel,并向接收方发送数据
        Channel channel = ChannelUtil.getChannel(msg.getToUserId());
        if(channel == null){
            System.out.println(msg.getToUserId()+"不在线");
        }else {
            channel.writeAndFlush(msgResponsePacket);
        }
    }
}

这里当对方不在线时,没有做任何处理。可以实现将消息写入数据库,当对方登录时,检查数据库中是否有发送给自己的消息,有的话就读取。

客户端收到消息后的处理器

public class MsgResponseHandler extends SimpleChannelInboundHandler<MsgResponsePacket> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MsgResponsePacket msg) throws Exception {
        String message = msg.getMessage();
        System.out.println("收到"+msg.getFromUserId()+"发来的消息:" + message);
    }
}

重写一下用户登录成功后开启的新线程进行测试,并且在用户登录时额外加上userId。

public class LoginResponseHandler extends SimpleChannelInboundHandler<LoginResponsePacket> {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("开始登陆");
        System.out.println("请输入用户名");
        Scanner scanner = new Scanner(System.in);
        String username = scanner.nextLine();
        System.out.println("请输入密码");
        String password = scanner.nextLine();
        System.out.println("请输入用户ID");
        Long userId = scanner.nextLong();
        scanner.nextLine();

        LoginRequestPacket loginRequestPacket = new LoginRequestPacket();
        loginRequestPacket.setUserId(userId);
        loginRequestPacket.setUsername(username);
        loginRequestPacket.setPassword(password);

        ctx.channel().writeAndFlush(loginRequestPacket);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginResponsePacket loginResponsePacket) throws Exception {
        if(loginResponsePacket.isSuccess()){
            System.out.println("登录成功");
            //登录成功后,启动一个线程接受控制台输入,并发送数据
            startSendMsgThread(ctx.channel());
        }else {
            System.out.println(loginResponsePacket.getReason());
        }
    }

    private static void startSendMsgThread(Channel channel){
        new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            while (true){
                System.out.println("请输入要接收方userId");
                long toUserId = scanner.nextLong();
                scanner.nextLine();
                System.out.println("请输入要发送的内容");
                String msg = scanner.nextLine();
                MsgRequestPacket msgRequestPacket = new MsgRequestPacket();
                msgRequestPacket.setMessage(msg);
                msgRequestPacket.setToUserId(toUserId);

                channel.writeAndFlush(msgRequestPacket);
            }
        }).start();
    }
}

测试效果:开启服务器端,开启多个客户端,指定不同的userId,然后进行发生消息。发送消息时,先指定要发送给哪个用户,然后输入要发送的内容。

客户端100的打印结果:

image-20200730154911284

客户端200的打印结果:

image-20200730154955081

服务器端:

image-20200730155109697

至此,私聊功能实现。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页