Netty源码(十二)入栈事件和解码

752 阅读16分钟

前言

本章学习Netty的入栈事件处理解码处理

  • ACCEPT入栈事件:创建Channel并注册到Selector

  • READ入栈事件:读取数据

  • 实现自定义协议:自定义编解码器

  • ByteToMessageDecoder:如何处理TCP粘包拆包

  • 解码异常的处理方式:Dubbo是如何处理解码异常的

一、回顾

1、ChannelPipeline

一个ChannelHandler封装为一个ChannelHandlerContext放入ChannelPipeline。

一个ChannelPipeline组装了ChannelHandlerContext形成的双向链表,头节点是HeadContext,尾节点是TailContext。

出栈事件从TailContext开始传播到HeadContext,入栈事件从HeadContext传播到TailContext。

Pipeline.png

2、出栈

出栈事件的传播方式,案例:服务端注册Channel后触发的read出栈事件(设置关注事件为ACCEPT)。

read.png

出栈事件是通过ChannelOutboundInvoker->ChannelOutboundHandler->Unsafe的方式传播的。

其中涉及的关键类如下:Channel(ChannelOutboundInvoker)->Pipeline(ChannelOutboundInvoker)->TailContext(ChannelOutboundInvoker)->ChannelOutboundHandlers->HeadContext(ChannelOutboundHandler) ->Unsafe。

二、ACCEPT

回顾NioEventLoop中processSelectedKey方法处理SelectionKey上激活的事件,如果有READ或ACCEPT事件发生,会调用Channel对应的Unsafe实现类处理。

private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
    final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
    // ...
    try {
        int readyOps = k.readyOps();
        // ...
        // READ或ACCEPT事件,调用unsafe的read方法
        if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
            unsafe.read();
        }
    } catch (CancelledKeyException ignored) {
        unsafe.close(unsafe.voidPromise());
    }
}

对于服务端NioServerSocketChannel来说,这里会关注ACCEPT事件,对应的Unsafe实现类NioMessageUnsafe。抽象父类AbstractNioMessageChannel.NioMessageUnsafe实现read方法(注意区别于入栈的read事件,入栈的read事件是设置关注事件为READ或ACCEPT)。

private final class NioMessageUnsafe extends AbstractNioUnsafe {
		// 保存NioServerSocketChannel
    private final List<Object> readBuf = new ArrayList<Object>();

    @Override
    public void read() {
        assert eventLoop().inEventLoop();
        // Unsafe作为Channel的内部类,可以拿到Channel里的很多东西
        final ChannelConfig config = config();
        final ChannelPipeline pipeline = pipeline();
        final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
        allocHandle.reset(config);
        boolean closed = false;
        Throwable exception = null;
        try {
            // 1. 接收JDK SocketChannel,封装为Netty NioSocketChannel放入readBuf
            try {
                do {
                    int localRead = doReadMessages(readBuf);
                    if (localRead == 0) {
                        break;
                    }
                    if (localRead < 0) {
                        closed = true;
                        break;
                    }
                    allocHandle.incMessagesRead(localRead);
                } while (allocHandle.continueReading());
            } catch (Throwable t) {
                exception = t;
            }
            // 2. 触发入栈事件 channelRead
            int size = readBuf.size();
            for (int i = 0; i < size; i ++) {
                readPending = false;
                pipeline.fireChannelRead(readBuf.get(i));
            }
            readBuf.clear();
            allocHandle.readComplete();
            // 3. 触发入栈事件 channelReadComplete
            pipeline.fireChannelReadComplete();
            // ...
        } finally {
            // ...
        }
    }
}

服务端ACCEPT事件处理分为三步:

  • doReadMessages:接收JDK SocketChannel,封装为Netty NioSocketChannel放入readBuf。
  • fireChannelRead:触发入栈事件channelRead,这里是重点。
  • fireChannelReadComplete:当处理完所有连接,触发入栈事件channelReadComplete。

NioServerSocketChannel接收SocketChannel,并封装为NioSocketChannel放入结果集。

@Override
protected int doReadMessages(List<Object> buf) throws Exception {
    SocketChannel ch = SocketUtils.accept(javaChannel());
  	if (ch != null) {
    	buf.add(new NioSocketChannel(this, ch));
    	return 1;
  	}
    return 0;
}

接着Unsafe通过Pipeline传播channelRead入栈事件。Pipeline会将ChannelRead事件,从HeadContext开始传播到TailContext。注意HeadContext即是ChannelInboundHandler可以处理入栈事件,同时又是ChannelInboundInvoker可以通过fire方法向后传播入栈事件

Accept.png

// DefaultChannelPipeline
@Override
public final ChannelPipeline fireChannelRead(Object msg) {
    // 注意传入静态方法的是headContext实例
    AbstractChannelHandlerContext.invokeChannelRead(head, msg);
    return this;
}
// AbstractChannelHandlerContext
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
  final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
  EventExecutor executor = next.executor();
  if (executor.inEventLoop()) {
    next.invokeChannelRead(m);
  } else {

  }
}
// AbstractChannelHandlerContext(HeadContext)获取对应Handler也是HeadContext
private void invokeChannelRead(Object msg) {
  // 这里的handler获取的也是HeadContext自己
  ((ChannelInboundHandler) handler()).channelRead(this, msg);
}
// HeadContext作为ChannelInboundHandler
// 同时又是ChannelInboundInvoker
// 继续传播channelRead给下一个节点
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
  ctx.fireChannelRead(msg);
}

这里Server端的NioServerSocketChannel在初始化时,通过ChannelInitializer加入了ServerBootstrap.ServerBootstrapAcceptor这个特殊的InboundHandler。这个Netty提供的Handler会将客户端NioSocketChannel注册到child EventLoopGroup。这里所有的childXXX都来源于我们的配置(回顾第九章的ServerBootStrap和第10章的Server端启动初始化Channel)。另外这里就终止了channelRead向后传播的过程,因为没有调用ctx.fireChannelRead方法继续向后传播

private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter {
    private final EventLoopGroup childGroup;
    private final ChannelHandler childHandler;
    private final Entry<ChannelOption<?>, Object>[] childOptions;
    private final Entry<AttributeKey<?>, Object>[] childAttrs;
    private final Runnable enableAutoReadTask;

    ServerBootstrapAcceptor(
            final Channel channel, EventLoopGroup childGroup, ChannelHandler childHandler,
            Entry<ChannelOption<?>, Object>[] childOptions, Entry<AttributeKey<?>, Object>[] childAttrs) {
        this.childGroup = childGroup;
        this.childHandler = childHandler;
        this.childOptions = childOptions;
        this.childAttrs = childAttrs;
    }

    // 服务端接收客户端Channel
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        final Channel child = (Channel) msg; // 一个强转,因为doReadMessages放入的是Channel实例
        child.pipeline().addLast(childHandler); // 往往是配置的ChannelInitializer
        setChannelOptions(child, childOptions, logger);
        setAttributes(child, childAttrs);
        childGroup.register(child).addListener(new ChannelFutureListener() {
          @Override
          public void operationComplete(ChannelFuture future) throws Exception {
            if (!future.isSuccess()) {
              forceClose(child, future.cause());
            }
          }
        });
    }
}

这里childGroup.register方法就不说了,与第十章注册Channel部分基本一致,无非是从EventLoopGroup中选择一个EventLoop与Channel绑定。

三、READ

对于NioSocketChannel,关注READ事件。当通道上发生READ事件说明有字节可读,会调用NioByteUnsafe的read方法(注意区别于入栈的read事件,入栈的read事件是设置关注事件为READ或ACCEPT)。

// 读取对端传来的字节
@Override
public final void read() {
  final ChannelConfig config = config();
  final ChannelPipeline pipeline = pipeline();
  final ByteBufAllocator allocator = config.getAllocator();
  final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
  allocHandle.reset(config);
  ByteBuf byteBuf = null;
  do {
    // Handle根据情况,创建合适容量的ByteBuf,来承载Channel中的数据,每次容量可能发生变化
    byteBuf = allocHandle.allocate(allocator);
    // 从底层JDKChannel读取字节到byteBuf,记录读取字节数
    allocHandle.lastBytesRead(doReadBytes(byteBuf));
    if (allocHandle.lastBytesRead() <= 0) {
      byteBuf.release();
      byteBuf = null;
      break;
    }
    // 累计循环次数
    allocHandle.incMessagesRead(1);
    readPending = false;
    // 触发channelRead
    pipeline.fireChannelRead(byteBuf);
    byteBuf = null;
    // 判断是否可以继续读取
  } while (allocHandle.continueReading());
  allocHandle.readComplete();
  // 触发ChannelReadComplete
  pipeline.fireChannelReadComplete();
  // ...
}

首先NioSocketChannel#doReadBytes方法将数据读取到ByteBuf中。

@Override
protected int doReadBytes(ByteBuf byteBuf) throws Exception {
    final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
    allocHandle.attemptedBytesRead(byteBuf.writableBytes());
    return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}

接着和ACCEPT事件一样,也是触发channelRead入栈事件,区别是这里传递的是ByteBuf而不是NioSocketChannel。一般来说,channelRead事件要经过一个解码器再进入业务Handler,而业务Handler一般是在业务线程池中执行,防止阻塞IO线程(WorkerGroup中的EventLoop)。

channelRead.png

另外,read方法循环处理对端发来的字节时,通过allocHandle控制循环次数,保证对于一个Channel不处理过多数据,防止因为某个客户端Channel阻塞其他客户端Channel的请求。

主要处理逻辑在DefaultMaxMessagesRecvByteBufAllocator.MaxMessageHandle抽象类中,它的实现类是AdaptiveRecvByteBufAllocator.HandleImpl。

// DefaultMaxMessagesRecvByteBufAllocator.MaxMessageHandle
// 判断read方法是否可以继续循环
// 入参=defaultMaybeMoreSupplier
@Override
public boolean continueReading(UncheckedBooleanSupplier maybeMoreDataSupplier) {
            // 默认true,是否自动设置关注read事件
    return config.isAutoRead() &&
           // respectMaybeMoreData = true
           // maybeMoreDataSupplier 根据当前循环的情况,判断是否可能有更多数据需要读取
           (!respectMaybeMoreData || maybeMoreDataSupplier.get()) && 
           // 循环次数 < 16
           totalMessages < maxMessagePerRead &&
      		 // 累计读字节数 > 0
           totalBytesRead > 0; 
}

// 判断是否可能有更多的字节需要读取
private final UncheckedBooleanSupplier defaultMaybeMoreSupplier = new UncheckedBooleanSupplier() {
  @Override
  public boolean get() {
    // 尝试读取的字节 是否等于 实际从channel读取到的字节
    // 如果等于表示可能还有新来的字节需要从channel读取
    return attemptedBytesRead == lastBytesRead;
  }
};

AdaptiveRecvByteBufAllocator.HandleImpl会根据实际读取的字节数,动态调整指标,控制下次allocate方法创建ByteBuf的容量。具体逻辑不展开,主要知道每次读取Channel字节数据多少,Netty做了动态控制。

  • 底层Channel读取完成,触发lastBytesRead方法
  • continueReading判断跳出循环,触发readComplete方法
  • 通道无数据可读时跳出循环,触发readComplete方法
private final class HandleImpl extends MaxMessageHandle {
    private final int minIndex;
    private final int maxIndex;
    private int index;
    private int nextReceiveBufferSize;
    private boolean decreaseNow;

    HandleImpl(int minIndex, int maxIndex, int initial) {
        this.minIndex = minIndex;
        this.maxIndex = maxIndex;

        index = getSizeTableIndex(initial);
        nextReceiveBufferSize = SIZE_TABLE[index];
    }
    // 猜测本次需要allocate多大的内存来承载Channel中的字节
  	@Override
    public int guess() {
        return nextReceiveBufferSize;
    }
		// 读取通道数据后触发,入参是Channel本次读取的字节数量
    @Override
    public void lastBytesRead(int bytes) {
        if (bytes == attemptedBytesRead()) {
            record(bytes);
        }
        super.lastBytesRead(bytes);
    }
  
    // 本次通道读取循环退出调用
    @Override
    public void readComplete() {
        record(totalBytesRead());
    }

    // 根据实际读取的字节数,动态调整指标,控制guess方法返回下次创建ByteBuf的容量
    private void record(int actualReadBytes) {
        if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) {
            if (decreaseNow) {
                index = max(index - INDEX_DECREMENT, minIndex);
                nextReceiveBufferSize = SIZE_TABLE[index];
                decreaseNow = false;
            } else {
                decreaseNow = true;
            }
        } else if (actualReadBytes >= nextReceiveBufferSize) {
            index = min(index + INDEX_INCREMENT, maxIndex);
            nextReceiveBufferSize = SIZE_TABLE[index];
            decreaseNow = false;
        }
    }
}

四、解码如何解决粘包拆包

READ入栈事件往往需要一个解码Handler,将ByteBuf转换为业务报文。这一章看看如何使用Netty提供的解码器框架,用户代码如何配合Netty的解码框架解决粘包拆包问题

1、如何使用ByteToMessageDecoder实现自己的解码器

公共类

首先,先定义一个协议。整个数据报分为header和body两部分,XHeader定义如下。 Xheader.png

定义两类报文类型:XRequest是请求报文,XResponse是响应报文。

BizBody.png XDecoder继承Netty的ByteToMessageDecoder,是一个InboundChannelHandler,负责处理自定义协议解码。

public class XDecoder extends ByteToMessageDecoder {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // 1. 如果字节不足头部大小,直接返回
        if (in.readableBytes() < XHeader.HEADER_SIZE) {
            System.out.println("receive part header, header size = " + in.readableBytes());
            return;
        }
        // 2. 头部解析
        int readerIndex = in.readerIndex(); // 先将读索引保存下来,之后可以恢复
        if (in.readShort() != XHeader.MAGIC) {
            System.out.println("MAGIC ERROR, close channel "  + ctx.channel().remoteAddress());
            ctx.channel().close();
            return;
//            throw new IllegalArgumentException("MAGIC ERROR");
        }
        if (in.readByte() != XHeader.VERSION_1) {
            System.out.println("VERSION ERROR, close channel "  + ctx.channel().remoteAddress());
            ctx.channel().close();
            return;
//            throw new IllegalArgumentException("VERSION ERROR");
        }
        byte type = in.readByte();
        int length = in.readInt();
        // 3. 剩余可读字节数 不足 头部中的长度标识,恢复读索引返回
        if (in.readableBytes() < length) {
            System.out.println("receive part data, length in header = " + length + ", data size = " + in.readableBytes());
            in.readerIndex(readerIndex);
            return;
        }
        // 4. 解析body
        byte[] data = new byte[length];
        in.readBytes(data);
        if (type == XHeader.REQUEST) {
            XRequest xRequest = objectMapper.readValue(data, XRequest.class);
            out.add(xRequest);
        } else if (type == XHeader.RESPONSE) {
            XResponse xResponse = objectMapper.readValue(data, XResponse.class);
            out.add(xResponse);
        }
        // 未知type会继续下一次报文解析
    }
}

服务端

XServer是服务端,Pipeline加入了通用解码器、Response编码器、Request业务处理器。

public class XServer {
    public static void main(String[] args) throws InterruptedException {
        ServerBootstrap bootstrap = new ServerBootstrap();
        EventLoopGroup boss = new NioEventLoopGroup(1);
        EventLoopGroup worker = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors());
        try {
            bootstrap.group(boss, worker)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler())
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline()
                                    .addLast("decoder", new XDecoder()) // 解码器
                                    .addLast("encoder", new XResponseEncoder()) // 编码器
                                    .addLast("xreq", new XRequestHandler()); // 业务处理器
                        }
                    });
            ChannelFuture future = bootstrap.bind(9999).sync();
            System.out.println("server start at port 9999");
            future.channel().closeFuture().sync();
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}
// Xresponse编码器
public class XResponseEncoder extends MessageToByteEncoder<XResponse> {
    private final ObjectMapper objectMapper = new ObjectMapper();
    @Override
    protected void encode(ChannelHandlerContext ctx, XResponse msg, ByteBuf out) throws Exception {
        out.writeShort(XHeader.MAGIC);
        out.writeByte(XHeader.VERSION_1);
        out.writeByte(XHeader.RESPONSE);
        byte[] bytes = objectMapper.writeValueAsBytes(msg);
        out.writeInt(bytes.length);
        out.writeBytes(bytes);
    }
}
// 业务handler 处理客户端请求,这里将客户端的data转换为大写返回
public class XRequestHandler extends SimpleChannelInboundHandler<XRequest> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, XRequest msg) throws Exception {
        XResponse response = new XResponse();
        response.code = "0";
        response.msg = "success";
        response.data = msg.data.toUpperCase();
        System.out.println(new ObjectMapper().writeValueAsString(msg));
        ctx.writeAndFlush(response);
    }
}

客户端

XClient是客户端,Pipeline加入了通用解码器、Request编码器、Response业务处理器

public class XClient {
    private static final int PORT = 9999;
    public static void main(String[] args) throws InterruptedException {
        ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);

        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup group = new NioEventLoopGroup(1);
        try {
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) throws Exception {
                            ch.pipeline()
                                    .addLast("decoder", new XDecoder()) // 解码器
                                    .addLast("encoder", new XRequestEncoder()) // 请求编码器
                                    .addLast("xres", new XResponseHandler()); // 响应处理器
                        }
                    });
            ChannelFuture future = bootstrap.connect("127.0.0.1", PORT).sync();
            assert future.isSuccess();
            System.out.println("connect success");
            // 1. 正常调用服务端
            writeSuccess(future.channel());
        } finally {
            group.shutdownGracefully();
        }
    }
    // 循环给服务端发送{"data": "hello"}
    private static void writeSuccess(Channel channel) throws InterruptedException {
        XRequest request = new XRequest();
        request.data = "hello";
        while (channel.isActive()) {
            channel.writeAndFlush(request);
            Thread.sleep(1000);
        }
    }
}
// XRequest编码器
public class XRequestEncoder extends MessageToByteEncoder<XRequest> {
    private final ObjectMapper objectMapper = new ObjectMapper();
    @Override
    protected void encode(ChannelHandlerContext ctx, XRequest msg, ByteBuf out) throws Exception {
        out.writeShort(XHeader.MAGIC);
        out.writeByte(XHeader.VERSION_1);
        out.writeByte(XHeader.REQUEST);
        byte[] bytes = objectMapper.writeValueAsBytes(msg);
        out.writeInt(bytes.length);
        out.writeBytes(bytes);
    }
}
// XResponse业务处理器
public class XResponseHandler extends SimpleChannelInboundHandler<XResponse> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, XResponse msg) throws Exception {
        System.out.println(new ObjectMapper().writeValueAsString(msg));
    }
}

2、ByteToMessageDecoder

ByteToMessageDecoder是Netty提供给用的抽象解码器实现类,用户只要实现抽象decode方法即可实现解码。ByteToMessageDecoder的优势就是协助用户解决了TCP粘包拆包问题,只不过需要用户小心地实现decode方法

先来看一下ByteToMessageDecoder的成员变量,可以看到其内部维护了一个累积ByteBuf,用于对同一个Channel的字节数据累积。

public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {
    // 累积ByteBuf
    ByteBuf cumulation;
    // 累加器
    private Cumulator cumulator = MERGE_CUMULATOR;
    // 每次ChannelRead是否仅仅处理一次ByteBuf(调用一次用户的decode方法)默认false
    private boolean singleDecode;
    // 是否是首次累加
    private boolean first;
}

当READ事件被触发后,NioSocketChannel会读取JDKChannel中的字节数据到一个ByteBuf中,这个ByteBuf就是ByteToMessageDecoder的入参msg。从整体上看ByteToMessageDecoder的channelRead方法分为几步:

  • Cumulator累积msg到cumulation
  • 调用用户的decode方法
  • finally资源释放,将decode完成的数据通过channelRead向后传播
// ByteToMessageDecoder
// 累积ByteBuf
ByteBuf cumulation;
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    if (msg instanceof ByteBuf) {
        // 认为是个ArrayList,只不过用到了对象池和FastThreadLocal技术
        CodecOutputList out = CodecOutputList.newInstance();
        try {
            // 累积msg中的可读字节到cumulation,这里会释放msg
            first = cumulation == null;
            cumulation = cumulator.cumulate(ctx.alloc(),
                    first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);
            // 调用用户的decode方法...
            callDecode(ctx, cumulation, out);
        } catch (DecoderException e) {
            throw e;
        } catch (Exception e) {
            throw new DecoderException(e);
        } finally {
            if (cumulation != null && !cumulation.isReadable()) {
              // 如果cumulation已经被读完了,释放并设置为空
              numReads = 0;
              cumulation.release();
              cumulation = null;
            } else if (++numReads >= discardAfterReads) {
              // 防止cumulation过度扩容膨胀
              // 当channelRead处理过16次以后,丢弃累加器中的已读字节
              numReads = 0;
              discardSomeReadBytes();
            }
            // 将解码成功的out列表里的报文向后传播
            int size = out.size();
            firedChannelRead |= out.insertSinceRecycled();
            fireChannelRead(ctx, out, size);
        }
    } else {
        ctx.fireChannelRead(msg);
    }
}

Cumulator

/**
 * Cumulate {@link ByteBuf}s.
 */
public interface Cumulator {
    /**
     * Cumulate the given {@link ByteBuf}s and return the {@link ByteBuf} that holds the cumulated bytes.
     * The implementation is responsible to correctly handle the life-cycle of the given {@link ByteBuf}s and so
     * call {@link ByteBuf#release()} if a {@link ByteBuf} is fully consumed.
     */
    ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in);
}

Cumulator是实现粘包拆包的一个关键接口,负责将输入buf累加到一个累积buf中。根据javadoc看到,如果输入buf读取完毕,应当在这里release释放资源。

ByteToMessageDecoder提供了两种实现,一种是基于内存拷贝的,一种是基于CompositeByteBuf的(Netty零拷贝其中之一)。

MERGE_CUMULATOR即为默认的累加器实现,基于内存拷贝的方式将in累计到cumulation中。

/**
 * Cumulate {@link ByteBuf}s by merge them into one {@link ByteBuf}'s, using memory copies.
 */
public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
    @Override
    public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
        // 累加buf没数据可读 且 in是一个非composite的bytebuf,直接使用in作为累加buffer
        if (!cumulation.isReadable() && in.isContiguous()) {
            cumulation.release();
            return in;
        }
        try {
            final int required = in.readableBytes();
            // 如果累加buf的可用空间不够了,执行扩容
            if (required > cumulation.maxWritableBytes() ||
                    (required > cumulation.maxFastWritableBytes() && cumulation.refCnt() > 1) ||
                    cumulation.isReadOnly()) {
                return expandCumulation(alloc, cumulation, in);
            }
            // 将in的可读数据,都写入累加buf
            cumulation.writeBytes(in, in.readerIndex(), required);
            in.readerIndex(in.writerIndex());
            return cumulation;
        } finally {
            // in写入累加器后,就可以释放了
            // (io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe.read创建的ByteBuf)
            in.release();
        }
    }
};

COMPOSITE_CUMULATOR是基于CompositeByteBuf的零拷贝累加器,根据javadoc描述,这种累加器的劣势在于用户实现decode逻辑较为复杂,且使用这种累加器实现的Decoder速度较慢

/**
 * Cumulate {@link ByteBuf}s by add them to a {@link CompositeByteBuf} and so do no memory copy whenever possible.
 * Be aware that {@link CompositeByteBuf} use a more complex indexing implementation so depending on your use-case
 * and the decoder implementation this may be slower then just use the {@link #MERGE_CUMULATOR}.
 */
public static final Cumulator COMPOSITE_CUMULATOR = new Cumulator() {
    @Override
    public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
        if (!cumulation.isReadable()) {
            cumulation.release();
            return in;
        }
        CompositeByteBuf composite = null;
        try {
            if (cumulation instanceof CompositeByteBuf && cumulation.refCnt() == 1) {
                composite = (CompositeByteBuf) cumulation;
                if (composite.writerIndex() != composite.capacity()) {
                    composite.capacity(composite.writerIndex());
                }
            } else {
                composite = alloc.compositeBuffer(Integer.MAX_VALUE).addFlattenedComponents(true, cumulation);
            }
            composite.addFlattenedComponents(true, in);
            in = null;
            return composite;
        } finally {
            if (in != null) {
                in.release();
                if (composite != null && composite != cumulation) {
                    composite.release();
                }
            }
        }
    }
};

decode

callDecode方法负责循环执行用户的decode方法,注意入参in是外部传入的累加bufout集合存放用户解码后的数据

/**
 * Called once data should be decoded from the given {@link ByteBuf}. This method will call
 * {@link #decode(ChannelHandlerContext, ByteBuf, List)} as long as decoding should take place.
 *
 * @param ctx           the {@link ChannelHandlerContext} which this {@link ByteToMessageDecoder} belongs to
 * @param in            the {@link ByteBuf} from which to read data
 * @param out           the {@link List} to which decoded messages should be added
 */
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    try {
        while (in.isReadable()) {
            int outSize = out.size();
            if (outSize > 0) {
                fireChannelRead(ctx, out, outSize);
                out.clear();
                // ...
                outSize = 0;
            }

            int oldInputLength = in.readableBytes();
            // 执行用户的decode方法
            // 这里源码是decodeRemovalReentryProtection方法
            // 简化展示为decode不影响主要逻辑
            decode(ctx, in, out);
            if (ctx.isRemoved()) {
                break;
            }
            // out列表没有新增元素
            if (outSize == out.size()) {
                if (oldInputLength == in.readableBytes()) {
                    // 可读字节数没有发生变化,中断本次解码
                    break;
                } else {
                    // 可读字节数发生变化,继续解码
                    continue;
                }
            }
            // out列表新增元素了,但是byteBuf的可读字节数没有变化(writeIndex - readIndex)
            if (oldInputLength == in.readableBytes()) {
                throw new DecoderException(...);
            }

            // out列表新增元素了,byteBuf的可读字节数也发生变化了,继续下一次循环

            // 是否仅读取一次就结束循环(默认false)
            if (isSingleDecode()) {
                break;
            }
        }
    } catch (DecoderException e) {
        throw e;
    } catch (Exception cause) {
        throw new DecoderException(cause);
    }
}

callDecode循环控制条件依赖于框架与用户的约定,循环是否中断取决于用户decode如何操作累加buf(in)和解码结果集(out)。

解码中断条件.png

根据上面的分析,用户的decode方法要如何解决粘包拆包问题呢?

解决粘包问题,需要让循环继续循环,保证累加Buffer可读字节发生变化且结果集大小变化,即解码累加buffer的一个数据包放入out结果集;当然自己在decode方法内部循环解决粘包问题也是可以的。

解决拆包问题,需要让循环退出,保证可读字节不变,结果集大小不变,让循环直接break,即如果用户decode按照自定义协议不能读取到完整报文,就恢复buffer读下标,直接返回。

例如XDecoder自定义协议的解码实现。

拆包问题:当读取字节不足时,保证读索引不变化,不操作结果集out直接返回即可,外部解码循环会中断。

粘包问题:每次decode只处理一个数据包,让Netty框架外部循环调用decode方法。

public class XDecoder extends ByteToMessageDecoder {
    private final ObjectMapper objectMapper = new ObjectMapper();
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // 1. 如果字节不足头部大小,直接返回
        if (in.readableBytes() < XHeader.HEADER_SIZE) {
            return;
        }
        // 2. 头部解析
        int readerIndex = in.readerIndex(); // 先将读索引保存下来,之后可以恢复
        if (in.readShort() != XHeader.MAGIC) {
          // ...
          // 抛出异常?关闭通道?恢复读索引返回?
        }
        if (in.readByte() != XHeader.VERSION_1) {
          // ...
        }
        byte type = in.readByte();
        int length = in.readInt();
        // 3. 剩余可读字节数 不足 头部中的长度标识,恢复读索引返回
        if (in.readableBytes() < length) {
            in.readerIndex(readerIndex);
            return;
        }
        // 4. 解析body
        // ...
    }
}

五、解码异常如何处理

自定义协议解码,如果对端传过来的数据报无法通过自定义协议的校验,应该如何处理?

int readerIndex = in.readerIndex();
if (in.readShort() != XHeader.MAGIC) {
  // ...
  // 抛出异常?关闭通道?恢复读索引返回?
}

方案一:恢复读索引返回?

恢复读索引返回,不操作out结果集,将非法报文将始终保存在累加buffer中。内存溢出?

方案二:抛出异常?

相当于读索引不恢复,跳过已读数据,退出本次解码循环,等待下一次channelRead事件。但是下一次读到的可能还是异常数据?

方案三:关闭通道?

貌似没什么风险,既然客户端的数据报是非法的。业务是否允许?

来看看Dubbo是如何处理的,客户端用XClient连接20880端口,直接发送ByteBuf;服务端用dubbo-2.7.9版本的demo。

package org.apache.dubbo.demo.provider;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Application {
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-provider.xml");
        context.start();
        System.in.read();
    }
}

Dubbo编码规则如下:

Dubbo编码规则.png

场景一:Dubbo发送魔数校验不通过的报文

客户端如下。

private static void writeDubboError(Channel channel) throws InterruptedException {
    final long start = System.currentTimeMillis();
    channel.closeFuture().addListener(new GenericFutureListener<Future<? super Void>>() {
        @Override
        public void operationComplete(Future<? super Void> future) throws Exception {
            System.out.println("dubbo channel关闭超时时间:" + (System.currentTimeMillis() - start) / 1000);
        }
    });
    // 循环发送0给dubbo
    while (channel.isActive()) {
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer().writeBytes(new byte[1]);
        channel.writeAndFlush(byteBuf);
        Thread.sleep(100);
    }
}

先看测试结果,Dubbo服务端的累加Buffer在持续增长

Dubbo1.png

客户端在持续发送了180s数据后,channel被关闭了。

Dubbo2.png

首先Dubbo会校验前两个字节的魔数。

// ExchangeCodec
@Override
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
    // 校验魔数0xdabb,如果魔数校验不通过走父类decode
    if (readable > 0 && header[0] != MAGIC_HIGH
            || readable > 1 && header[1] != MAGIC_LOW) {
        int length = header.length;
        // 将可读字节都读入header
        if (header.length < readable) {
            header = Bytes.copyOf(header, readable);
            buffer.readBytes(header, length, readable - length);
        }
        // 循环判断直到找到magic_high和magic_low
        for (int i = 1; i < header.length - 1; i++) {
            if (header[i] == MAGIC_HIGH && header[i + 1] == MAGIC_LOW) {
                buffer.readerIndex(buffer.readerIndex() - header.length + i);
                header = Bytes.copyOf(header, i);
                break;
            }
        }
        // 调用父类decode
        return super.decode(channel, buffer, readable, header);
    }
}

如果魔数校验不通过,走父类TelnetCodec判断能否解码,最终TelnetCodec判定没有收到回车符,返回DecodeResult.NEED_MORE_INPUT

// TelnetCodec
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] message) throws IOException {
    // ...省略其他
    byte[] enter = null;
    for (Object command : ENTER) {
        if (endsWith(message, (byte[]) command)) {
            enter = (byte[]) command;
            break;
        }
    }
    if (enter == null) {
        return DecodeResult.NEED_MORE_INPUT;
    }
}

最终,最外部的ByteToMessageDecoder实现类InternalDecoder,选择重置累加Buffer的读索引,等待下一次channelRead(方案一),这就是累加Buffer在持续增长的原因。同时看到Dubbo是在用户decode实现里循环解码,并不依赖ByteToMessageDecoder的循环解码。

private class InternalDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf input, List<Object> out) throws Exception {
			  // 封装input到Dubbo自己的ChannelBuffer中
        ChannelBuffer message = new NettyBackedChannelBuffer(input);
        // 封装NettyChannel
        NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);

        do {
            int saveReaderIndex = message.readerIndex();
            Object msg = codec.decode(channel, message);
            if (msg == Codec2.DecodeResult.NEED_MORE_INPUT) {
                // 重置读索引并退出
                message.readerIndex(saveReaderIndex);
                break;
            } else {
                // 加入结果集
                if (msg != null) {
                    out.add(msg);
                }
            }
        } while (message.readable());
    }
}

此外,客户端在发送了180s非法请求报文之后,channel连接就断开了。原因是Dubbo使用了Netty提供的IdleStateHandler,默认当客户端连接空闲超过180s后就会断开连接(注意Handler顺序,空闲检测Handler在解码器之前)。

NettyServer#doOpen方法,启动Netty服务端。

protected void doOpen() throws Throwable {
    bootstrap.group(bossGroup, workerGroup)
            // ...
            .childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                     // 空闲连接超时时长,默认180s
                    int idleTimeout = UrlUtils.getIdleTimeout(getUrl());
                     // 编解码器
                    NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
                    if (getUrl().getParameter(SSL_ENABLED_KEY, false)) {
                        ch.pipeline().addLast("negotiation",
                                SslHandlerInitializer.sslServerHandler(getUrl(), nettyServerHandler));
                    }
                    ch.pipeline()
                            .addLast("decoder", adapter.getDecoder())
                            .addLast("encoder", adapter.getEncoder())
                             // 180s无读写,发送IdleStateEvent
                            .addLast("server-idle-handler", new IdleStateHandler(0, 0, idleTimeout, MILLISECONDS))
                            .addLast("handler", nettyServerHandler);
                }
            });
	  //...
}

NettyServerHandler处理IdleStateEvent,关闭Channel

@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    if (evt instanceof IdleStateEvent) {
        NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
        channel.close();
        // ...
    }
}

场景二:Dubbo发送不存在的序列化方式

根据Dubbo解码规则,发送不存在的序列化方式(第3个字节的低5位)0给Dubbo服务端。

 private static void writeDubboError2(Channel channel) throws InterruptedException {
   final long start = System.currentTimeMillis();
   channel.closeFuture().addListener(new GenericFutureListener<Future<? super Void>>() {
     @Override
     public void operationComplete(Future<? super Void> future) throws Exception {
       System.out.println("dubbo channel关闭超时时间:" + (System.currentTimeMillis() - start) / 1000);
     }
   });
   String body = "hello";
   while (channel.isActive()) {
     ByteBuf buf = ByteBufAllocator.DEFAULT.buffer()
       .writeShort(MAGIC) // 2字节 魔数
       .writeByte(0) // 1字节 序列化方式/事件类型/请求类型/twoway...
       .writeByte(1) // 1字节 状态
       .writeLong(1L) // 8字节 id
       .writeInt(body.length()) // 4字节 body长度
       .writeBytes(body.getBytes()); // body
     channel.writeAndFlush(buf);
     Thread.sleep(1000);
   }
 }

Dubbo服务端异常日志,从日志中看到这里跳过了5个字节:

[10/04/21 11:35:36:480 CST] NettyServerWorker-5-1  WARN dubbo.DubboCodec:  [DUBBO] Decode response failed: Unrecognized serialize type from consumer: 0, dubbo version: , current host: 192.168.0.104
java.io.IOException: Unrecognized serialize type from consumer: 0
	at org.apache.dubbo.remoting.transport.CodecSupport.getSerialization(CodecSupport.java:89)
	at org.apache.dubbo.remoting.transport.CodecSupport.deserialize(CodecSupport.java:95)
	at org.apache.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody(DubboCodec.java:108)
	at org.apache.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:132)
	at org.apache.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:90)
	at org.apache.dubbo.rpc.protocol.dubbo.DubboCountCodec.decode(DubboCountCodec.java:48)
	at 
[10/04/21 11:35:36:481 CST] NettyServerWorker-5-1  WARN codec.ExchangeCodec:  [DUBBO] Skip input stream 5, dubbo version: , current host: 192.168.0.104

Dubbo将Buffer封装为InputStream解析报文,解码异常内部会catch住,在最外部的decode方法的finally块中跳过整段数据报

// ExchangeCodec
@Override
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
    // 校验魔数0xdabb,如果魔数校验不通过走父类decode
    if (readable > 0 && header[0] != MAGIC_HIGH
            || readable > 1 && header[1] != MAGIC_LOW) {
      // ...
    }
    // 可读字节不足16,收到半包,直接返回
    if (readable < HEADER_LENGTH) {
        return DecodeResult.NEED_MORE_INPUT;
    }

    // body长度不足,收到半包,直接返回
    int len = Bytes.bytes2int(header, 12);
    int tt = len + HEADER_LENGTH;
    if (readable < tt) {
        return DecodeResult.NEED_MORE_INPUT;
    }
    // buffer转换为InputStream,底层还是读写buffer,可读字节为len
    ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);
    try {
        return decodeBody(channel, is, header);
    } finally {
        // 如果decodeBody处理完了以后,还有可读字节,全部跳过
        if (is.available() > 0) {
            try {
                if (logger.isWarnEnabled()) {
                    logger.warn("Skip input stream " + is.available());
                }
                StreamUtils.skipUnusedStream(is);
            } catch (IOException e) {
                logger.warn(e.getMessage(), e);
            }
        }
    }
}

// DubboCodec
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
  byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
  // get request id.
  long id = Bytes.bytes2long(header, 4);
  if ((flag & FLAG_REQUEST) == 0) {
    // decode response.
    Response res = new Response(id);
    if ((flag & FLAG_EVENT) != 0) {
      res.setEvent(true);
    }
    // get status.
    byte status = header[3];
    res.setStatus(status);
    try {
      if (status == Response.OK) {
        // ...
      } else {
        // 走这里根据proto反序列化buffer中的数据,发生异常
        ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
        res.setErrorMessage(in.readUTF());
      }
    } catch (Throwable t) {
      if (log.isWarnEnabled()) {
        log.warn("Decode response failed: " + t.getMessage(), t);
      }
      res.setStatus(Response.CLIENT_ERROR);
      res.setErrorMessage(StringUtils.toString(t));
    }
    return res;
  } else {
    // decode request.
  }
}

这个场景与前一个不同点是,这里会返回一个异常的业务Response对象,最终会加入结果集,所以不会触发空闲连接检测,不会关闭channel

// DubboCountCodec
@Override
public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
    int save = buffer.readerIndex();
    MultiMessage result = MultiMessage.create();
    do {
        // 这里正常返回解析后的对象
        Object obj = codec.decode(channel, buffer);
        if (Codec2.DecodeResult.NEED_MORE_INPUT == obj) {
            buffer.readerIndex(save);
            break;
        } else {
            result.addMessage(obj);
            logMessageLength(obj, buffer.readerIndex() - save);
            save = buffer.readerIndex();
        }
    } while (true);
    if (result.isEmpty()) {
        return Codec2.DecodeResult.NEED_MORE_INPUT;
    }
    if (result.size() == 1) {
        return result.get(0);
    }
    return result;
}

总结

  • ACCEPT入栈事件:服务端NioServerSocketChannel关注ACCEPT事件,当通道中有accept事件触发时,会调用NioMessageUnsafe的read方法处理,接收NioSocketChannel。虽然处理的是SelectionKey上的accept事件,但是Unsafe会通过Pipeline触发channelRead。Netty的ServerBootstrap.ServerBootstrapAcceptor这个入栈处理器channelRead方法,将客户端NioSocketChannel注册并初始化。
  • READ入栈事件:NioSocketChannel关注READ事件,当通道中有READ事件触发时,会调用NioByteUnsafe从Channel中读取ByteBuf,并调用Pipeline触发channelRead。channelRead通常会经过解码Handler和业务Handler。
  • 实现自定义协议:通过继承ByteToMessageDecoder实现解码,继承MessageToByteEncoder实现编码。
  • ByteToMessageDecoder:Netty提供的解码抽象类,其中解码decode方法需要用户配合Netty框架实现。入参ByteBuf是一个会持续累加的buffer,入参out集合用于存储解码成功的业务对象。此外,decode方法通过控制累加buffer和out集合,可以解决粘包拆包问题。
  • Dubbo解码异常的处理方式
    • 魔数校验不通过,持续累积客户端的数据到累加Buffer中,直到180s触发空闲连接检测,关闭channel。
    • 序列化方式不存在,跳过整个数据报并将异常业务对象放入解码结果集。