Netty面试专题

时间:2024-03-26 16:33:46

文章目录

  • BIO 、NIO 和 AIO 的区别
  • NIO 的组成?
  • Netty 的特点?
  • Netty 的线程模型?
  • TCP 粘包/拆包的原因及解决方法?
  • 了解哪几种序列化协议?
  • 如何选择序列化协议?
  • Netty 的零拷贝实现?
  • Netty 的高性能表现在哪些方面?
  • NIOEventLoopGroup 源码?
    • 代码版
    • 文字版


BIO 、NIO 和 AIO 的区别

以下是关于BIO、NIO和AIO的区别:

特征 BIO NIO AIO
处理方式 一个连接一个线程,阻塞式IO 一个请求一个线程,事件驱动模型,非阻塞IO 一个有效请求一个线程,异步IO
线程利用率 较高
IO特性 面向流 面向缓冲区 面向缓冲区
阻塞/非阻塞 阻塞 非阻塞 非阻塞
IO类型 单向 双向 双向

在NIO中,采用了Reactor模式,事件分发器等待事件发生,然后将事件分发给事先注册的事件处理函数或回调函数,由它们来实际进行IO操作。

NIO的特点包括:

  • 事件驱动模型
  • 单线程处理多任务
  • 非阻塞IO
  • 基于事件的响应
  • IO多路复用,提高了可伸缩性和实用性
  • 基于缓冲区的IO

NIO相比于BIO和AIO,更适合处理大量连接并且对响应时间要求较高的情况。

NIO 的组成?

NIO(New I/O,也称为Non-blocking I/O)的核心组件包括:

  1. Buffer(缓冲区):Buffer是一个对象,它包含特定基本数据类型的元素,如int、char等,并提供了对其内容的访问和操作方法。与Channel进行交互时,数据是从Channel读入Buffer,或从Buffer写入Channel。

    • flip()方法:将Buffer从写模式切换到读模式,重置position为0,limit设置为之前position的值。
    • clear()方法:清空缓冲区,重置position为0,limit设置为capacity的值。
    • rewind()方法:重绕缓冲区,将position置为0,不改变limit,可重复读取之前写入的数据。
    • DirectByteBuffer:直接缓冲区,分配在堆外内存,减少了一次系统空间到用户空间的拷贝,但创建和销毁的成本较高,一般用于大型、持久的缓冲区。
  2. Channel(通道):Channel表示IO源与目标打开的连接,是双向的,但不能直接访问数据,只能与Buffer进行交互。常见的Channel有FileChannel、SocketChannel、ServerSocketChannel等。

    • FileChannel:用于文件IO操作。
    • SocketChannel:用于TCP网络通信的客户端。
    • ServerSocketChannel:用于TCP网络通信的服务端。
  3. Selector(选择器):Selector允许一个线程处理多个Channel,是NIO的核心组件之一。通过Selector,一个单独的线程可以管理多个Channel,监听这些Channel上的事件,当事件发生时,会通知注册在Selector上的Channel。

    • open()方法:打开一个Selector。
    • register()方法:向Selector注册Channel,并指定关注的事件类型,如读、写、连接、接收等。
    • select()方法:轮询已注册的Channel,获取已经就绪的事件。
    • Selector在Linux上的实现类是EPollSelectorImpl,委托给EPollArrayWrapper实现,使用epoll机制提高性能。
    • Selector的使用能够提高系统的资源利用率,降低线程数目,实现更高效的IO操作。
  4. Pipe(管道):Pipe是两个线程之间的单向数据连接,其中一个线程作为管道的源,向管道写数据,另一个线程作为管道的目标,从管道读取数据。

    • Pipe有两个通道,一个为sink通道,用于写入数据;另一个为source通道,用于读取数据。

以上是NIO的核心组件,它们共同构成了NIO编程模型,使得Java可以实现高性能、高并发的IO操作。

Netty 的特点?

Netty是一个高性能、异步事件驱动的NIO框架,具有以下特点:

  1. 异步事件驱动:Netty采用异步事件驱动模型,基于Reactor线程模型,使得IO操作非阻塞,提高了系统的并发能力和吞吐量。

  2. 多协议支持:Netty提供对TCP、UDP和文件传输等多种协议的支持,使得开发人员能够轻松地构建各种网络应用。

  3. 对NIO的优化:Netty在底层对NIO进行了优化,处理了epoll空轮询引起的CPU占用飙升等问题,简化了NIO的处理方式,提供更高效的Socket底层支持。

  4. 编解码器支持:Netty提供了丰富的编解码器支持,能够自动处理TCP粘包和分包问题,简化了数据的处理过程。

  5. 线程池支持:Netty可使用接受/处理线程池,提高了连接效率,同时对重连、心跳检测等功能提供了简单的支持。

  6. 配置灵活性:Netty允许配置IO线程数、TCP参数以及TCP接收和发送缓冲区的大小等参数,同时采用直接内存代替堆内存,通过内存池循环利用ByteBuf,提高了内存的利用率。

  7. 引用计数器管理:Netty使用引用计数器及时申请释放不再引用的对象,降低了GC频率,提高了系统的性能。

  8. 线程安全:Netty大量使用volatile、CAS和原子类,采用了线程安全的类和读写锁等机制,保证了系统的线程安全性。

综上所述,Netty具有高性能、灵活性和可扩展性的特点,适用于构建各种高性能的网络应用程序。

Netty 的线程模型?

Netty采用了主从多线程模型,其线程模型包括以下几个关键点:

  1. Boss线程池和Work线程池

    • Boss线程池负责处理连接请求的accept事件,即监听服务端口,接受客户端连接,并将对应的SocketChannel交给Work线程池处理。
    • Work线程池负责处理请求的read和write事件,即消息的读取、解码、编码和发送。
  2. 单线程模型

    • 在主从多线程模型中,Boss线程通常只有一个,负责监听服务端口,接受客户端连接,主要用于接入认证、握手等操作。
    • Work线程池中的NIO线程则负责网络IO的操作,包括消息的读取、解码、编码和发送。
  3. 主从多线程模型

    • 在主从多线程模型中,Boss线程负责接收客户端的连接请求,并将对应的SocketChannel交给Work线程池处理。
    • Work线程池中的NIO线程则负责具体的IO操作,包括消息的读取、解码、编码和发送。
  4. 任务分发

    • 主线程(Boss线程)负责接收连接,处理握手等操作。
    • 从线程池(Work线程池)负责具体的IO操作,包括读取、解码、编码和发送等。

通过这种主从多线程模型,Netty能够高效地处理大量的客户端连接,保证了网络IO的并发性和可靠性。

TCP 粘包/拆包的原因及解决方法?

TCP粘包/拆包问题是由于TCP协议是面向流的,数据在传输过程中被划分成一个个的数据包进行发送和接收,但是并不保证数据包的边界和完整性,从而导致接收端可能会出现多个数据包粘合在一起(粘包),或者一个数据包被拆分成多个数据包接收(拆包)的情况。

解决TCP粘包/拆包问题的常见方法包括:

  1. 消息定长:固定长度的消息,每个消息都是固定长度的,接收端按照固定长度读取数据即可,例如FixedLengthFrameDecoder类可以用来处理定长消息。

  2. 包尾增加特殊字符分割:在每个数据包的尾部增加特殊的分隔符作为包的结束标志,接收端根据分隔符来判断一个数据包的结束,例如使用LineBasedFrameDecoder类处理基于行的协议,或者DelimiterBasedFrameDecoder类处理自定义的分隔符协议。

  3. 将消息分为消息头和消息体:在消息中添加消息头,消息头中包含消息的长度信息,接收端先读取消息头中的长度信息,然后根据长度信息读取对应长度的消息体,例如使用LengthFieldBasedFrameDecoder类处理带有长度字段的消息。

以上方法可以根据具体的业务场景和需求选择合适的方式来解决TCP粘包/拆包问题,从而保证数据的正确性和完整性。

了解哪几种序列化协议?

有关序列化协议的介绍如下:

  1. XML

    • 优点:人机可读性好,可指定元素或特性的名称。
    • 缺点:序列化后数据量大,文件格式复杂,传输占带宽。
    • 适用场景:配置文件存储数据、实时数据转换。
  2. JSON

    • 优点:兼容性好、数据格式简单、易于读写、序列化后数据量小、解析速度快。
    • 缺点:描述性相对较差、不适合对性能要求极高的情况。
    • 适用场景:跨防火墙访问、可调试性要求高、Web应用的Ajax请求、传输数据量相对较小的服务。
  3. Fastjson

    • 优点:接口简单易用、速度快。
    • 缺点:偏离了标准和功能性、文档不全。
    • 适用场景:协议交互、Web输出、Android客户端。
  4. Thrift

    • 优点:序列化后体积小、速度快、支持多种语言、对数据字段的增删具有较强的兼容性。
    • 缺点:使用者相对较少、跨防火墙访问不安全、不具备可读性。
    • 适用场景:分布式系统的RPC解决方案。
  5. Avro

    • 优点:支持丰富的数据类型、自我描述属性、快速可压缩的二进制数据、跨编程语言实现。
    • 缺点:对于静态类型语言的用户不直观。
    • 适用场景:Hadoop中的持久化数据格式。
  6. Protobuf

    • 优点:序列化后码流小、性能高、可实现协议的前向兼容、易于管理和维护。
    • 缺点:需要依赖工具生成代码、支持语言相对较少。
    • 适用场景:对性能要求高的RPC调用、跨防火墙访问要求高、应用层对象的持久化。

其他一些序列化协议还包括:protostuff、JBoss Marshalling、MessagePack、Hessian、Kryo等,它们各自具有特定的优缺点和适用场景。

如何选择序列化协议?

选择序列化协议时需要考虑以下因素:

  1. 性能要求:对于性能要求较高的场景,如100ms以上的服务,需要选择性能较好的序列化协议,如Protobuf、Thrift或Avro。

  2. 数据传输载荷:如果传输的数据量较小,或对于移动端的网络通信,可以选择JSON等体积较小的协议。

  3. 跨语言支持:如果需要与其他语言进行交互,需要选择支持跨语言的序列化协议,如Protobuf、Thrift或Avro。

  4. 调试需求:在调试环境较恶劣的情况下,选择可读性较好的JSON或XML协议可以提高调试效率。

  5. 持久化需求:对于数据持久化的应用场景,如T级别的数据存储,可以考虑使用Protobuf或Avro。

  6. RPC解决方案:如果需要提供完整的RPC解决方案,可以选择Thrift作为序列化协议。

  7. 灵活性和易用性:根据项目的开发习惯和需求,选择合适的序列化协议。例如,如果项目主要使用静态类型语言,可以考虑使用Protobuf;如果需要简单易用的接口,可以选择JSON。

  8. 安全性:考虑数据传输过程中的安全性问题,如对数据进行加密处理。

综合考虑以上因素,可以根据具体的业务场景和需求选择合适的序列化协议。

Netty 的零拷贝实现?

Netty实现零拷贝主要通过以下几种方式:

  1. 使用堆外直接内存:Netty的接收和发送ByteBuffer采用直接内存(Direct Buffers),通过堆外内存进行Socket读写,避免了内存缓冲区的二次拷贝。这样一来,数据可以直接从应用程序的缓冲区发送到网络套接字,或者从网络套接字接收到应用程序的缓冲区,而无需在中间发生额外的数据拷贝。

  2. CompositeByteBuf:Netty的CompositeByteBuf类可以将多个ByteBuf合并为一个逻辑上的ByteBuf,避免了传统方式下通过内存拷贝将几个小Buffer合并成一个大的Buffer的问题。

  3. FileRegion:通过FileRegion包装的FileChannel.transferTo方法实现文件传输,直接将文件缓冲区的数据发送到目标Channel,避免了通过循环写入方式导致的内存拷贝问题。

  4. wrap方法:通过wrap方法,可以将byte[]数组、ByteBuf、ByteBuffer等包装成一个Netty ByteBuf对象,从而避免拷贝操作。

以上这些方式都能够有效地减少数据在内存中的拷贝次数,提高了数据的传输效率,实现了零拷贝的效果。

另外,关于Selector的BUG,Netty采取了一些解决办法。例如,对Selector的select操作周期进行统计,若连续发生多次空轮询,则触发了epoll死循环bug。此时,重建Selector并重新注册SocketChannel,从而解决了该问题。

Netty 的高性能表现在哪些方面?

Netty 的高性能主要表现在以下几个方面:

  1. 异步非阻塞IO:Netty基于NIO,采用异步非阻塞的IO模型。这种模型下,一个线程可以处理多个并发连接,不会因为某一个连接的IO操作阻塞而影响其他连接的处理,从而提高了系统的吞吐量和并发性能。

  2. 零拷贝:Netty通过使用直接内存和CompositeByteBuf等技术实现了零拷贝。这样可以避免数据在内存中的复制,提高了数据传输的效率。

  3. 高效并发编程:Netty在设计上遵循了高效并发编程的原则,通过串行化无锁化设计、使用volatile、CAS和原子类等技术,保证了线程安全性和并发性能。

  4. 优化的TCP参数配置:Netty允许用户对TCP参数进行灵活配置,如接收和发送缓冲区的大小、TCP_NODELAY参数等。这样可以根据具体的应用场景对网络通信进行优化,提高了传输效率和性能。

  5. 流量整型:Netty提供了流量整型的机制,可以防止上下游网络元素性能不均衡导致的问题,保护后端业务线程免受突发流量的冲击,从而提高了系统的稳定性和可靠性。

  6. 可靠性和安全性:Netty提供了链路有效性检测、内存保护机制、优雅停机等功能,保证了系统的稳定性和可靠性。同时,Netty还支持SSL、TLS等安全协议,保障了数据传输的安全性。

综上所述,Netty在异步非阻塞IO、零拷贝、高效并发编程、TCP参数配置、流量整型、可靠性和安全性等方面都表现出色,从而实现了高性能的网络通信。

NIOEventLoopGroup 源码?

代码版

以下是对 NioEventLoopGroup 类的部分源码分析:

public class NioEventLoopGroup extends MultithreadEventLoopGroup {
    public NioEventLoopGroup() {
        this(0, (Executor) null);
    }

    public NioEventLoopGroup(int nThreads) {
        this(nThreads, (Executor) null);
    }

    public NioEventLoopGroup(int nThreads, Executor executor) {
        this(nThreads, executor, SelectorProvider.provider());
    }

    public NioEventLoopGroup(int nThreads, Executor executor, SelectorProvider selectorProvider) {
        super(nThreads, executor, selectorProvider, NioSelectStrategyFactory.INSTANCE, RejectedExecutionHandlers.reject());
    }

    @Override
    protected EventLoop newChild(Executor executor, Object... args) throws Exception {
        return new NioEventLoop(this, executor, (SelectorProvider) args[0],
                ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
    }
}

NioEventLoopGroup 的构造函数中,会调用父类 MultithreadEventLoopGroup 的构造函数,并通过 newChild 方法创建 NioEventLoop 对象。NioEventLoopGroupMultithreadEventLoopGroup 的子类,用于处理 NIO 的事件循环。

public class NioEventLoop extends SingleThreadEventLoop {

    NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider, SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
        super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler, RejectedTaskTracker.NOOP);
        this.selector = selectorProvider.openSelector();
        this.unwrappedSelector = this.selector;
        this.selectStrategy = strategy;
    }

    @Override
    protected void run() {
        while (!this.isShuttingDown()) {
            try {
                switch (this.selectStrategy.calculateStrategy(this.selectNowSupplier, this.hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.BUSY_WAIT:
                        // fall-through
                    case SelectStrategy.SELECT:
                        this.select(this.wakenUp.getAndSet(false));
                        if (this.wakenUp.get()) {
                            this.selector.wakeup();
                        }
                    default:
                }

                this.cancelledKeys = 0;
                this.needsToSelectAgain = false;
                final int ioRatio = this.ioRatio;
                if (ioRatio == 100) {
                    try {
                        this.processSelectedKeys();
                    } finally {
                        this.runAllTasks();
                    }
                } else {
                    final long ioStartTime = System.nanoTime();
                    try {
                        this.processSelectedKeys();
                    } finally {
                        final long ioTime = System.nanoTime() - ioStartTime;
                        this.runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                    }
                }
            } catch (Throwable var11) {
                logger.warn("Unexpected exception in the selector loop.", var11);

                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException var10) {
                    // Do nothing
                }
            }
        }
    }
}

NioEventLooprun 方法中,通过循环处理事件循环的逻辑。首先根据选择策略确定是否需要执行选择操作,然后处理已选择的键集合,接着执行所有任务。在执行过程中,还会根据 I/O 比率来决定任务执行时间,以确保合理的时间分配。同时,处理可能出现的异常并进行相应的处理。

这是 NioEventLoopGroup 类和 NioEventLoop 类的部分源码,展示了它们的主要构造函数和事件循环逻辑。

文字版

NioEventLoopGroup(其实是 MultithreadEventExecutorGroup)内部维护一个类型为 EventExecutor children [], 默认大小是处理器核数 * 2, 这样就构成了一个线程池,初始化EventExecutor 时 NioEventLoopGroup 重载 newChild 方法,所以 children 元素的实际类型为 NioEventLoop。
线程启动时调用 SingleThreadEventExecutor 的构造方法,执行 NioEventLoop 类的 run 方法,首先会调用 hasTasks()方法判断当前 taskQueue 是否有元素。如果 taskQueue 中有元素,执行 selectNow()方法,最终执行 selector.selectNow(),该方法会立即返回。如果 taskQueue 没有元素,执行 select(oldWakenUp) 方法,select ( oldWakenUp) 方法解决了 Nio 中的 bug ,selectCnt 用来记录 selector.select 方法的 执行次数和标识是否执行过 selector.selectNow(),若触发了 epoll 的空轮询 bug,则会反复 执行 selector.select(timeoutMillis),变量 selectCnt 会逐渐变大,当 selectCnt 达到阈值(默 认 512),则执行 rebuildSelector 方法,进行 selector 重建,解决 cpu 占用 100%的 bug。
rebuildSelector 方法先通过 openSelector 方法创建一个新的 selector。然后将 old selector 的 selectionKey 执行 cancel。最后将 oldselector 的 channel 重新注册到新的 selector 中。 rebuild 后,需要重新执行方法 selectNow,检查是否有已 ready 的 selectionKey。

接下来调用 processSelectedKeys方法(处理 I/O 任务),当 selectedKeys != null 时,调用 processSelectedKeysOptimized 方法,迭代 selectedKeys获取就绪的 IO 事件的 selectkey 存 放在数组 selectedKeys 中,然后为每个事件都调用processSelectedKey来处理它, processSelectedKey 中分别处理 OP_READ;OP_WRITE;OP_CONNECT 事件。

最后调用 runAllTasks 方法(非 IO 任务),该方法首先会调用 fetchFromScheduledTaskQueue 方法,把 scheduledTaskQueue 中已经超过延迟执行时间的任务移到 taskQueue 中等待被执 行,然后依次从 taskQueue 中取任务执行,每执行 64 个任务,进行耗时检查,如果已执行 时间超过预先设定的执行时间,则停止执行非 IO 任务,避免非 IO 任务太多,影响 IO 任务 的执行。

每个 NioEventLoop 对应一个线程和一个 Selector ,NioServerSocketChannel 会主动注册到某 一个 NioEventLoop 的 Selector 上, NioEventLoop 负责事件轮询。

Outbound事件都是请求事件, 发起者是 Channel,处理者是 unsafe,通过 Outbound 事 件进行通知,传播方向是 tail 到 head 。Inbound事件发起者是 unsafe,事件的处理者是 Channel, 是通知事件,传播方向是从头到尾。

内存管理机制,首先会预申请一大块内存 Arena ,Arena 由许多 Chunk 组成,而每个 Chunk 默认由 2048 个 page 组成。 Chunk 通过 AVL 树的形式组织 Page,每个叶子节点表示一个Page,而中间节点表示内存区域,节点自己记录它在整个 Arena 中的偏移地址。当区域被 分配出去后,中间节点上的标记位会被标记,这样就表示这个中间节点以下的所有节点都 已被分配了。大于 8k 的内存分配在 poolChunkList 中,而 PoolSubpage 用于分配小于 8k 的 内存,它会把一个 page 分割成多段,进行内存分配。

ByteBuf 的特点:支持自动扩容(4M),保证 put 方法不会抛出异常、通过内置的复合缓冲 类型,实现零拷贝(zero-copy);不需要调用 flip()来切换读/写模式,读取和写入索引分开;方法链;引用计数基于 AtomicIntegerFieldUpdater 用于内存回收; PooledByteBuf 采用 二叉树来实现一个内存池,集中管理内存的分配和释放,不用每次使用都新建一个缓冲区 对象。 UnpooledHeapByteBuf 每次都会新建一个缓冲区对象。