深入理解Netty解码器原理

975 阅读2分钟

1. 粘包和拆包发生的原因

1.1. 拆包的原因:

  1. 发送的数据大于TCP的发送缓冲区会进行第一次拆包。
  2. 发送的数据大于MSS(一般来说是1460个字节)进行第二次拆包。
  3. 大于IP以太网的MTU(最大传输单元),IP协议会进行数据的分片,进行第三次拆包。

1.2. 粘包的原因

  1. 开启nagle算法,data1,data2同时放到tcp的发送缓冲区。
  2. 应用处理数据不及时,data1,data2同时积压在tcp的接收方缓冲区。

2. 解决方法

  1. 基于分隔符的协议。
  2. 基于定长的协议。
  3. 基于变长的协议,将消息分为消息体和消息头。
  4. 自定义更复杂的协议。

3. Netty的提供的解决方式

一般来说我们都会定义自己的协议,Netty提供了ByteToMessageDecoder用于自定义消息的解码。

public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {
    // 累加器
    ByteBuf cumulation;
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof ByteBuf) {
            // 初始化一个集合用来存储解码后的数据
            CodecOutputList out = CodecOutputList.newInstance();
            try {
                first = cumulation == null;
                // 分配一个ByteBuf用于累加这个channel的可读数据
                cumulation = cumulator.cumulate(ctx.alloc(),
                        first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);
                // 解码        
                callDecode(ctx, cumulation, out);
            } catch (DecoderException e) {
                throw e;
            } catch (Exception e) {
                throw new DecoderException(e);
            } finally {
                if (cumulation != null && !cumulation.isReadable()) {
                    numReads = 0;
                    cumulation.release();
                    cumulation = null;
                } else if (++ numReads >= discardAfterReads) {
                    // We did enough reads already try to discard some bytes so we not risk to see a OOME.
                    // See https://github.com/netty/netty/issues/4275
                    numReads = 0;
                    discardSomeReadBytes();
                }

                int size = out.size();
                firedChannelRead |= out.insertSinceRecycled();
                fireChannelRead(ctx, out, size);
                out.recycle();
            }
        } else {
            ctx.fireChannelRead(msg);
        }
    }
    
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        numReads = 0;
        discardSomeReadBytes();
        if (!firedChannelRead && !ctx.channel().config().isAutoRead()) {
            ctx.read();
        }
        firedChannelRead = false;
        ctx.fireChannelReadComplete();
    }
    
    protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        try {
            while (in.isReadable()) {
                int outSize = out.size();
                // 判断集合中是不是已经存在解码后的数据
                if (outSize > 0) {
                    // 后面的handler处理数据
                    fireChannelRead(ctx, out, outSize);
                    out.clear();

                    // Check if this handler was removed before continuing with decoding.
                    // If it was removed, it is not safe to continue to operate on the buffer.
                    //
                    // See:
                    // - https://github.com/netty/netty/issues/4635
                    if (ctx.isRemoved()) {
                        break;
                    }
                    outSize = 0;
                }
                // 解码前记录下可读的字节数
                int oldInputLength = in.readableBytes();
                // 调用具体的子类进行解码
                decodeRemovalReentryProtection(ctx, in, out);

                // Check if this handler was removed before continuing the loop.
                // If it was removed, it is not safe to continue to operate on the buffer.
                //
                // See https://github.com/netty/netty/issues/1664
                if (ctx.isRemoved()) {
                    break;
                }
                // 解码后,如果没有解码到数据
                if (outSize == out.size()) {
                    // 没有读取数据
                    if (oldInputLength == in.readableBytes()) {
                        break;
                    } else {
                      // 读取了数据,但是没有解码到可用的bean, 可能是占包,继续进行解码。
                        continue;
                    }
                }
                // 没有读取任何数据,但是发生了解码,抛出一个异常
                if (oldInputLength == in.readableBytes()) {
                    throw new DecoderException(
                            StringUtil.simpleClassName(getClass()) +
                                    ".decode() did not read anything but decoded a message.");
                }

                if (isSingleDecode()) {
                    break;
                }
            }
        } catch (DecoderException e) {
            throw e;
        } catch (Exception cause) {
            throw new DecoderException(cause);
        }
    }
}

3.1. 优化Netty解码器

static void fireChannelRead(ChannelHandlerContext ctx, List<Object> msgs, int numElements) {
        if (msgs instanceof CodecOutputList) {
            fireChannelRead(ctx, (CodecOutputList) msgs, numElements);
        } else {
            for (int i = 0; i < numElements; i++) {
                ctx.fireChannelRead(msgs.get(i));
            }
        }
    }
// 可以优化这个方法,将集合穿给下个channleHandler,减少channleHandler的调用次数。
static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, int numElements) {
        for (int i = 0; i < numElements; i ++) {
            ctx.fireChannelRead(msgs.getUnsafe(i));
        }
    }    
    

4. 参考

  1. SOFABolt编解码机制解析