Netty底层理解一 :(编/解码、心跳监听实现)

481 阅读7分钟

netty 编码机制

netty涉及的编码组件主要包含:channel、ChannelHandler、pipeline。

  • ChannelHandler

    • ChannelHandler担当了一个数据出站、入站的容器。可以通过实现ChannelInboundHandler接口或者ChannelOutboundHandler接口来实现的数据进站和出站,这些数据会被我们业务逻辑代码处理。当然我们可以可以实现ChannelInBoundHandler中的方法,来对我们的数据进行加工。业务逻辑通常写在一个或者多个ChannelInboundHandler中。 ChannelOutboundHandler原理一样,只不过它是用来处理出站数据的
  • ChannePipeline

    • ChannePipeline帮助ChannelHandler提供了责任链调用的容器。pipeline中提供了一个双向链表用来确定多个ChannelHandler之间的调用顺序。默认情况下Head是直接指向Tail节点的,在我们向pipeline(管道)中加入多个ChannelHandler时,那么底层会依照我们加入的顺序在管道中进行一个handler的排序。

    • 以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过pipeline中的一系列ChannelOutboundHandler(ChannelOutboundHandler 调用是从tail到head方向逐个调用每个handler的逻辑),并被这些Handler处理,反之则称为入站的,入站只调用pipeline里的 ChannelInboundHandler逻辑(ChannelInboundHandler调用是从head到tail方向逐个调用每个handler的逻辑)。

  • 编码解码器

    • StringDecoder 解码器,在进站时将字节码解析为string。
    • StringEncoder 编码器,在出站时将string解析为byte[]。
    • ObjectDecoder 对象解码器,解析对象时使用。解析为对象。
    • ObjectEncoder 对对象数据进行编码时使用。解析为byte[].

! 如果在pipeline同时加入了StringDecoder、StringEncoder,在出站和进站的操作中,不会受到加入管道的先后顺序影响。netty底层帮助我们去自动选择对应操作的handler。 不会同时出现又编码,又解码的无意义操作。

netty 的拆包 粘包

TCP是一个流协议,就是没有界限的一长串二进制数据。TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。面向流的通信是无消息保护边界的。 如下图所示,client发了两个数据包D1和D2,但是server端可能会收到如下几种情况的数据。

  • Netty提供了多个解码器,可以进行分包的操作,如下:

    • LineBasedFrameDecoder (回车换行分包)
    • DelimiterBasedFrameDecoder(特殊分隔符分包)
    • FixedLengthFrameDecoder(固定长度报文来分包)最常用
  • FixedLengthFrameDecoder方式需要将每次传输的数据的长度和对象本身都传过去,但是传递对象一把需要通过objectencoder进行编码传输,此时我们就可以自定义编码器,只需要extends MessageToByteEncoder对自己需要传送的对象进行编码。

  • 数据传送对象

    !在serverHandler和clientHandler内extends时,勿忘修改接收处理的泛型。

    public class MyMessageProtocol {
    
    //定义一次发送包体长度
    private int len;
    //一次发送包体内容
    private byte[] content;
    
    public int getLen() {
        return len;
    }
    
    public void setLen(int len) {
        this.len = len;
    }
    
    public byte[] getContent() {
        return content;
    }
    
    public void setContent(byte[] content) {
        this.content = content;
    }
    }
     
    
  • 自定义解码器:

  public class MyMessageDecoder extends ByteToMessageDecoder {

    int length = 0;

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        //需要将得到二进制字节码-> MyMessageProtocol 数据包(自定义传送对象)
        System.out.println(in);

        if(in.readableBytes() >= 4) {// 因为客户端传送的数据的length是一个int 占四个字节。
            if (length == 0){
                length = in.readInt(); //直接调用netty封装的方法获取数据传送的长度
            }
            if (in.readableBytes() < length) {
                System.out.println("当前可读数据不够,继续等待。。");
                return;
            }
            byte[] content = new byte[length];//读取到数据
            if (in.readableBytes() >= length){
                in.readBytes(content);

                //封装成MyMessageProtocol对象,传递到下一个handler业务处理
                MyMessageProtocol messageProtocol = new MyMessageProtocol();
                messageProtocol.setLen(length);
                messageProtocol.setContent(content);
                out.add(messageProtocol);//将数据写出到下个责任链handler
            }
            length = 0;//长度置0 准备处理下次数据传输
        }
    }
}
  • 自定义编码器:
    public class MyMessageEncoder extends MessageToByteEncoder<MyMessageProtocol> {
    @Override
    protected void encode(ChannelHandlerContext ctx, MyMessageProtocol msg, ByteBuf out) throws Exception {
        System.out.println("MyMessageEncoder encode 方法被调用");
        out.writeInt(msg.getLen());
        out.writeBytes(msg.getContent());
    }
}
  • serverHander数据处理
public class MyServerHandler extends SimpleChannelInboundHandler<MyMessageProtocol> {


    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MyMessageProtocol msg) throws Exception {
        System.out.println("====服务端接收到消息如下====");
        System.out.println("长度=" + msg.getLen());
        System.out.println("内容=" + new String(msg.getContent(), CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
  • clientHandler数据处理
public class MyClientHandler extends SimpleChannelInboundHandler<MyMessageProtocol> {

   	//发生连接时自动调用  或者可以在client处进行发送消息
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for(int i = 0; i< 10; i++) {
            String msg = "测试信息";
            //创建协议包对象
            MyMessageProtocol messageProtocol = new MyMessageProtocol();
            messageProtocol.setLen(msg.getBytes(CharsetUtil.UTF_8).length);
            messageProtocol.setContent(msg.getBytes(CharsetUtil.UTF_8));
            ctx.writeAndFlush(messageProtocol);
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MyMessageProtocol msg) throws Exception {

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

}

  • netty的心跳监测机制

个人理解:长连接和短连接,俩者之间没有绝对的区分当连接后长时间没有断开便可以称之为长连接,如果连接后 很短的时间内 就断开的话 那就称之为短连接。

  • 利用IdleStateHandler 的构造函数进行超时时间的设置。

    • 通过IdleStateHandler方法来进行心跳检测,构造函数设置监听的心跳时间间隔。

    • readerIdleTime:写超时时间。

    • writerIdleTime: 读超时时间。

    • allIdleTime: 既包含写超时时间也包含读超时时间。

  • 将我们设置传送的参数进行类内参数赋值,为下面心跳超时判断逻辑提供参数。

  • ChannelActive 是IdleStateHandler实现的最重要的类,当客户端和服务端建立连接之后,会执行以下俩个方法:

 public void channelActive(ChannelHandlerContext ctx) throws Exception {
        this.initialize(ctx);
        super.channelActive(ctx);
    }
  • this.initialize(ctx); 此方法主要是根据我们设置的不同构造函数参数来选择走不同的心跳检测方法,如下:
  private void initialize(ChannelHandlerContext ctx) {
        switch(this.state) {
        case 1:
        case 2:
            return;
        default:
            this.state = 1;
            this.initOutputChanged(ctx);
            this.lastReadTime = this.lastWriteTime = this.ticksInNanos();
            if (this.readerIdleTimeNanos > 0L) {
                this.readerIdleTimeout = this.schedule(ctx, new IdleStateHandler.ReaderIdleTimeoutTask(ctx), this.readerIdleTimeNanos, TimeUnit.NANOSECONDS);
            }

            if (this.writerIdleTimeNanos > 0L) {
                this.writerIdleTimeout = this.schedule(ctx, new IdleStateHandler.WriterIdleTimeoutTask(ctx), this.writerIdleTimeNanos, TimeUnit.NANOSECONDS);
            }

            if (this.allIdleTimeNanos > 0L) {
                this.allIdleTimeout = this.schedule(ctx, new IdleStateHandler.AllIdleTimeoutTask(ctx), this.allIdleTimeNanos, TimeUnit.NANOSECONDS);
            }

        }
    }

看到这里,我们可以根据出现的schedule得出netty的心跳检测机制是基于定时器的任务来执行。以我们设置的参数,逻辑会进入到第一个if条件内,传入的参数this.readerIdleTimeNanos 也是上面我们提到的构造函数内我们设置的readtimeout的时间,定时器传入我们设置的readtimeout参数,接下来又会怎么处理呢?

  • this.schedule
ScheduledFuture<?> schedule(ChannelHandlerContext ctx, Runnable task, long delay, TimeUnit unit) {
        return ctx.executor().schedule(task, delay, unit);
    }
  • 在this.schedule方法内我们可以看到里面是一个延迟的线程池执行策略,会根据设置的延迟时间去执行task。所以现在我们需要去反观上面传入的task内部执行的什么逻辑,也就是new IdleStateHandler.ReaderIdleTimeoutTask(ctx)内部的执行逻辑。

  • new IdleStateHandler.ReaderIdleTimeoutTask(ctx);

    private final class ReaderIdleTimeoutTask extends IdleStateHandler.AbstractIdleTask {
        ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
            super(ctx);
        }
    
  • 继承了IdleStateHandler.AbstractIdleTask 继续追踪到super(ctx)内

 private abstract static class AbstractIdleTask implements Runnable {
        private final ChannelHandlerContext ctx;

        AbstractIdleTask(ChannelHandlerContext ctx) {
            this.ctx = ctx;
        }

        public void run() {
            if (this.ctx.channel().isOpen()) {
                this.run(this.ctx);
            }
        }

        protected abstract void run(ChannelHandlerContext var1);
    }
  • 这里可以看到最终实现的是一个Runable接口,继续追踪到实现方法run内
        protected void run(ChannelHandlerContext ctx) {
            long nextDelay = IdleStateHandler.this.readerIdleTimeNanos;
            if (!IdleStateHandler.this.reading) {
                nextDelay -= IdleStateHandler.this.ticksInNanos() - IdleStateHandler.this.lastReadTime;
            }

            if (nextDelay <= 0L) {
                IdleStateHandler.this.readerIdleTimeout = IdleStateHandler.this.schedule(ctx, this, IdleStateHandler.this.readerIdleTimeNanos, TimeUnit.NANOSECONDS);
                boolean first = IdleStateHandler.this.firstReaderIdleEvent;
                IdleStateHandler.this.firstReaderIdleEvent = false;

                try {
                    IdleStateEvent event = IdleStateHandler.this.newIdleStateEvent(IdleState.READER_IDLE, first);
                    IdleStateHandler.this.channelIdle(ctx, event);
                } catch (Throwable var6) {
                    ctx.fireExceptionCaught(var6);
                }
            } else {
                IdleStateHandler.this.readerIdleTimeout = IdleStateHandler.this.schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
            }

        }
    }
  • 可以看到run方法内 我们先判断是否这个pipeline还存在数据读取,只有不存在数据读取的时候,才会进行执行代码。this.reading参数在channelRead和channelReadComplete内修改的:

  • lastReadTime

    • 在channelComplete内设置。lastReadTime 是在最后一次读取完数据,将lastReadTime设置为当前时间。
  • 超时逻辑

    • nextDelay -= IdleStateHandler.this.ticksInNanos() - IdleStateHandler.this.lastReadTime;

      • nextDelay: nextDelay的值是我们设置的最大超时时间,如果当前时间和最后一次读取时间的差值大于我们设置的最大超时时间的话,那么就需要断开资源链接。
      • 如果没有超时会走里面的else逻辑,进行下一次定时器任务。
    • 在超时后通过这俩行代码创建一个新的IdleStateEvent,传送到channelIdle内,调用我们自己定义的超时处理逻辑。

      IdleStateEvent event = IdleStateHandler.this.newIdleStateEvent(IdleState.READER_IDLE, first);
      IdleStateHandler.this.channelIdle(ctx, event);
    
    • 在channleIdle内我们可以看到如下代码
     protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
        ctx.fireUserEventTriggered(evt);
    }
    

    !在netty内只要调用到fire开头的方法代表着 会去调用下一个handler进行处理,也就是在这里我们会调用自己编写的超时处理逻辑代码,对超时连接的管理。

  • 超时连接后IdleStateHandler会调用下一个handler处理事件,我们需要在IdleStateHandler下一个handler--HeartBeatServerHandler(上文有图)内,实现userEventTriggered()来进行超时连接处理。

    public class HeartBeatServerHandler extends SimpleChannelInboundHandler<String> {
    
    int readIdleTimes = 0;
    
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
        System.out.println(" ====== > [server] message received : " + s);
        if ("Heartbeat Packet".equals(s)) {
            ctx.channel().writeAndFlush("ok");
        } else {
            System.out.println(" 其他信息处理 ... ");
        }
    }
    
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        IdleStateEvent event = (IdleStateEvent) evt;
    
        String eventType = null;
        switch (event.state()) {
            case READER_IDLE:
                eventType = "读空闲";
                readIdleTimes++; // 读空闲的计数加1
                break;
            case WRITER_IDLE:
                eventType = "写空闲";
                // 不处理
                break;
            case ALL_IDLE:
                eventType = "读写空闲";
                // 不处理
                break;
        }
    
        System.out.println(ctx.channel().remoteAddress() + "超时事件:" + eventType);
        if (readIdleTimes > 3) { //次数根据资源量和业务需求进行自主设置
            System.out.println(" [server]读空闲超过3次,关闭连接,释放更多资源");
            ctx.channel().writeAndFlush("idle close");
            ctx.channel().close();//断开连接  客户端可能存在网络波动 可以在断连后 继续连接
        }
    }
    
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.err.println("=== " + ctx.channel().remoteAddress() + " is active ===");
    }
    }
    

!event.state()是在上文中超时后 ,创建一个新的IdleStateEvent打上的状态。根据不同的 状态我们可以知道服务端和客户端之间连接的状态。

  • 未超时逻辑
    • 无其他处理逻辑 ,会在次调用schedule方法。