【Netty】利用Netty實現心跳檢測和重連機制

一、前言

  心跳機制是定時發送一個自定義的結構體(心跳包),讓對方知道自己還活着,以確保連接的有效性的機制。
  我們用到的很多框架都用到了心跳檢測,比如服務註冊到 Eureka Server 之後會維護一個心跳連接,告訴 Eureka Server 自己還活着。本文就是利用 Netty 來實現心跳檢測,以及客戶端重連。

二、設計思路

  1. 分爲客戶端和服務端
  2. 建立連接後,客戶端先發送一個消息詢問服務端是否可以進行通信了。
  3. 客戶端收到服務端 Yes 的應答後,主動發送心跳消息,服務端接收到心跳消息後,返回心跳應答,周而復始。
  4. 心跳超時利用 Netty 的 ReadTimeOutHandler 機制,當一定週期內(默認值50s)沒有讀取到對方任何消息時,需要主動關閉鏈路。如果是客戶端,重新發起連接。
  5. 爲了避免出現粘/拆包問題,使用 DelimiterBasedFrameDecoderStringDecoder 來處理消息。

三、編碼

  1. 先編寫客戶端 NettyClient
public class NettyClient {

    private static final String HOST = "127.0.0.1";

    private static final int PORT = 9911;

    private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

    EventLoopGroup group = new NioEventLoopGroup();


    private void connect(String host,int port){
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY,true)
                    .remoteAddress(new InetSocketAddress(host,port))
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ByteBuf delimiter = Unpooled.copiedBuffer("$_", CharsetUtil.UTF_8);
                            ch.pipeline()
                                    .addLast(new DelimiterBasedFrameDecoder(1024,delimiter))
                                    .addLast(new StringDecoder())
                                    // 當一定週期內(默認50s)沒有收到對方任何消息時,需要主動關閉鏈接
                                    .addLast("readTimeOutHandler",new ReadTimeoutHandler(50))
                                    .addLast("heartBeatHandler",new HeartBeatReqHandler());
                        }
                    });
            // 發起異步連接操作
            ChannelFuture future = b.connect().sync();
            future.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 所有資源釋放完之後,清空資源,再次發起重連操作
            executor.execute(()->{
                try {
                    TimeUnit.SECONDS.sleep(5);
                    //發起重連操作
                    connect(NettyClient.HOST,NettyClient.PORT);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }

    public static void main(String[] args) {
        new NettyClient().connect(NettyClient.HOST,NettyClient.PORT);
    }

}

這裏稍微複雜點的就是38行開始的重連部分。
2. 心跳消息發送類 HeartBeatReqHandler

package cn.sp.heartbeat;

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 * Created by 2YSP on 2019/5/23.
 */
@ChannelHandler.Sharable
public class HeartBeatReqHandler extends SimpleChannelInboundHandler<String> {

    private volatile ScheduledFuture<?> heartBeat;

    private static final String hello = "start notify with server$_";

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer(hello.getBytes()));
        System.out.println("================");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        if (heartBeat != null){
            heartBeat.cancel(true);
            heartBeat = null;
        }
        ctx.fireExceptionCaught(cause);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        if ("ok".equalsIgnoreCase(msg)){
            //服務端返回ok開始心跳
            heartBeat = ctx.executor().scheduleAtFixedRate(new HeartBeatTask(ctx),0,5000, TimeUnit.MILLISECONDS);
        }else {
            System.out.println("Client receive server heart beat message : --->"+msg);
        }
    }

    private class HeartBeatTask implements Runnable{

        private final ChannelHandlerContext ctx;

        public HeartBeatTask(ChannelHandlerContext ctx){
            this.ctx = ctx;
        }


        @Override
        public void run() {
            String heartBeat = "I am ok";
            System.out.println("Client send heart beat message to server: ----->"+heartBeat);
            ctx.writeAndFlush(Unpooled.copiedBuffer((heartBeat+"$_").getBytes()));
        }
    }
}

channelActive()方法在首次建立連接後向服務端問好,如果服務端返回了 “ok” 就創建一個線程每隔5秒發送一次心跳消息。如果發生了異常,就取消定時任務並將其設置爲 null,等待 GC 回收。
3. 服務端 NettyServer

public class NettyServer {

    public static void main(String[] args) {
        new NettyServer().bind(9911);
    }

    private void bind(int port){
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(group)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());

                            ch.pipeline()
                                    .addLast(new DelimiterBasedFrameDecoder(1024,delimiter))
                                    .addLast(new StringDecoder())
                                    .addLast("readTimeOutHandler",new ReadTimeoutHandler(50))
                                    .addLast("HeartBeatHandler",new HeartBeatRespHandler());
                        }
                    });
            // 綁定端口,同步等待成功
             b.bind(port).sync();
            System.out.println("Netty Server start ok ....");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
  1. 心跳響應類 HeartBeatRespHandler
package cn.sp.heartbeat;

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

/**
 * Created by 2YSP on 2019/5/23.
 */
@ChannelHandler.Sharable
public class HeartBeatRespHandler extends SimpleChannelInboundHandler<String> {

    private static final String resp = "I have received successfully$_";

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        if (msg.equals("start notify with server")){
            ctx.writeAndFlush(Unpooled.copiedBuffer("ok$_".getBytes()));
        }else {
            //返回心跳應答信息
            System.out.println("Receive client heart beat message: ---->"+ msg);
            ctx.writeAndFlush(Unpooled.copiedBuffer(resp.getBytes()));
        }
    }

}

第一次告訴客戶端我已經準備好了,後面打印客戶端發過來的信息並告訴客戶端我已經收到你的消息了。

四、測試

啓動服務端再啓動客戶端,可以看到心跳檢測正常,如下圖。
服務端控制檯:
在這裏插入圖片描述
客戶端控制檯:
在這裏插入圖片描述
現在讓服務端宕機一段時間,看客戶端能否重連並開始正常工作。

關閉服務端後,客戶端週期性的連接失敗,控制檯輸出如圖:
在這裏插入圖片描述
重新啓動服務端,過一會兒就會發現重連成功了。
在這裏插入圖片描述

五、總結

總得來說,使用 Netty 實現心跳檢測還是比較簡單的,這裏比較懶沒有使用其他序列化協議(如 ProtoBuf 等),如果感興趣的話大家可以自己試試。
代碼地址,點擊這裏
有篇SpringBoot 整合長連接心跳機制的文章寫的也很不錯,地址https://crossoverjie.top/2018/05/24/netty/Netty(1)TCP-Heartbeat/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章