netty 心跳包和断线重连机制

为什么需要心跳包???

心跳包主要是用来做TCP长连接保活的。有时 socket 虽然是连接的但中间网络可能有问题,这时你还在不停的往外发送数据,但对方是收不到的,你不知道对方是不是还活着,不知道 socket 通道是不是还是联通的。 心跳包就是你发送一些试探包给对方,对方回应,如果一定时间内比如30秒内没有收到任何数据,说明对方或网络可能有问题了。这时你主动断开 socket 连接,避免浪费资源。

TCP 本来就有 keepAlive 机制为什么还需要应用层自己实现心跳???

TCP keepAlive 也是在一定时间内(默认2小时)socket 上没有接收到数据时主动断开连接,避免浪费资源,这时远端很可能已经down机了或中间网络有问题。也是通过发送一系列试探包看有没有回应来实现的。

TCP keepAlive 依赖操作系统,默认是关闭的,需要修改操作系统配置打开。
所以在应用层实现心跳包还是必须的。

netty 中通过 IdleStateHandler 在空闲的时候发送心跳包

为什么在空闲的时候发送心跳包,而不是每隔固定时间发送???

这个是显而易见的,正常通信时说明两端连接是没有问题的,所以只在空闲的时候发送心跳包。如果每隔固定时间发送就会浪费资源占用正常通信的资源。

假设现在要做一个手机端推送的项目,所有手机通过 TCP 长连接连接到后台服务器。心跳机制是这样的:

  1. 手机端在写空闲的时候发送心跳包给服务端,用 IdleStateHandler 来做 socket 的空闲检测。 如果 5 秒内没有写任何数据,则发送心跳包到服务端
ch.pipeline().addLast(new IdleStateHandler(0, 5, 0, TimeUnit.SECONDS));
ch.pipeline().addLast(new HeartbeatKeeper());

@ChannelHandler.Sharable
public class HeartbeatKeeper extends ChannelInboundHandlerAdapter {
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleState state = ((IdleStateEvent) evt).state();
            if (state == IdleState.WRITER_IDLE) {
                System.out.println("client send heart beat");
                ctx.channel().writeAndFlush("heart beat\n");
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }
}
  1. 服务端设置读超时,如果 30 秒内没有收到一个客户端的任何数据则关闭连接。
ch.pipeline().addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
ch.pipeline().addLast(new IdleStateTrigger());

@ChannelHandler.Sharable
public class IdleStateTrigger extends ChannelInboundHandlerAdapter {

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleState state = ((IdleStateEvent) evt).state();
            if (state == IdleState.READER_IDLE) {
                ctx.channel().close();
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }
}

服务端接收到心跳包后要不要回复???

看其他博客说不要回复,如果有 10万空闲连接,光回复心跳包就要占用大量资源。服务端读超时后直接关闭连接,客户端再进行重连。

断线重连

断线重连也很简单就是在 channelInactive 的时候重新 connect 就行了。参考其他博客专门用一个 ChannelInboundHandler 来处理断线重连。

@ChannelHandler.Sharable
public class ConnectionWatchDog extends ChannelInboundHandlerAdapter implements TimerTask {
    private final Bootstrap bootstrap;
    private final String host;
    private final int port;
    private volatile boolean reconnect;
    private int attempts;
    private Channel channel;
    private HashedWheelTimer timer = new HashedWheelTimer();
    private int reconnectDelay = 5;

    public ConnectionWatchDog(Bootstrap bootstrap, String host, int port, boolean reconnect) {
        this.bootstrap = bootstrap;
        this.host = host;
        this.port = port;
        this.reconnect = reconnect;
    }

    public Channel getChannel() {
        return this.channel;
    }

    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channelActive");
        channel = ctx.channel();
        ctx.fireChannelActive();
    }

    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channelInactive");
        ctx.fireChannelInactive();
        channel = null;

        if (reconnect) {
            attempts = 0;
            scheduleReconnect();
        }
    }

    private void connect() {
        bootstrap.connect(host, port).addListener((future) -> {
            if (future.isSuccess()) {
                System.out.println("connected to " + host + ":" + port);
                attempts = 0;
            } else {
                System.out.println("connect failed " + attempts + " , to reconnect after " + reconnectDelay + " 秒");
                // 这里现在每5秒重连一次直到连接上,可自己实现重连逻辑
                scheduleReconnect();
            }
        });
    }

    public void run(Timeout timeout) {
        synchronized (this.bootstrap) {
            ++attempts;
            connect();
        }
    }

    private void scheduleReconnect() {
        timer.newTimeout(this, reconnectDelay, TimeUnit.SECONDS);
    }

    public void setReconnect(boolean reconnect) {
        this.reconnect = reconnect;
    }
}

这个 watchDog Handler 应当放在 ChannelPipeline 的最前面

public void connect(String host, int port) {
    Bootstrap bootstrap = new Bootstrap().group(new NioEventLoopGroup())
            .channel(NioSocketChannel.class)
            .option(ChannelOption.TCP_NODELAY, true)
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);

    watchDog = new ConnectionWatchDog(bootstrap, host, port, true);
    bootstrap.handler(new ChannelInitializer<SocketChannel>() {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline().addLast(watchDog);
            ch.pipeline().addLast(new IdleStateHandler(0, 5, 0, TimeUnit.SECONDS));
            ch.pipeline().addLast(new HeartbeatKeeper());
            ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
            ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
            ch.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));

            ch.pipeline().addLast(new ClientDemoHandler());
        }
    });

    // 这里如果第一次连接不成功也可以尝试多次连接
    bootstrap.connect(host, port);
}

客户端给服务端发送心跳包,服务端用给客户端发心跳包吗???

其实客户端和服务端都是相对的,这个看应用场景。如果客户端想要及时处理断网,路由故障等情况就需要接受服务端发来的心跳来检测。像断网,路由故障这种情况,两边都不知道TCP连接的状态,必须靠心跳。长连接服务端一般都要接收心跳包的,如果没有心跳可能会有大量的无效连接,直接耗尽服务器资源,无效的连接要尽早关闭掉。

DEMO:
https://github.com/lesliebeijing/Netty-Demo

基于 Netty 写的一个简单的推送 DEMO,可用在手机端推送
https://github.com/lesliebeijing/EncPush

Netty 客户端用在 Android 中也很稳定,我们的物联网项目Android和后台都是用的 Netty。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,158评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,600评论 1 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,785评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,655评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,075评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,002评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,146评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,918评论 0 211
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,671评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,838评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,318评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,636评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,343评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,187评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,982评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,126评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,934评论 2 279