netty解码器分析和记录


目录
  • netty半包处理器分析
  • netty验证demo如下
    • 验证粘包和拆包
    • 验证客户端一个字节一个字节写
  • netty解码总结
  • netty Channel累积缓冲区cumulation
  • 为什么netty不适合传输文件
  • 为什么dubbo不适合传输问题件
  • github branch-netty分支。

    netty解码总结

    先确定获取都一个完整包(采用netty现有的或者继承ByteToMessageDecoder重写),然后触发pipeline进行解码,解码动作我们自己实现(只继承inbound适配器即可,不需要继承ByteToMessageDecoder )

    netty Channel累积缓冲区cumulation

    每个Channle都有个与之对应的Pipeline,如果pipeline有继承了ByteToMessageDecoder的解码器,那么此Channle就有个累积缓冲区cumulation,累积缓冲区是什么时候清空的呢? 最大允许多大呢?

    累积缓冲区cumulation在不可读的情况下,会被清空,不可读,说明报文是(多个)完整的且已经被解码,即报文都被解码后,累积缓冲区被清空。

    累加缓冲区cumulation最大可以多大呢?从ByteToMessageDecoder内看不出来。

    为什么netty不适合传输文件

    netty处理read事件: 由IO线程自旋,处理注册到Selector上的通道的事件。NioEventLoop#run -> NioEventLoop#processSelectedKey(java.nio.channels.SelectionKey, io.netty.channel.nio.AbstractNioChannel) -> io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read

    @Override
    public final void read() {
        //省略
        ByteBuf byteBuf = null;
        int messages = 0;
        boolean close = false;
        try {
            int totalReadAmount = 0;
            boolean readPendingReset = false;
            do {
                byteBuf = allocHandle.allocate(allocator);//创建缓冲区
                int writable = byteBuf.writableBytes();
                int localReadAmount = doReadBytes(byteBuf);//把socket数据写入到缓冲区
                //省略
                pipeline.fireChannelRead(byteBuf);//触发pipeline的channelRead
                byteBuf = null;
    
                //省略
            } while (++ messages < maxMessagesPerRead);//最大连续读取16次
    
            pipeline.fireChannelReadComplete();//触发pipeline的channelReadComplete
            //省略
        } catch (Throwable t) {
            handleReadException(pipeline, byteBuf, t, close);
        } finally {
            //省略
        }
    }
    

    原因有如下两处:

    1.由于EventLoop是串行执行,如果文件过大,那么在读取输入流的时候,在这里循环16次才结束,如果大多数channel都这样,严重会导致其他channel的read事件处理阻塞。

    2.每次读取后,触发pipeline的channelRead,把读取到的数据保存到累加缓冲区io.netty.handler.codec.ByteToMessageDecoder#cumulation,这样会导致累加缓冲区使用内存过大。

    为什么dubbo不适合传输问题件

    dubbo默认报文最大是8m,为什么这样设置,官方文档为什么不建议传输文件或者大报文呢?

    原因如下:

    1.dubbo采用的序列化,需要一次性把对象序列化,如果对象很大(包含文件),那么一次性序列化对象并加载到内存,会导致oom。

    2.dubbo默认使用的netty,且是单连接模型,一个客户端对一个服务端只有一个tcp连接,netty消息发送也是采用的串行无锁化,如果一个对象比较大,会导致这个channel一直在发送(write&flush),从而导致其他channel的发送排队阻塞(在taskQueue内排队无法及时处理),迟迟无法发送出去,最终会导致客户端其它的consumer等待超时。

    3.大对象也会导致dubbo服务端读取,从而导致阻塞其它channel的数据读取。 和2的原因一样,netty使用的串行无锁化,如果一个任务执行过程,就会到其它任务阻塞。

    既然单连接模型有这个缺点,为什么dubbo还要采用呢?因为省资源,tcp资源是宝贵的,且单连接可以满足rpc的场景;rpc的场景通常是服务端少,客户端很多,如果为每个客户端都创建tcp连接,会导致tcp连接数爆满,因为不知道具体有多少个客户端会调用服务端。

    dubbo也是可以使用多连接模型的,在客户端发送时,采用轮询机制从tcp连接池内选择一个tcp连接进行发送。但是通常不建议这样。

    因此:dubbo仅适合业务小报文传输(默认小于8m),不适合大报文和文件传输。

    为什么http适合传输文件

    http适合传输文件需要从客户端和服务端来说

    客户端

    使用httpclient等工具传输文件,比如要传输的文件在本地,每次只读取文件的一个 Buffer 大小,然后将这个 Buffer 的数据使用 Socket 发送即可;在这种方式下,同时存在于内存中的数据,只会有一个 Buffer 大小,不会有 Dubbo 那样将全部数据读取至内存的问题。

    服务端

    针对web服务容器是tomcat来说,Tomcat 在读取文件报文(form-data)时,会先将报文暂存至磁盘,然后通过 FileItem 读取磁盘中的报文内容。所以在对于 Server 端来说,不会一次性将完整的报文数据读取至内存中,也就不会有内存占用过高的问题。

    因此平时的上传文件,都是使用的客户端直连oss上传,不经过中间系统,这样也不会导致我们系统的连接阻塞、带宽占用等问题。