Netty的粘包拆包&心跳机制&断线重连

编解码

编解码的概念

Netty的编解码其实很好理解,不管是Netty还是传统的socket连接也需要做编解码,比如说 tomcat,tomcat的请求过程中,比如在bio的传统交互过程中,http定义了一种协议,那么过程就是从socket中读取流到tomcat bio自己的缓冲区中,然后从缓冲区中按照一定的格式,比如先解析请求行(GET/POST /XXX/XX.do HTTP/1.1),然后解析请求体,然后调用适配器调用Pipeline管道的Valve方法最终到达过滤器链,再到程序员编写的servlet,然后servlet处理完成以后,还不是要编码成浏览器端能够识别的返回数据,所以编解码再网络交互的过程中是无处不在的,在公司中一直做第三方交互的业务模块中可能对这个比较熟悉,比如你需要接入第三方的一个系统,那么第三方可能会给你一大堆的api接口并且他们的报文传输格式,那么你拿到这个文档之后,肯定在交互的中间会加入一个报文转换层,就是第三方进来过后,你收到报文过后需要解码你自己系统能够识别的报文,然后你处理完成过后返回给对方的时候,也还需要编码成对方能够识别的报文格式,这就是编解码,所以在netty中也是有这种概念,但是netty中可能有点不太一样,做过网络的都知道在网络传输过程中,是不能够直接传输字符串,对象之类,需要将这些编码成 二进制流数组,然后进行传输。所以在netty中也是一样的,netty也是基于网络协议来的,所以在网络上传输netty也要遵循二进制流的传输格式。
在这里插入图片描述

Netty的编解码

Netty涉及到编解码的组件有Channel、ChannelHandler、ChannelPipe等,先大概了解下这几个组件的作用。
ChannelHandler
ChannelHandler充当了处理入站和出站数据的应用程序逻辑容器。例如,实现ChannelInboundHandler接口(或ChannelInboundHandlerAdapter),你就可以接收入站事件和数据,这些数据随后会被你的应用程序的业务逻辑处理。当你要给连接的客户端发送响应时,也可以从ChannelInboundHandler冲刷数据。你的业务逻辑通常写在一个或者多个ChannelInboundHandler中。ChannelOutboundHandler原理一样,只不过它是用来处理出站数据的。
ChannelPipeline
ChannelPipeline提供了ChannelHandler链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过pipeline中的一系列ChannelOutboundHandler(ChannelOutboundHandler调用是从tail到head方向逐个调用每个handler的逻辑),并被这些Handler处理,反之则称为入站的,入站只调用pipeline里的ChannelInboundHandler逻辑(ChannelInboundHandler调用是从head到tail方向逐个调用每个handler的逻辑)。
在这里插入图片描述
编码解码器
当你通过Netty发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种格式(比如java对象);如果是出站消息,它会被编码成字节。
Netty提供了一系列实用的编码解码器,他们都实现了ChannelInboundHadnler或者ChannelOutboundHandler接口。在这些类中,channelRead方法已经被重写了。以入站为例,对于每个从入站Channel读取的消息,这个方法会被调用。随后,它将调用由已知解码器所提供的decode()方法进行解码,并将已经解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler。
Netty提供了很多编解码器,比如编解码字符串的StringEncoder和StringDecoder,编解码对象的ObjectEncoder和ObjectDecoder等。

根据上图可以知道Netty的数据通信是需要经过管道的一些列的处理器,有个规则就是当接受数据的时候是需要解码的,所以是从head进行进入,tail出来,而不是从head到tail的所有处理器都会执行,比如是接受数据,那么在管道中的处理器只会经过解码器,而编码器不会执行,而如果是发送数据,这个时候就是从tail到head,而且只会经过编码器,所以:
接受数据:从head—>tail,解码器;
发送数据:从tail------>head,编码器

示例

前面说了,如果我们向网络中发送一个字符串,是发不出去的,在netty中有一个对象是ByteBuf,这个对象的array就是封装的一组数据的二进制,所以netty中收发数据都是通过这个来实现的,发送的时候将你的发送的内容封装成ByteBuf,然后接受的时候将二进制数据转成ByteBuf,然后交给我们处理器来处理,关于字符串是没有办法发出去的,下来有兴趣的可以去试,你不就加任何的处理器肯定是发布出去的,太简单的东西就不记录了;在上一篇的笔记中记录了一个使用netty实现的聊天系统,那么聊天系统都是发送的字符串,但是为什么能够发出去呢?我们先看下几个代码片段:
在这里插入图片描述
这是Netty的一个客户端,启动过后,通过监听控制台的输入,那然将收入的字符串发送到server端,但是server是接受到的,那为什么能够接受到呢?所以关键点就在管道的处理器,看上图是不是有三个处理器
StringEncoder:这个是String的编码器,发送数据的时候调用,在管道中是属于head
StringDecoder:这个是String字符串的解码器,在接受数据时候调用
ChatClientHandler:这个是我们的自定义的一个处理器,在管道中是属于tail
处理器的之间的顺序是否能够调换呢?如果是不同类型的是可以调换,但是同类型的是不能够调换的,比如上面的三个处理器,其中ChatClientHandler作为接受数据的最后对数据的应用,而如果是发送数据的话,它是第一个发出去的原始 数据,如果这个时候你把这个处理器放在管道的head上,那肯定不行啊,管道接受到的数据是一个二进制,而这个处理器要得到的数据是string字符串,所以肯定不行,所以当你在写netty的处理器的时候,一定要明白的一个就是你的处理器顺序代表着不同的数据处理逻辑,至于是否能够调换就要根据你自己数据的业务逻辑和容错性。

自定义发送对象

这里自定义一个编解码处理器,用来处理对象的传输,我们都知道,对象肯定是不能在网络上直接传输的,需要对其进行处理,比如jdk最原始的序列化和反序列化,简单理解就是在发送数据的时候将发送的对象编码成一个字符串流(序列化),对方接受到过后解码成一个对象使用(反序列化),就是这么一个概念。

JDK原始的序列化

在netty中已经帮我们实现了jdk原始的序列化和反序列化,其实看源码就很清楚的知道就是使用的JDK的序列化和反序列化,就是ObjectEncoder和ObjectDecoder,如果需要使用就非常简单 ,只需要在处理器中直接使用即可


import com.bml.architect.netty.serialiable.codec.ProtostuffDecoder;
import com.bml.architect.netty.serialiable.codec.ProtostuffEncoder;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.serialization.ClassResolver;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;

public class NettySerialServer {

    public static void main(String[] args) {
        EventLoopGroup boss = new NioEventLoopGroup(1);
        EventLoopGroup work = new NioEventLoopGroup();


        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boss, work)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024).
                    childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();
                   // pipeline.addLast(new ObjectDecoder(10240, ClassResolvers.cacheDisabled(null)));
                    //pipeline.addLast(new ObjectEncoder());
                    pipeline.addLast(new ProtostuffDecoder());
                    pipeline.addLast(new ProtostuffEncoder());
                    pipeline.addLast(new SerialServerHandler());
                }
            });
            System.out.println("服务端启动完成...");
            ChannelFuture sync = bootstrap.bind(9000).sync();
            sync.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            boss.shutdownGracefully();
            work.shutdownGracefully();
        }

    }
}

import com.bml.architect.netty.base.NettyClientHandler;
import com.bml.architect.netty.serialiable.codec.ProtostuffDecoder;
import com.bml.architect.netty.serialiable.codec.ProtostuffEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;

public class NettySerialClient {

    public static void main(String[] args) {
        EventLoopGroup clientGoup = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(clientGoup)
                    .channel(NioSocketChannel.class)// 使用 NioSocketChannel 作为客户端的通道实现
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new ObjectDecoder(10240, ClassResolvers.cacheDisabled(null)));
                            pipeline.addLast(new ObjectEncoder());
                            pipeline.addLast(new SerialClientHandler());
                        }
                    });

            //启动客户端去连接服务器端
            System.out.println("客户端启动完成...");

            ChannelFuture cf = bootstrap.connect("127.0.0.1", 9000).sync();
            //对关闭通道进行监听
            cf.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            clientGoup.shutdownGracefully();
        }
    }
}

public class SerialClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("接受到服务端发送的消息:" +msg);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("开始向服务端发送数据");
        User user = new User();
        user.setId(1);
        user.setName("白茂林");
        user.setAddress("四川成都市");
        ctx.writeAndFlush(user);
    }
}
public class SerialServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("接受到客户端发送过来的消息="+msg);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端数据读取完毕");
        User user = new User();
        user.setAddress("四川成都市温江区");
        user.setName("李四");
        user.setId(2);
        ctx.writeAndFlush(user);
    }
}

上面这段代码的含义就是启动一个服务端,然后启动客户端,客户端启动的时候向服务端发送要给对象User,然后服务端接受到这个过后,当数据读写完毕,回写一个user对象,比较简单的一个例子,因为上面说了对象是不能直接在网络上传输的,所以就需要使用到编解码器,而netty中默认的编解码器采用的是JDK原始的对象序列化和反序列化实现的,实现的类是ObjectEncoder和ObjectDecoder,只需要在处理器管道中加入这两个编解码器就可以了,看上面的例子;但是JDK原始的这种序列化和反序列化在最求高性能的场景下可能表现不是那么的好,所以如果性能最求上需要达到更高的化,序列化还有一种实现就是google开源的protobuf,但是protobuf需要维护大量的proto文件比较麻烦,现在一般可以使用protostuff。

protostuff序列化

protostuff是一个基于protobuf实现的序列化方法,它较于protobuf最明显的好处是,在几乎不损耗性能的情况下做到了不用我们写.proto文件来实现序列化。使用它也非常简单,代码如下:
引入依赖:

<dependency>
    <groupId>com.dyuproject.protostuff</groupId>
    <artifactId>protostuff-api</artifactId>
    <version>1.0.10</version>
</dependency>
<dependency>
    <groupId>com.dyuproject.protostuff</groupId>
    <artifactId>protostuff-core</artifactId>
    <version>1.0.10</version>
</dependency>
<dependency>
    <groupId>com.dyuproject.protostuff</groupId>
    <artifactId>protostuff-runtime</artifactId>
    <version>1.0.10</version>
</dependency>

protostuff使用示例:

import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.Schema;
import com.dyuproject.protostuff.runtime.RuntimeSchema;

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

/**
 * protostuff 序列化工具类,基于protobuf封装
 */
public class ProtostuffUtil {

    private static Map<Class<?>, Schema<?>> cachedSchema = new ConcurrentHashMap<Class<?>, Schema<?>>();

    private static <T> Schema<T> getSchema(Class<T> clazz) {
        @SuppressWarnings("unchecked")
        Schema<T> schema = (Schema<T>) cachedSchema.get(clazz);
        if (schema == null) {
            schema = RuntimeSchema.getSchema(clazz);
            if (schema != null) {
                cachedSchema.put(clazz, schema);
            }
        }
        return schema;
    }

    /**
     * 序列化
     *
     * @param obj
     * @return
     */
    public static <T> byte[] serializer(T obj) {
        @SuppressWarnings("unchecked")
        Class<T> clazz = (Class<T>) obj.getClass();
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        try {
            Schema<T> schema = getSchema(clazz);
            return ProtostuffIOUtil.toByteArray(obj, schema, buffer);
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        } finally {
            buffer.clear();
        }
    }

    /**
     * 反序列化
     *
     * @param data
     * @param clazz
     * @return
     */
    public static <T> T deserializer(byte[] data, Class<T> clazz) {
        try {
            T obj = clazz.newInstance();
            Schema<T> schema = getSchema(clazz);
            ProtostuffIOUtil.mergeFrom(data, obj, schema);
            return obj;
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }

    public static void main(String[] args) {
        byte[] userBytes = ProtostuffUtil.serializer(new User(1, "bml","四川成都市"));
        User user = ProtostuffUtil.deserializer(userBytes, User.class);
        System.out.println(user);
    }
}

但是如果在netty中,你也可一个工具类,然后每次在处理器中直接调用相关的api去处理,但是我们也可以将它写成要给编解码器,来替换ObjectEncoder和ObjectDecoder,那么如何来写呢?其实就很简单,根据你自己传输的格式去做就可以了,我这里简单写了下,只提供一种思路,可能真实的业务场景中还更复杂一点,我这里记录的是笔记,所以写的笔记简单

import com.bml.architect.entity.User;
import com.bml.architect.util.ProtostuffUtil;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.List;

public class ProtostuffDecoder extends ByteToMessageDecoder {


    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {

        int readLen = in.readableBytes();
        if(readLen > 0){
            byte [] array = new byte[readLen];
            in.readBytes(array);
            out.add(ProtostuffUtil.deserializer(array, User.class));
        }

    }
}

这个是解码的,比如客户端发送了一个对象过来,这个对象是被Protostuff序列化的,我这里需要解码,然后进行反序列化,readLen表示是否有数据过来,如果有就读取readLen长度,readLen长度就是ByteBuf中的writeIndex-readIndex的长度就是真实发送的数据长度


public class ProtostuffEncoder extends MessageToByteEncoder<User> {
    @Override
    protected void encode(ChannelHandlerContext ctx, User msg, ByteBuf out) throws Exception {
        out.writeBytes(ProtostuffUtil.serializer(msg));
    }
}

发送我这里写的比较简单,就是简单的序列化过后发送出去;编解码器写好过后,直接在上面的例子中替换ObjectEncoder和ObjectDecoder,其他代码都不用动,也能达到效果,这里就不演示了。

Netty粘包拆包

粘包和拆包是什么意思呢?我们都知道TCP是一个流协议,就是没有界限的一长串二进制数据。TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区
的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。面向流的通信是无消息保护边界的;简单来说就是比如客户端发送比较频繁,而服务端从缓冲区读取数据的时候可能读取错位,因为所有的数据都会到缓冲区中,服务端也不知道你发送了多少数据,只能依此读取,所以说粘包和拆包不它不是Netty中的问题,只要在网络中传输都会存在这个种问题,Netty的底层原理是epoll,但是传统的BIO它也是通过网络发送,缓冲区接受的,所以都会有这种问题
在这里插入图片描述
如上图,客户端发送数据 D1和D2,如果服务端正常读取数据就是先读取了D1,在读取D2,但是有可能出现D1和D2出现一起了,这就是粘包,也有可能D1和D2的一部分在一起了,D2的一部分和D1一部分在一起,反正就是出现了数据被分离了,这就叫做拆包;如果还不是很明白的话,下面给个例子,就上节课的聊天程序,我们启动服务端,先启动一个客户端,然后启动第二个客户端的时候循环发送消息,循环发送的时候,服务端会将消息转发给其他客户端
在这里插入图片描述
上面的这段程序的输出就很明显的测试处了Netty的粘包和拆包,上图种的第一个红框框就出现了粘包,第二个红框框出现拆包,因为我发送的是中文,在UTF-8种,一个中文3个字节,而出现拆包过后,字节被拆掉,所以出现乱码,那么出现这种肯定是不行的,我们的交易的过程中肯定要保证数据的完整性,那么应该怎么做呢?

解决方案

1)消息定长度,传输的数据大小固定长度,例如每段的长度固定为100字节,如果不够空位补空格
2)在数据包尾部添加特殊分隔符,比如下划线,中划线等,这种方法简单易行,但选择分隔符的时候一定要注意每条数据的内部一定不能出现分隔符。
3)发送长度:发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度来判断每条数据的开始和结束。
Netty提供了多个解码器,可以进行分包的操作,如下:
1.LineBasedFrameDecoder (回车换行分包)
2.DelimiterBasedFrameDecoder(特殊分隔符分包)
3.FixedLengthFrameDecoder(固定长度报文来分包)
我这里就演示下其中一个,就是自定义分隔符DelimiterBasedFrameDecoder,使用方法也非常简单,就是在处理器管道中增加一个处理器

pipeline.addLast(new DelimiterBasedFrameDecoder(10240, Unpooled.copiedBuffer("_".getBytes())));

第一个参数表示接受的数据的最大字节,第二 个参数表示自定义的分隔符,在接受数据方的管道中增加这个处理器过后,输出如下:
在这里插入图片描述
数据就规整了,这就是解决粘包和拆包的一种方案,但是试想一下,真正的生产系统中,可能出现的字符千变万化,如果你定义的分割字符是|,那么就必须保证传输的报文中不能存在|这个字符,这就非常受限了,万一出现了这个字符,那么完蛋了,接受的数据就乱了,所以现在在企业中乃至于银行中用的非常多的一种方案是增加报文长度,比如http请求中都 有一个content-length来表示数据包的长度,根据这个长度来读取数据即可,简单来说就是数据长度+数据内容,那么接收方先拿到这个数据长度,根据这个数据长度来读取数据即可。
定义一个消息对象
length是要发送的消息长度,context是消息的内容


public class MsgProto {

    private int length;

    private byte []content;


    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    public byte[] getContent() {
        return content;
    }

编解码器
编码器将先写入长度,再写入内容,解码器先读取长度,再根据长度读取内容

public class MsgProtoEncoder extends MessageToByteEncoder<MsgProto> {
    @Override
    protected void encode(ChannelHandlerContext ctx, MsgProto msg, ByteBuf out) throws Exception {
        out.writeInt(msg.getLength());
        out.writeBytes(msg.getContent());
    }
}

public class MsgProtoDecoder extends ByteToMessageDecoder {

    int length = 0;

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {


        System.out.println(in);

        //所以可读取的长度必须大于等于4,如果小于4,则说明可能是一个连接事件,没有数据可以读
        if (in.readableBytes() >= 4) {
            if (length == 0) {
                length = in.readInt();
            }

            if (in.readableBytes() < length) {
                System.out.println("数据读取不够,等待数据的读取...");
            }

            if (in.readableBytes() >= length) {
                byte[] data = new byte[length];
                //可读的长度必须要大于真实的数据长度才是符合要求
                in.readBytes(data);
                MsgProto msg = new MsgProto();
                msg.setLength(length);
                msg.setContent(data);
                out.add(msg);//传入下一个handler
            }
        }
        length = 0;


    }
}

客户端数据发送:
循环发送20次

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    System.out.println("开始向服务端发送数据");
    for(int i = 1;i<20;i++){
        User user = new User();
        user.setId(i);
        user.setName("bml" + i);
        user.setAddress("四川成都市");
        // ctx.writeAndFlush(user);
        MsgProto msg = new MsgProto();
        msg.setContent(user.toString().getBytes("UTF-8"));
        msg.setLength(user.toString().getBytes("UTF-8").length);
        ctx.writeAndFlush(msg);
    }

}

服务端 接收:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    System.out.println("接受到客户端发送过来的消息="+msg);
    MsgProto msgProto = (MsgProto) msg;
    System.out.println("接受的数据长度:"+msgProto.getLength());
    System.out.println("接受的数据内容:"+new String(msgProto.getContent(),"UTF-8"));
}

在这里插入图片描述
数据也是没有出现粘包和拆包的,所以这种方式是目前用的最多的,就是长度+内容,根据长度去读取内容

Netty心跳机制

所谓心跳, 即在 TCP 长连接中, 客户端和服务器之间定期发送的一种特殊的数据包, 通知对方自己还在线, 以确保 TCP 连接的有效性;TCP连接其实不存在长连接,长连接只是说客户端和服务端进行保持活动而已,而如果说偶一般的socket连接,如果没有主动关闭,那么这个连接对象会一直存在,但是如果说在真正的生产系统中,当客户端连接了服务端,如果长时间不发送数据,那么可能防火墙会给直接断开这个连接,如果说客户端连接了服务端,如果说客户端不发送数据,那么服务端会一直将这个连接对象放入缓存中,不会断开的,但是如果说这个时候客户端已经挂掉了,那么服务端是不知道的,如果没有心跳机制,那么服务端也不知道客户端是否还存活在,那么如果客户端已经断掉了,那么服务端保持的连接也没有任何作用了,所以这个时候就需要将这个连接对象释放以及时释放内存和连接,所以心跳机制是很有必要的,心跳机制无非就是说客户端建立了连接过后,不管通过发送小的数据包告诉服务端,我们需要保持连接,不要断开这个连接,但是如果说服务端长时间没有收到心跳包,那么服务端就可以知道客户端可能挂掉了,那么服务端就会及时释放这个连接;在Netty中也有心跳的机制,Netty作为一个高性能的通信框架,肯定是会考虑到这种TCP连接的心跳机制的,包括在zookeeper和dubbo底层使用了Netty作为通信框架,也是会有心跳机制来保活的,在Netty中心跳机制非常简单,还是一样的,在管道中添加心跳的处理器即可,在Netty中使用IdleStateHandler来处理心跳,我们来看下 它的构造器:

public IdleStateHandler(
        long readerIdleTime, long writerIdleTime, long allIdleTime,
        TimeUnit unit) {
    this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
}

这里解释下三个参数的含义:
readerIdleTimeSeconds: 读超时. 即当在指定的时间间隔内没有从 Channel 读取到数据时, 会触发一个 READER_IDLE 的
IdleStateEvent 事件.
writerIdleTimeSeconds: 写超时. 即当在指定的时间间隔内没有数据写入到 Channel 时, 会触发一个 WRITER_IDLE 的
IdleStateEvent 事件.
allIdleTimeSeconds: 读/写超时. 即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件.
注:这三个参数默认的时间单位是秒。若需要指定其他时间单位,可以使用另一个构造方法:

IdleStateHandler(boolean observeOutput, long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit)

要实现Netty服务端心跳检测机制需要在服务器端的ChannelInitializer中加入如下的代码:

ch.pipeline().addLast(new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS));

上面的参数意思就是对读数据进行心跳,指定在3s内没有从channel中读取数据会触发一次超时次数

IdleStateHandler源码分析

IdleStateHandler也是netty管道中的一个处理器,它的实现原理就是添加一个调度器,然后从时间间隔中如果发现没有数据发送过来,那么会触发读超时,然后调用channelIdle方法,然后传入到下一个handler中,它的实现非常简单,就是我们知道netty在读取数据完成的时候会调用channelReadComplete,然后调用channelReadComplete的时候会记录数据发送过来的时间,然后启动心跳检测的时候,通过当前时间减去最后一次数据读取的时间,然后计算出这个时间差,和之前设置的3s对比,如果超过了这个3s,然后记录超时,会调用channelIdle方法,将超时的信息传入到下一个handler进行处理,如果没有超时,那么又会循环去启动一个schedule开启一个新的心跳,至于超时几次会关闭连接,那么就有我们自己去设置。
io.netty.handler.timeout.IdleStateHandler#channelReadComplete
当客户端发送了数据过后,比如心跳包过后,会进入这个方法,当服务端数据读取完毕会记录一个最后一次读取数据的时间lastReadTime

public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    if ((readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) && reading) {
        lastReadTime = ticksInNanos();
        reading = false;
    }
    ctx.fireChannelReadComplete();
}

io.netty.handler.timeout.IdleStateHandler#channelActive
通道激活的时候会被调用,Netty这里通过通道激活的时候去启动心跳,也就是一个Schedule

public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
    // Initialize early if channel is active already.
    if (ctx.channel().isActive()) {
        initialize(ctx);
    }
    super.channelRegistered(ctx);
}
private void initialize(ChannelHandlerContext ctx) {
    // Avoid the case where destroy() is called before scheduling timeouts.
    // See: https://github.com/netty/netty/issues/143
    switch (state) {
    case 1:
    case 2:
        return;
    }

    state = 1;
    initOutputChanged(ctx);

    lastReadTime = lastWriteTime = ticksInNanos();
    //对读超时添加schedule
    if (readerIdleTimeNanos > 0) {
        readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
                readerIdleTimeNanos, TimeUnit.NANOSECONDS);
    }
    //对写超时添加schedule
    if (writerIdleTimeNanos > 0) {
        writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
                writerIdleTimeNanos, TimeUnit.NANOSECONDS);
    }
    //对读写超时添加schedule
    if (allIdleTimeNanos > 0) {
        allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
                allIdleTimeNanos, TimeUnit.NANOSECONDS);
    }
}
//添加一个定时器,在delay过后,执行一个线程,这个线程就是对心跳进行检测
ScheduledFuture<?> schedule(ChannelHandlerContext ctx, Runnable task, long delay, TimeUnit unit) {
    return ctx.executor().schedule(task, delay, unit);
}

所以对于读超时心跳就看ReaderIdleTimeoutTask这个线程类中的run方法,其他的写超时和读写超时都是一样的原理,这里就用读超时的心跳来分析
io.netty.handler.timeout.IdleStateHandler.ReaderIdleTimeoutTask#run

 @Override
    protected void run(ChannelHandlerContext ctx) {
        //这个readerIdleTimeNanos就是我们传入的3s转成的一个nanos(纳秒)
        long nextDelay = readerIdleTimeNanos;
        //reading在调用读取数据完成的那个方法过后会设置为false,表示数据读取完毕了,就是表示没有在读取数据
        if (!reading) {
            //这个表达式的意思就是nextDelay =nextDelay -(ticksInNanos() - lastReadTime),
            //简单的意思就是说延迟的时间(我们传入的3s)-(当前时间-最后一次读取数据的时间)得到的一个新的时间
            //差,如果这个时间差是小于0的,表示这次心跳检测为超时,就是说如果设置了3s超时,你发数据的时间都是3
            //s以后了,那么就表示超时了,就算超时了,这里还继续添加一个readerIdleTimeNanos后执行的一个schedule
            //因为就算这里超时了,但是偶尔有网络的波动很正常,不能因为一次超时就断开了,所以这里还是再次添加了一次
            //一个Schedule,继续检查,然后会本次超时会调用channelIdle这个方法,这个方法中会调用ctx.fireUserEventTriggered
            //这个fireUserEventTriggered的意思就是要调用下一个handler的UserEventTriggered,那么我们在
            //这个心跳的处理器后面添加了一个自己的处理器来实现这个UserEventTriggered方法,那么就可以收到
            //每一次心跳检测超时的事件,那么根据我们自己来设定超时几次来关闭客户端的这个连接,后续通过代码来实现
            nextDelay -= ticksInNanos() - lastReadTime;
        }

        if (nextDelay <= 0) {
            // Reader is idle - set a new timeout and notify the callback.
            //到这里表示超时了,再次启动一个schedule延迟(readerIdleTimeNanos=3s)过后执行
            readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);

            boolean first = firstReaderIdleEvent;
            firstReaderIdleEvent = false;

            try {
                //封装成一个IdleStateEvent事件对象,可以根据这个event得到具体的是读超时、写超时还是读写超时
                IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
                //调用超时,将超时的事件传入下一个handler去处理,下一个handler就要实现UserEventTriggered方法
                channelIdle(ctx, event);
            } catch (Throwable t) {
                ctx.fireExceptionCaught(t);
            }
        } else {
            // Read occurred before the timeout - set a new timeout with shorter delay.
            //如果没有超时,那么这里也启动一个schedule进行检测心跳,请注意,这里的延迟时间不在是3s,我们传入
            //的那个检测时间,举个例子:比如我们传入的是3s,那么如果没有超时,那么2s过后发过来了数据,那么
            //nextDelay=3-2=1s,所以下一次是就是1s过后就要去检测了,为什么呢?你想哈,最后一次心跳过来的是2s,
            //那么到这里是不是就意思说从上一次发心跳数据到现在为止已经过去了2s了,所以应该是只需要等待1s就要检测
            //这个心跳了,所以这里采用的是嵌套调用,计算延迟事件来达到心跳机制的检测
            readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
        }
    }
}

Netty的心跳检测机制主要的核心逻辑就上面的run方法,非常简单,不复杂,就是netty的这个心跳机制处理器就是主要登记每次心跳发过来的时候记录最后一次接受数据的时间,然后心跳机制根据这个时间来算时间差,判断是否有超时的情况,如果有超时的情况,那么丢给下一个handler去处理,下一个handler一般是我们的自定义的handler,其中会实现一个UserEventTriggered方法来处理超时,就由我们开发者去规定是否要断开连接。
心跳检测的例子
服务端的管道中添加这些handler,其中最重要的是IdleStateHandler和HeartNettyServerHandler,IdleStateHandler就是心跳检测的handler,而HeartNettyServerHandler就是来处理超时过后的一些处理

ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS));
ch.pipeline().addLast(new HeartNettyServerHandler());

userEventTriggered方法就是如果心跳检测超时了,就会调用这个handler的userEventTriggered方法进行处理,我这里是如果超时3次就关闭连接


public class HeartNettyServerHandler extends SimpleChannelInboundHandler<String> {

    private int readTimeCount = 0 ;
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {

        if(msg.equals("heart Packet")){
            System.out.println("recive client hart Packet is :"+msg);
            ctx.writeAndFlush("ok");
        }
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        IdleStateEvent event = (IdleStateEvent) evt;
        String msg = "";
        switch (event.state()){
            case ALL_IDLE:
                msg = "读写空闲";
                break;
            case READER_IDLE:
                msg = "读空闲";
                readTimeCount ++;
                break;
            case WRITER_IDLE:
                msg = "写空闲";
                break;
        }
        if(!msg.equals("")){
            System.out.println(msg);
        }
        if(readTimeCount > 3){
            System.out.println("server 端读取空闲超过3次,关闭连接,释放资源");
            ctx.writeAndFlush("idle close");
            ctx.channel().close();
        }
    }
}

客户端的handler:

public class HeartNettyClientHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {

        System.out.println("client recive data is:"+msg);
        if(msg != null && msg.equals("idle close")){
            System.out.println("服务端关闭了此连接,客户端也关闭");
            ctx.channel().closeFuture();
        }
    }

public class HeartNettyClient {


    public static void main(String[] args) {

        EventLoopGroup clientGoup = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(clientGoup)
                    .channel(NioSocketChannel.class)// 使用 NioSocketChannel 作为客户端的通道实现
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast(new StringEncoder());
                            ch.pipeline().addLast(new HeartNettyClientHandler());
                        }
                    });

            //启动客户端去连接服务器端
            ChannelFuture cf = bootstrap.connect("127.0.0.1", 8000).sync();
            Channel channel = cf.channel();
            String text = "heart Packet";
            while (channel.isActive()){
                //每次4s过后发,肯定要超时
              Thread.sleep(4000);
              channel.writeAndFlush(text);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            clientGoup.shutdownGracefully();
        }
    }


}

在这里插入图片描述
在这里插入图片描述

Netty断线自动重连实现

1、客户端启动连接服务端时,如果网络或服务端有问题,客户端连接失败,可以重连,重连的逻辑加在客户端。
参见代码com.tuling.netty.reconnect.NettyClient
2、系统运行过程中网络故障或服务端故障,导致客户端与服务端断开连接了也需要重连,可以在客户端处理数据的Handler的channelInactive方法中进行重连。
3、重连分为两种,一种是客户端启动过后连接不上服务端,自动重连,这种重连有可能是服务端没有启动,还有一种就是与服务端网络有故障;还有一种就是在运行过程中,客户端与服务端断开了,这种情况有可能是网络波动或者手动断开了,比如服务器在运行过程中由于一些原因断开了,如果服务端启动好了,那么客户端应该要自动冲洗连接上;基于这两种Netty中都可以实现,通过bootstrap添加一个监听器 ,监听连接情况,重连是在客户端进行的,这个要清楚,服务端是不会主动去连接客户端的,所以重连的情况也只存在于客户端中。
客户端的示例代码:


public class ReConnNettyClient {

    private static Bootstrap bootstrap;

    private String ip;
    private int port;

    public ReConnNettyClient(String ip, int port) {
        this.ip = ip;
        this.port = port;

    }

    private void init() {
        EventLoopGroup clientGoup = new NioEventLoopGroup();
        final ReConnNettyClient client = this;
        bootstrap = new Bootstrap();
        bootstrap.group(clientGoup)
                .channel(NioSocketChannel.class)// 使用 NioSocketChannel 作为客户端的通道实现
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new ReConnNettyClientHandler(client));
                    }
                });
        try {
            connec();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }


    public static void main(String[] args) {

        ReConnNettyClient client = new ReConnNettyClient("127.0.0.1", 8000);
        client.init();

    }

    public void connec() throws InterruptedException {

        ChannelFuture connect = bootstrap.connect("127.0.0.1", 8000);
        connect.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (!future.isSuccess()) {
                    future.channel().eventLoop().schedule(() -> {
                        System.err.println("服务器断开,自动重连");
                        try {
                            connec();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }, 3, TimeUnit.SECONDS);
                } else {
                    System.out.println("服务器连接成功");
                }
            }
        });
        connect.channel().closeFuture().sync();
    }


}

客户端处理,可以处理在运行中突然断开了,然后再次进行重连

public class ReConnNettyClientHandler extends ChannelInboundHandlerAdapter {

    private ReConnNettyClient nettyClient;

    public ReConnNettyClientHandler(ReConnNettyClient nettyClient) {
        this.nettyClient = nettyClient;
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.err.println("运行中断开,重新连接");
        ctx.channel().eventLoop().schedule(() -> {
            try {
                nettyClient.connec();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 3, TimeUnit.SECONDS);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello server", CharsetUtil.UTF_8));

    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("接受到服务端的消息:" + buf.toString(CharsetUtil.UTF_8));
    }
}

代码都在码云上:https://gitee.com/scjava/architect-project.git

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值