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

306 阅读2分钟

承接上篇文章Netty实战之认证机制+心跳机制+群聊(服务端) 下篇netty客户端文章在此。

maven依赖

<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>

yml

netty:
  port: 9090
#  ip: 10.10.10.28
  reconnectDelay: 5

代码实现

客户端

/**
 * @author shark
 */
@Slf4j
@Component("nettyClientRunner")
public class NettyClientRunner implements ApplicationRunner {

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

    @Value("${netty.ip}")
    private String ip;
    //重连次数
    @Value("${netty.reconnectDelay}")
    private int reconnectDelay;

    private final EventLoopGroup workerGroup = new NioEventLoopGroup();
    //创建一个单线程池用于控制台输出
    private final ExecutorService inputExecutor = createSingleThreadExecutor();


    @Override
    public void run(ApplicationArguments args) throws Exception {
        connect();
    }

    private ExecutorService createSingleThreadExecutor() {
        return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(), r -> {
            Thread thread = new Thread(r);
            thread.setName("UserInputThread");
            thread.setDaemon(true);
            return thread;
        });
    }
    public void connect() {
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(workerGroup)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .handler(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 LoginServerHandler());
                        pipeline.addLast(new HeartbeatHandler());
                        //pipeline.addLast(new ReconnectHandler(NettyClientRunner.this));
                        pipeline.addLast(new GroupChatClientHandler());
                    }
                });
        try {
            ChannelFuture future = bootstrap.connect(ip, port);
            future.addListener((ChannelFutureListener) channelFuture -> {
                if (channelFuture.isSuccess()) {
                    log.info("Connected to server at {}:{}", ip, port);
                } 
                //去掉断线重连逻辑
//                else {
//                    log.error("Connection to server failed. Scheduling reconnection in {} seconds.", reconnectDelay);
//                    channelFuture.channel().eventLoop().schedule(this::connect, reconnectDelay, TimeUnit.MINUTES);
//                }
            });
            //控制台输出
            inputExecutor.execute(() -> handleUserInput(future.channel()));
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("Client connection interrupted", e);
        }
    }

    private void handleUserInput(Channel channel) {
        try (Scanner scanner = new Scanner(System.in)) {
            while (!Thread.currentThread().isInterrupted()) {
                if (scanner.hasNextLine()) {
                    String msg = scanner.nextLine();
                    AbstractMessage chat = MessageWrapper.wrapMessage(MessageType.CHAT, MessageType.CHAT.getCode(), msg);
                    channel.writeAndFlush(Unpooled.copiedBuffer(chat.toByte()));
                }
            }
        } catch (Exception e) {
            log.error("Error reading user input", e);
        }
    }
    
    //应用断开时关闭资源
    @PreDestroy
    public void shutdown() {
        log.info("Shutting down the Netty client gracefully...");
        workerGroup.shutdownGracefully();
        inputExecutor.shutdown();
    }
}
/**
 * @author shark
 * @version 1.0.0
 * @description 登陆类
 * @date 2023/8/3 11:16
 */
@Slf4j
@ChannelHandler.Sharable
public class LoginServerHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        JSONObject jsonObject = JSON.parseObject(msg);
        if (MessageType.LOGIN_RESPONSE.getCode() == jsonObject.getIntValue("code")) {
            LoginResMessage message = JSON.parseObject(msg,LoginResMessage.class);
            if (AuthType.SUCCESS.getCode().equals(message.getAuth())) {
                log.info(message.getMsg());
                ctx.pipeline().remove(this);
                ctx.fireChannelRead(msg);
            }else{
                log.error("server auth fail");
                ctx.close();
            }
        }

    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        AbstractMessage loginMessage = MessageWrapper.wrapMessage(MessageType.LOGIN,MessageType.LOGIN.getCode(),
                "admin","123456");
        ctx.channel().writeAndFlush(Unpooled.copiedBuffer(loginMessage.toByte()));
        ctx.fireChannelActive();
    }
}
/**
 * @author shark
 * @version 1.0.0
 * @description 心跳
 * @date 2023/7/31 14:24
 */
@Slf4j
@ChannelHandler.Sharable
public class HeartbeatHandler extends ChannelInboundHandlerAdapter {

    private static final String HEARTBEAT_MESSAGE = "Heartbeat";

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        //与客户端连接时5秒一次心跳,时间间隔与服务端读超时一致
        scheduleHeartbeat(ctx);
    }

    private void scheduleHeartbeat(ChannelHandlerContext ctx) {
        ctx.executor().schedule(() -> {
            if (ctx.channel().isActive()) {
                AbstractMessage heartbeatMessage = MessageWrapper.wrapMessage(MessageType.HEARTBEAT,
                        MessageType.HEARTBEAT.getCode(),HEARTBEAT_MESSAGE);
                ctx.writeAndFlush(Unpooled.copiedBuffer(heartbeatMessage.toByte()));
                scheduleHeartbeat(ctx);
            }
        }, 5, TimeUnit.SECONDS);
    }
}
/**
 * @author shark
 * @version 1.0.0
 * @description 断线重连
 * @date 2023/8/2 09:48
 */
@Slf4j
@ChannelHandler.Sharable
public class ReconnectHandler extends ChannelDuplexHandler {

    private final NettyClientRunner nettyClientRunner;

    public ReconnectHandler(NettyClientRunner nettyClientRunner) {
        this.nettyClientRunner = nettyClientRunner;
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //与服务器断开连接时,重试连接
        log.info("server connect failed");
        nettyClientRunner.connect();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("{} 错误关闭",ctx.channel().remoteAddress());
        super.exceptionCaught(ctx, cause);
        if (ctx.channel().isActive()) {
            ctx.close();
        }
    }
}
/**
 * @author shark
 * @version 1.0.0
 * @description  聊天类
 * @date 2023/7/31 15:05
 */
@Slf4j
@ChannelHandler.Sharable
public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {

    @Override
    public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        JSONObject jsonObject = JSON.parseObject(msg);
        //聊天类消息
        if (MessageType.CHAT_RESPONSE.getCode() == jsonObject.getIntValue("code")) {
            ChatResMessage chatMessage = JSON.parseObject(msg,ChatResMessage.class);
            if (ChatMessageType.SERVER.getType().equals(chatMessage.getChatType())) {
                log.info("服务端消息:{}",chatMessage.getContent());
            }else if (ChatMessageType.MYSELF.getType().equals(chatMessage.getChatType())) {
                log.info("自己发的消息:{}",chatMessage.getContent());
            }else if (ChatMessageType.OTHER.getType().equals(chatMessage.getChatType())) {
                log.info("{}:{}",ctx.channel().remoteAddress(),chatMessage.getContent());
            }
        }
    }
}

其中的枚举类,消息类,消息工厂类同Netty实战之认证机制+心跳机制+群聊(服务端)

总结

操作步骤

首先启动服务端,再启动客户端一 image.png image.png 然后启动客户端二 image.png 服务端知道客户端一和二上线了 image.png 此时客户端二收到服务端发来客户端一上线的消息 image.png 客户端一发出消息,服务端发来自己发的消息 image.png 服务端收到消息,并向聊天室内的组员发消息 image.png 客户端二收到服务端发来客户端一发的消息 image.png 客户端二发消息 image.png 客户端一收到服务端发来客户端二发的消息 image.png 在此说明,客户端和客户端是不能总结发消息的,都是需要服务端进行中转的