Netty实战之认证机制+心跳机制+群聊(服务端)

前段时间在项目中有用过Netty做过网络通信,由于项目时间紧,一直没有总结,借着最近有一点空余时间,写个群聊的例子梳理一下如何快速上手一个Netty项目。

客户端代码见Netty实战之登陆机制+心跳机制+群聊(客户端)

maven依赖

xml 复制代码
<fastjosn.version>2.0.23</fastjosn.version>
<hutool.version>5.8.11</hutool.version>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<!--fastjson2-->
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>${fastjosn.version}</version>
</dependency>
<!-- hutool 的依赖配置-->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-core</artifactId>
    <version>${hutool.version}</version>
</dependency>

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.53.Final</version>
</dependency>

代码实现

服务端

java 复制代码
/**
 * @author shark
 * @version 1.0.0
 * @description 服务启动类
 * @date 2023/7/31 10:32
 */
@Slf4j
@Component
public class NettyServerRunner implements ApplicationRunner {

    @Value("${netty.port}")
    private int port;

    //处理客户端连接
    private final EventLoopGroup bossGroup = new NioEventLoopGroup();
    
    //轮询注册到selector上io就绪的SocketChnanel(传说中的主从reactor线程模式)
    private final EventLoopGroup workerGroup = new NioEventLoopGroup(2);

    @Override
    public void run(ApplicationArguments args) {
        try {
            //以下是模版代码,所有netty程序的通用代码
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(port))
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            //分割符解码器
                            pipeline.addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("$".getBytes())));
                            pipeline.addLast("decoder",new StringDecoder());
                            pipeline.addLast("encoder",new StringEncoder());
                            pipeline.addLast(new AuthClientHandler());
                            pipeline.addLast(new IdleStateHandler(0, 10, 10, TimeUnit.SECONDS));
                            pipeline.addLast(new HeartbeatHandler());
                            pipeline.addLast(new GroupChatServerHandler());
                        }
                    });
            ChannelFuture channelFuture = serverBootstrap.bind().sync();
            if (channelFuture.isSuccess()) {
                log.info("server run success!port:{}", port);
            }
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("server error:{}", e.getMessage());
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
            log.info("server close!");
        }
    }
}

这里ChannelPipeline的调用链上的顺序也是有讲究的,netty有入站消息和出站消息,入站的调用顺序是注册的顺序,而出站是反过来的,所以是编解码 -> 认证 -> 心跳 -> 聊天,这里也有个坑,就是handler调用链把消息传递给下一个handler要加fireChannelxxx()的方法,英译释放xxx的通道的意思,很好理解

ChannelHandler类

less 复制代码
/**
 * @author shark
 * @version 1.0.0
 * @description 认证类
 * @date 2023/8/3 11:01
 */
@Slf4j
@Component
@ChannelHandler.Sharable
public class AuthClientHandler extends SimpleChannelInboundHandler<String> {

    @Override
    public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        JSONObject jsonObject = JSON.parseObject(msg);
        //是认证消息就走下去
        if (MessageType.LOGIN.getCode() == jsonObject.getIntValue("code")) {
            LoginMessage message = JSON.parseObject(msg, LoginMessage.class);
            if (authClient(ctx.channel(), message.getUserName())) {
                //消息工厂类构造消息
                AbstractMessage loginRes = MessageWrapper.wrapMessage(MessageType.LOGIN_RESPONSE,
                        MessageType.LOGIN_RESPONSE.getCode(), AuthType.SUCCESS.getCode(), AuthType.SUCCESS.getMsg());
                //认证成功发送响应消息
                ctx.channel().writeAndFlush(Unpooled.copiedBuffer(loginRes.toByte()));
                //认证成功后,不在这个channel上继续认证
                ctx.pipeline().remove(this);
                //调用下一个handler,不经过这里永远卡在这个handler
                ctx.fireChannelRead(msg);
            } else {
                //认证失败则关闭,可在这里加认证次数限制逻辑,一个IP在一段时间内限制几次
                AbstractMessage loginRes = MessageWrapper.wrapMessage(MessageType.LOGIN_RESPONSE,
                        MessageType.LOGIN_RESPONSE.getCode(), AuthType.FAILURE.getCode(), AuthType.FAILURE.getMsg());
                ctx.channel().writeAndFlush(Unpooled.copiedBuffer(loginRes.toByte()));
                ctx.close();
                log.warn("[{}]认证失败", message.getUserName());
            }
        }
    }
    //认证逻辑
    private boolean authClient(Channel channel, String userName) {
        if ("admin".equals(userName) || "shark".equals(userName)) {
            //群聊ChannelGroup添加组员
            ChannelManage.CHANNEL_GROUP.add(channel);
            //存到Map中,此处不做解释,后面会用到
            ChannelManage.CHANNEL_MAP.put(channel.remoteAddress().toString(), channel);
            AbstractMessage chatMessage = MessageWrapper.wrapMessage(MessageType.CHAT_RESPONSE, MessageType.CHAT_RESPONSE.getCode(),
                    ChatMessageType.SERVER.getType(), "[客户端]" + channel.remoteAddress() + "上线了");
            ChannelManage.CHANNEL_GROUP.writeAndFlush(Unpooled.copiedBuffer(chatMessage.toByte()));
            log.info("[客户端]" + channel.remoteAddress() + "上线了");
            return true;
        }
        return false;
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
        if (ctx.channel().isActive()) {
            ctx.close();
        }
    }
less 复制代码
/**
 * @author shark
 * @version 1.0.0
 * @description 心跳
 * @date 2023/7/31 14:24
 */
@Slf4j
@Component
@ChannelHandler.Sharable
public class HeartbeatHandler extends ChannelDuplexHandler {

    // 自定义心跳内容
    private static final String HEARTBEAT_MESSAGE = "Heartbeat";

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent e = (IdleStateEvent) evt;
            //多少秒没有读请求
            if (e.state() == IdleState.READER_IDLE) {
                log.warn("Disconnecting due to no inbound traffic.");
                ctx.close();
             //多少秒没有写请求
            } else if (e.state() == IdleState.WRITER_IDLE) {
                AbstractMessage heartbeatMessage = MessageWrapper.wrapMessage(MessageType.HEARTBEAT,
                        MessageType.HEARTBEAT.getCode(),HEARTBEAT_MESSAGE);
                ctx.writeAndFlush(Unpooled.copiedBuffer(heartbeatMessage.toByte()));
            } else if (IdleState.ALL_IDLE == e.state()) {
                log.warn("No action");
            }
        }
    }
}
less 复制代码
/**
 * @author shark
 * @version 1.0.0
 * @description 聊天类
 * @date 2023/7/31 15:00
 */
@Slf4j
@Component
@ChannelHandler.Sharable
public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {

    @Override
    public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        JSONObject jsonObject = JSON.parseObject(msg);
        //聊天请求
        if (MessageType.CHAT.getCode() == jsonObject.getIntValue("code")) {
            ChatMessage chatMessage = JSON.parseObject(msg,ChatMessage.class);
            log.info("{}:{}", ctx.channel().remoteAddress(), chatMessage.getContent());
            //加入聊天组的群发
            broadcastMessage(ctx.channel(), chatMessage.getContent());
        }
    }

    private void broadcastMessage(Channel sender, String message) {
        ChannelManage.CHANNEL_GROUP.forEach(channel -> {
            AbstractMessage chatMessage;
            //是否发送者发的消息
            if (channel != sender) {
                chatMessage = MessageWrapper.wrapMessage(MessageType.CHAT_RESPONSE, MessageType.CHAT_RESPONSE.getCode(),
                        ChatMessageType.OTHER.getType(),message);
            } else {
                chatMessage = MessageWrapper.wrapMessage(MessageType.CHAT_RESPONSE, MessageType.CHAT_RESPONSE.getCode(),
                        ChatMessageType.MYSELF.getType(),message);
            }
            channel.writeAndFlush(Unpooled.copiedBuffer(chatMessage.toByte()));
        });
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //客户端建立连接时加入群聊组
        ChannelManage.CHANNEL_GROUP.remove(ctx.channel());
        ChannelManage.CHANNEL_MAP.remove(ctx.channel().remoteAddress().toString());
        AbstractMessage chatMessage = MessageWrapper.wrapMessage(MessageType.CHAT, MessageType.CHAT.getCode(),
                ChatMessageType.SERVER.getType(),"[客户端]" + ctx.channel().remoteAddress() + "下线了");
        ChannelManage.CHANNEL_GROUP.writeAndFlush(Unpooled.copiedBuffer(chatMessage.toByte()));
        log.info("{}------下线", ctx.channel().remoteAddress());
    }
}
java 复制代码
/**
 * @author shark
 * @version 1.0.0
 * @description
 * @date 2023/7/31 18:12
 */
public class ChannelManage {
    public static final ConcurrentHashMap<String, Channel> CHANNEL_MAP = new ConcurrentHashMap<>();
    public static final ChannelGroup CHANNEL_GROUP = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

}

枚举类

kotlin 复制代码
/**
 * @author shark
 * @version 1.0.0
 * @description 认证枚举
 * @date 2023/8/3 13:41
 */
@Getter
public enum AuthType {

    /* 登陆认证成功 */
    SUCCESS("1","认证成功"),
    /* 登陆认证失败 */
    FAILURE("0","认证失败");

    private final String code;
    private final String msg;

    AuthType(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}
typescript 复制代码
/**
 * @author shark
 * @version 1.0.0
 * @description
 * @date 2023/8/3 17:22
 */
@Getter
public enum ChatMessageType {
    /* 服务端 */
    SERVER("SERVER"),
    /* 客户端 */
    MYSELF("MYSELF"),
    /* 其他客户端 */
    OTHER("OTHER");

    public final String type;

    ChatMessageType(String type) {
        this.type = type;
    }
}
scss 复制代码
/**
 * @author shark
 * @version 1.0.0
 * @description
 * @date 2023/8/3 13:02
 */
@Getter
public enum MessageType {

    /* 登陆 */
    LOGIN(1),
    /* 登陆响应 */
    LOGIN_RESPONSE(2),
    /* 心跳 */
    HEARTBEAT(3),
    /* 聊天 */
    CHAT(4),
    /* 聊天响应 */
    CHAT_RESPONSE(5);

    private final int code;

    MessageType(int code) {
        this.code = code;
    }
}

消息封装类

java 复制代码
/**
 * @author shark
 * @version 1.0.0
 * @description 
 * @date 2023/8/3 12:43
 */
@Data
@NoArgsConstructor
public abstract class AbstractMessage implements Serializable {
    //消息类型
    private int code;

    private long timestamp;

    public AbstractMessage(int code) {
        this.code = code;
        this.timestamp = Instant.now().toEpochMilli();
    }

    public String toJson() {
        return JSON.toJSONString(this);
    }

    public byte[] toByte() {
        return toJson().concat("$").getBytes();
    }
}
less 复制代码
/**
 * @author shark
 * @version 1.0.0
 * @description
 * @date 2023/8/3 13:09
 */
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ChatMessage extends AbstractMessage {

    private String content;

    public ChatMessage(int code, String[] args) {
        super(code);
        this.content = args[0];
    }
}
less 复制代码
/**
 * @author shark
 * @version 1.0.0
 * @description
 * @date 2023/8/3 21:45
 */
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ChatResMessage extends AbstractMessage {
    //聊天消息类型(服务端,客户端,其他客户端)
    private String chatType;

    private String content;

    public ChatResMessage(int code, String[] args) {
        super(code);
        this.chatType = args[0];
        this.content = args[1];
    }
}
scala 复制代码
/**
 * @author shark
 * @version 1.0.0
 * @description
 * @date 2023/8/3 13:09
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class HeartMessage extends AbstractMessage {

    private String heartbeat;
    
    public HeartMessage(int code, String[] args) {
        super(code);
        this.heartbeat = args[0];
    }
}
less 复制代码
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class LoginMessage extends AbstractMessage {

    private String userName;
    private String password;

    public LoginMessage(int code, String[] args) {
        super(code);
        this.userName = args[1];
        this.password = args[2];
    }
}
less 复制代码
/**
 * @author shark
 * @version 1.0.0
 * @description
 * @date 2023/8/3 13:40
 */
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class LoginResMessage extends AbstractMessage {

    private String auth;
    private String msg;

    public LoginResMessage(int code, String[] args) {
        super(code);
        this.auth = args[0];
        this.msg = args[1];
    }
}

消息工厂类

typescript 复制代码
/**
 * @author shark
 * @version 1.0.0
 * @description
 * @date 2023/8/3 15:22
 */
public class MessageWrapper {
    private static final Map<MessageType, MessageConstructor> MESSAGE_CONSTRUCTORS = new HashMap<>();

    static {
        MESSAGE_CONSTRUCTORS.put(MessageType.LOGIN, LoginMessage::new);
        MESSAGE_CONSTRUCTORS.put(MessageType.CHAT, ChatMessage::new);
        MESSAGE_CONSTRUCTORS.put(MessageType.CHAT_RESPONSE, ChatResMessage::new);
        MESSAGE_CONSTRUCTORS.put(MessageType.HEARTBEAT, HeartMessage::new);
        MESSAGE_CONSTRUCTORS.put(MessageType.LOGIN_RESPONSE, LoginResMessage::new);
    }

    @FunctionalInterface
    private interface MessageConstructor {
        AbstractMessage create(int code, String... args);
    }

    public static AbstractMessage wrapMessage(MessageType type, int code, String... args) {
        MessageConstructor constructor = MESSAGE_CONSTRUCTORS.get(type);
        if (constructor == null) {
            throw new IllegalArgumentException("Unsupported message type: " + type);
        }
        return constructor.create(code, args);
    }

使用工厂类创建消息体更加的优雅,但是这里有一个弊端,是消息的构造方法,根据索引下标来获取消息,这里虽然是根据枚举类构造了不同的消息类型,但是当类型的种类越来越膨胀的时候,这里会越来越复杂,而且在序列化消息的时候,需要一个默认的构造方法,所以需要@NoArgsConstructor,否则带有数组的构造方法序列化后会有一个bug,当获取不到下标的回报错,虽然可以判空处理,但就失去了美感

Netty的主从reactor多线程模型

服务端代码已经完全贴在上面了,这个简单例子主要有消息的编解码,心跳机制的处理,认证服务的拦截以及工厂方法创建消息包,遇到的问题也写在注释在上面。

之前又聊到netty的线程模型,以下就八股一下netty的主从reactor多线程模型吧

接下来我要复制一下网上的结论

  • Reactor 主线程 MainReactor 对象通过 Select 监控建立连接事件,收到事件后通过 Acceptor 接收,处理建立连接事件;
  • Acceptor 处理建立连接事件后,MainReactor 将连接分配 Reactor 子线程给 SubReactor 进行处理;
  • SubReactor 将连接加入连接队列进行监听,并创建一个 Handler 用于处理各种连接事件;
  • 当有新的事件发生时,SubReactor 会调用连接对应的 Handler 进行响应;
  • Handler 通过 Read 读取数据后,会分发给后面的 Worker 线程池进行业务处理;
  • Worker 线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给 Handler 进行处理;
  • Handler 收到响应结果后通过 Send 将响应结果返回给 Client。 这个面试的时候背背就行类,想理解还是结合源码是想想这个过程

上面的过场走完了,我主要说明一下netty的多路复用器selector,网上介绍netty说它是事件驱动的nio,nio就是选择器轮询每个注册上的io请求,监听到io请求,就开启一个线程去处理请求。我之前准备面试的时候也是这么背的,然后看了b站上图灵的免费课,发现这么说对也不对,这个可能和netty的版本有关系,之前epoll还没出来,使用的select和poll,采用主动轮询的方式监听事件是否就绪。

下面就单reactor模型来解释,netty是如何高效的监听io请求,使时间复杂度到达O(1)

    1. 首先客户端与服务端三次握手后连接成功,会产生一个ScoketChannel注册到selector
    1. 在linux环境中selector通过调用系统函数epoll()监听文件描述符fd,文件描述符是计算机操作系统中用于唯一标识和访问文件或输入/输出资源的整数值。可以理解为进程打开文件的唯一索引,多个fd可能指向同一文件,epoll()会给fd注册事件,通过监听这些事件感应fd是否就绪(可读可写或者连接就绪),当fd准备就绪时,返回就绪的事件到一个队列里
    1. selector一直轮询就绪队列上的就绪时间然后调用对应的handler去处理

这样更高效的完成需要处理的事件,但是还是避免不了短时间产生大量就绪io请求的压力,比如在线人数达到几十万或者接近百万级别的话,需要做集群负载策略,借助redis绑定连接关系,通过网关路由到对应的服务端,有兴趣的可以去看看b站上号称千万级别im架构的视频。

相关推荐
0zxm1 小时前
06 - Django 视图view
网络·后端·python·django
m0_748257181 小时前
Spring Boot FileUpLoad and Interceptor(文件上传和拦截器,Web入门知识)
前端·spring boot·后端
小_太_阳2 小时前
Scala_【1】概述
开发语言·后端·scala·intellij-idea
智慧老师2 小时前
Spring基础分析13-Spring Security框架
java·后端·spring
搬码后生仔3 小时前
asp.net core webapi项目中 在生产环境中 进不去swagger
chrome·后端·asp.net
凡人的AI工具箱3 小时前
每天40分玩转Django:Django国际化
数据库·人工智能·后端·python·django·sqlite
Lx3524 小时前
Pandas数据重命名:列名与索引为标题
后端·python·pandas
小池先生4 小时前
springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失
java·spring boot·后端
百罹鸟4 小时前
【vue高频面试题—场景篇】:实现一个实时更新的倒计时组件,如何确保倒计时在页面切换时能够正常暂停和恢复?
vue.js·后端·面试
小蜗牛慢慢爬行5 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate