[02][07][01] Netty 简介

1. Netty 高性能特性

高性能的三个主题

  • IO 模型: 使用 BIO, NIO 或 AIO 的 IO 模型进行通信
  • 数据协议: 数据报文协议和序列化反序列化协议
  • 线程模型: Reactor 线程模型

1.1 线程模型

1.1.1 同步阻塞 IO(BIO)

同步阻塞 IO: 客户端向服务端发起一个数据读取请求, 客户端在收到服务端返回数据之前, 一直处于阻塞状态, 直到服务端返回数据后完成本次会话
在 BIO 模型中如果想实现异步操作, 就只能使用多线程模型, 也就是一个请求对应一个线程, 这样就能够避免服务端的链接被一个客户端占用导致连接数无法提高

同步阻塞 IO 主要体现在两个阻塞点

  • 服务端接收客户端连接时的阻塞
  • 客户端和服务端的 IO 通信时, 数据未就绪的情况下的阻塞

xx

1.1.2 非阻塞 IO (NIO)

非阻塞 IO: 客户端向服务端发起请求时, 不管服务端的数据是否准备就绪, 服务端都会直接返回, 如果服务端的数据未准备就绪时, 客户端会收到一个空的返回, 那客户端怎么拿到最终的数据呢?客户端只能通过轮询的方式来获得请求结果

NIO 相比 BIO 来说, 少了阻塞的过程, 在性能和连接数上都会有明显提高, 但是轮询过程中会有很多空轮询, 而空轮询会存在大量的系统调用 (发起内核指令从网卡缓冲区中加载数据, 用户空间到内核空间的切换), 随着连接数量的增加, 会导致性能问题

xx

1.1.3 多路复用机制

I/O 多路复用的本质是通过一种机制 (系统内核缓冲 I/O 数据), 让单个进程可以监视多个文件描述符, 一旦某个描述符就绪 (一般是读就绪或写就绪), 能够通知程序进行相应的读写操

什么是 fd: 在 linux 中, 内核把所有的外部设备都当成是一个文件来操作, 对一个文件的读写会调用内核提供的系统命令, 返回一个 fd(文件描述符). 而对于一个 socket 的读写也会有相应的文件描述符, 称为 socketfd

常见的 IO 多路复用方式有 select, poll, epoll, 都是 Linux API 提供的 IO 复用方式

  • select: 进程可以通过把一个或者多个 fd 传递给 select 系统调用, 进程会阻塞在 select 操作上, 这样 select 可以帮我们检测多个 fd 是否处于就绪状态, 这个模式有两个缺点
  • 由于同时监听多个文件描述符, 假如说有 1000 个, 如果其中一个 fd 处于就绪状态了, 那么当前进程需要线性轮询所有的 fd, 也就是监听的 fd 越多, 性能开销越大
  • select 在单个进程中能打开的 fd 数量是有限制的, 默认是 1024, 对于那些需要支持单机上万的 TCP 连接来说确实有点少
  • epoll: linux 还提供了 epoll 的系统调用, epoll 是基于事件驱动方式来代替顺序扫描, 性能相对来说更高, 主要原理是, 当被监听的 fd 中, 有 fd 就绪时, 会告知当前进程具体哪一个 fd 就绪, 当前进程只需要去从指定的 fd 上读取数据即可, epoll 所能支持的 fd 数量上线是操作系统的最大文件句柄, 这个数字要远远大于 1024

由于 epoll 能够通过事件告知应用进程哪个 fd 是可读的, 称为异步非阻塞 IO, 当然它是伪异步的, 因为它还需要去把数据从内核同步复制到用户空间中, 真正的异步非阻塞, 是数据已经复制到用户空间

I/O 多路复用的好处是可以通过把多个 I/O 的阻塞复用到同一个 select 的阻塞上, 从而使得系统在单线程的情况下可以同时处理多个客户端请求. 它的最大优势是系统开销小, 并且不需要创建新的进程或者线程, 降低了系统的资源开销

客户端请求到服务端后, 此时客户端在传输数据过程中, 为了避免服务端在读取客户端数据过程中阻塞, 服务端会把该请求注册到 Selector 复路器上, 服务端此时不需要等待, 只需要启动一个线程, 通过 selector.select() 阻塞轮询复路器上就绪的 channel 即可, 也就是说, 如果某个客户端连接数据传输完成, 那么 select() 方法会返回就绪的 channel, 然后执行相关的处理即可

xx

1.1.4 异步 IO

异步 IO 和多路复用机制, 最大的区别在于: 当数据准备就绪后, 系统会异步把数据从内核空间拷贝到用户空间, 应用程序可以直接使用该数据

xx

在 Java 中可以使用 NIO 的 api 来完成多路复用机制实现伪异步 IO, Netty 的 I/O 模型是基于非阻塞 IO 实现的, 底层依赖的是 JDK NIO 框架的多路复用器 Selector 来实现, 一个多路复用器 Selector 可以同时轮询多个 Channel, 采用 epoll 模式后, 只需要一个线程负责 Selector 的轮询, 就可以接入成千上万个客户端连接

1.2 零拷贝

Netty 的“零拷贝”主要体现在如下三个方面:

  • Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS, 使用堆外直接内存进行 Socket 读写, 不需要进行字节缓冲区的二次拷贝.如果使用传统的堆内存 (HEAP BUFFERS) 进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中, 然后才写入 Socket 中.相比于堆外直接内存, 消息在发送过程中多了一次缓冲区的内存拷贝

  • Netty 提供了组合 Buffer 对象, 可以聚合多个 ByteBuffer 对象, 用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作, 避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer

  • Netty 的文件传输采用了 transferTo() 方法, 它可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write() 方式导致的内存拷贝问题

打开 AbstractNioByteChannel$NioByteUnsafe

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;
    boolean close = false;
    try {
        do {
            byteBuf = allocHandle.allocate(allocator);
            allocHandle.lastBytesRead(doReadBytes(byteBuf));

            if (allocHandle.lastBytesRead()<= 0){
                // nothing was read. release the buffer
                byteBuf.release(); byteBuf = null;
                close = allocHandle.lastBytesRead()< 0;
                break;
            }

            allocHandle.incMessagesRead(1);
            readPending = false;
            pipeline.fireChannelRead(byteBuf);
            byteBuf = null;
        } while (allocHandle.continueReading());

        allocHandle.readComplete();
        pipeline.fireChannelReadComplete();

        if (close){
            closeOnRead(pipeline);
        }
    } catch (Throwable t){
        handleReadException(pipeline, byteBuf, t, close, allocHandle);
    } finally {
        if (!readPending && !config.isAutoRead()){
            removeReadOp();
        }
    }
}

再找到 do while 中的 allocHandle.allocate(allocator) 方法, 实际上调用的是 DefaultMaxMessageRecvByteBufAllocator$MaxMessageHandle 的 allocate 方法

public ByteBuf allocate(ByteBufAllocator alloc){
    return alloc.ioBuffer(guess());
}

相当于是每次循环读取一次消息, 就通过 ByteBufferAllocator 的 ioBuffer 方法获取 ByteBuf 对象, 下面继续看它的接口定义

public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> {
    ...
}

当进行 Socket IO 读写的时候, 为了避免从堆内存拷贝一份副本到直接内存,Netty 的 ByteBuf 分配器直接创建非堆内存避免缓冲区的二次拷贝, 通过“零拷贝”来提升读写性能

CompositeByteBuf 将多个 ByteBuf 封装成一个 ByteBuf, 对外提供统一封装后的 ByteBuf 接口, 它的类定义如下:

通过继承关系我们可以看出 CompositeByteBuf 实际就是个 ByteBuf 的包装器, 它将多个 ByteBuf 组合成一个集合, 然后对外提供统一的 ByteBuf 接口

添加 ByteBuf, 不需要做内存拷贝, 相关代码如下:

private int addComponent0(boolean increaseWriterIndex, int cIndex, ByteBuf buffer){
    assert buffer != null;
    boolean wasAdded = false;

    try {
        checkComponentIndex(cIndex);
        int readableBytes = buffer.readableBytes();

        // No need to consolidate - just add a component to the list
        @SuppressWarnings("deprecation")
        Component c = new Component(buffer.order(ByteOrder.BIG_ENDIAN).slice());

        if (cIndex == components.size()){
            wasAdded = components.add(c);
            if (cIndex == 0){
                c.endOffset = readableBytes;
            } else {
                Component prev = components.get(cIndex - 1);
                c.offset = prev.endOffset;
                c.endOffset = c.offset + readableBytes;
            }
        } else {
            components.add(cIndex, c);
            wasAdded = true;
            if (readableBytes != 0){
                updateComponentOffsets(cIndex);
            }
        }

        if (increaseWriterIndex){
            writerIndex(writerIndex()+ buffer.readableBytes());
        }

        return cIndex;
    } finally {
        if (!wasAdded){
            buffer.release();
        }
    }
}

文件传输的"零拷贝",Netty 文件传输 DefaultFileRegion 通过 transferTo() 方法将文件发送到目标 Channel 中

public long transferTo(WritableByteChannel target, long position)throws IOException {
    long count = this.count - position;

    if (count < 0 || position < 0){
        throw new IllegalArgumentException( "position out of range: " + position + " (expected: 0 - " + (this.count - 1)+ ')');
    }

    if (count == 0){
        return 0L;
    }

    if (refCnt()== 0){
        throw new IllegalReferenceCountException(0);
    }

    // Call open to make sure fc is initialized. This is a no-oop if we called it before
    open();

    long written = file.transferTo(this.position + position, count, target);

    if (written > 0){
        transferred += written;
    }

    return written;
}

对于很多操作系统它直接将文件缓冲区的内容发送到目标 Channel 中, 而不需要通过拷贝的方式, 这是一种更加高效的传输方式, 它实现了文件传输的“零拷贝”

1.3 数据协议

影响序列化性能的关键因素总结如下

  • 序列化后的码流大小 (网络带宽的占用)
  • 序列化&反序列化的性能 (CPU 资源占用)
  • 是否支持跨语言 (异构系统的对接和开发语言切换)

Netty 默认提供了对 Google Protobuf 的支持, 通过扩展 Netty 的编解码接口, 用户可以实现其它的高性能序列化框架, 例如 Thrift 的压缩二进制编解码框架.下面我们一起看下不同序列化&反序列化框架序列化后的字节数组对比

从上图可以看出,Protobuf 序列化后的码流只有 Java 序列化的 1/4 左右.正是由于 Java 原生序列化性能表现太差, 才催生出了各种高性能的开源序列化技术和框架 (性能差只是其中的一个原因, 还有跨语言,IDL 定义等其它因素)

1.4 Reactor 模型

Reactor 多路复用高性能 I/O 设计模式, Reactor 本质上就是基于 NIO 多路复用机制提出的一个高性能 IO 设计模式, 它的核心思想是把响应 IO 事件和业务处理进行分离, 通过一个或者多个线程来处理 IO 事件, 然后将就绪得到事件分发到业务处理 Handlers 线程去异步非阻塞处理

Reactor 模型有三个重要的组件

  • Acceptor: 处理客户端连接请求
  • Reactor: 将 I/O 事件发派给对应的 Handler
  • Handlers: 执行非阻塞读/写

xx

这是最基本的单 Reactor 单线程模型 (整体的 I/O 操作是由同一个线程完成的)

其中 Reactor 线程, 负责多路分离套接字, 有新连接到来触发 connect 事件之后, 交由 Acceptor 进行处理, 有 IO 读写事件之后交给 Hanlder 处理

Acceptor 主要任务就是构建 Handler, 在获取到和 client 相关的 SocketChannel 之后, 绑定到相应的 Hanlder 上, 对应的 SocketChannel 有读写事件之后, 基于 Racotor 分发, Hanlder 就可以处理了 (所有的 IO 事件都绑定到 Selector 上, 由 Reactor 分发)

Reactor 模式本质上指的是使用 I/O 多路复用 (I/O multiplexing) + 非阻塞 I/O(non-blocking I/O) 的模式

1.4.1 多线程单 Reactor 模型

单线程 Reactor 缺点, 由于 Handler 和 Reactor 在同一个线程中是串行的执行的, 如果其中一个 Handler 处理线程阻塞将导致其他的业务处理阻塞, 这也将导致新的无法接收新的请求

为了解决这种问题, 在业务处理的地方加入线程池异步处理, 将 Reactor 和 Handler 在不同的线程来执行

xx

1.4.2 多线程多 Reactor 模型

在多线程单 Reactor 模型中, 所有的 I/O 操作是由一个 Reactor 来完成, 而 Reactor 运行在单个线程中, 它需要处理包括 accept() / read() / write() / connect() 操作, 在高负载, 大并发或大数据量的应用场景时, 容易成为瓶颈, 主要原因如下

  • 一个 NIO 线程同时处理成百上千的链路, 性能上无法支撑, 即便 NIO 线程的 CPU 负荷达到 100%, 也无法满足海量消息的读取和发送
  • 当 NIO 线程负载过重之后, 处理速度将变慢, 这会导致大量客户端连接超时, 超时之后往往会进行重发, 这更加重了 NIO 线程的负载, 最终会导致大量消息积压和处理超时, 成为系统的性能瓶颈

多线程多 Reactor 模式, Main Reactor 负责接收客户端的连接请求, 然后把接收到的请求传递 SubReactor (SubReactor 可以有多个), 具体的业务 IO 处理由 SubReactor 完成

多 Reactor 模型通常也可以等同于 Master-Workers 模式, 比如 Nginx 和 Memcached 等就是采用这种多线程模型

xx

  • Acceptor: 请求接收者, 并不真正负责连接请求的建立, 而只将其请求委托 Main Reactor 线程池来实现, 起到一个转发的作用
  • Main Reactor: 主 Reactor 线程组, 主要负责连接事件, 并将 IO 读写请求转发到 SubReactor 线程池
  • Sub Reactor: Main Reactor 通常监听客户端连接后会将通道的读写转发到 Sub Reactor 线程池中一个线程 (负载均衡), 负责数据的读写. 在 NIO 中通常注册通道的读 (OP_READ), 写事件 (OP_WRITE)

1.5 内存池

随着 JVM 虚拟机和 JIT 即时编译技术的发展, 对象的分配和回收是个非常轻量级的工作.但是对于缓冲区 Buffer, 情况却稍有不同, 特别是对于堆外直接内存的分配和回收, 是一件耗时的操作.为了尽量重用缓冲区,Netty 提供了基于内存池的缓冲区重用机制.下面我们一起看下 Netty ByteBuf 的实现:

性能测试经验表明, 采用内存池的 ByteBuf 相比于朝生夕灭的 ByteBuf, 性能高 23 倍左右 (性能数据与使用场景强相关).下面我们一起简单分析下 Netty 内存池的内存分配:

public ByteBuf directBuffer(int initialCapacity, int maxCapacity){
    if (initialCapacity == 0 && maxCapacity == 0){
        return emptyBuf;
    }

    validate(initialCapacity, maxCapacity);
    return newDirectBuffer(initialCapacity, maxCapacity);
}

继续看 newDirectBuffer 方法, 我们发现它是一个抽象方法, 由 AbstractByteBufAllocator 的子类负责具体实现, 代码如下:

代码跳转到 PooledByteBufAllocator 的 newDirectBuffer 方法, 从 Cache 中获取内存区域 PoolArena, 调用它的 allocate 方法进行内存分配:

protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity){
    PoolThreadCache cache = threadCache.get();
    PoolArena<ByteBuffer> directArena = cache.directArena;
    ByteBuf buf;

    if (directArena != null){
        buf = directArena.allocate(cache, initialCapacity, maxCapacity);
    } else {
        if (PlatformDependent.hasUnsafe()){
            buf = UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
        } else {
            buf = new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
        }
    }

    return toLeakAwareBuffer(buf);
}

PoolArena 的 allocate 方法如下:

PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity){
    PooledByteBuf<T> buf = newByteBuf(maxCapacity);
    allocate(cache, buf, reqCapacity);
    return buf;
}

我们重点分析 newByteBuf 的实现, 它同样是个抽象方法, 由子类 DirectArena 和 HeapArena 来实现不同类型的缓冲区分配, 由于测试用例使用的是堆外内存

因此重点分析 DirectArena 的实现: 如果没有开启使用 sun 的 unsafe, 则

protected PooledByteBuf<ByteBuffer> newByteBuf(int maxCapacity){
    if (HAS_UNSAFE){
        return PooledUnsafeDirectByteBuf.newInstance(maxCapacity);
    } else {
        return PooledDirectByteBuf.newInstance(maxCapacity);
    }
}

执行 PooledDirectByteBuf 的 newInstance 方法, 代码如下:

static PooledDirectByteBuf newInstance(int maxCapacity){
    PooledDirectByteBuf buf = RECYCLER.get();
    buf.reuse(maxCapacity);
    return buf;
}

通过 RECYCLER 的 get 方法循环使用 ByteBuf 对象, 如果是非内存池实现, 则直接创建一个新的 ByteBuf 对象.从缓冲池中获取 ByteBuf 之后, 调用 AbstractReferenceCountedByteBuf 的 setRefCnt 方法设置引用计数器, 用于对象的引用计数和内存回收 (类似 JVM 垃圾回收机制)

1.6 无锁化的串行设计理念

在大多数场景下, 并行多线程处理可以提升系统的并发性能.但是, 如果对于共享资源的并发访问处理不当, 会带来严重的锁竞争, 这最终会导致性能的下降.为了尽可能的避免锁竞争带来的性能损耗, 可以通过串行化设计, 即消息的处理尽可能在同一个线程内完成, 期间不进行线程切换, 这样就避免了多线程竞争和同步锁
为了尽可能提升性能,Netty 采用了串行无锁化设计, 在 IO 线程内部进行串行操作, 避免多线程竞争导致的性能下降.表面上看, 串行化设计似乎 CPU 利用率不高, 并发程度不够.但是, 通过调整 NIO 线程池的线程参数, 可以同时启动多个串行化的线程并行运行, 这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优

Netty 的串行化设计工作原理图如下

Netty 的 NioEventLoop 读取到消息之后, 直接调用 ChannelPipeline 的 fireChannelRead(Object msg), 只要用户不主动切换线程, 一直会由 NioEventLoop 调用到用户的 Handler, 期间不进行线程切换, 这种串行化处理方式避免了多线程操作导致的锁的竞争, 从性能角度看是最优的

1.7 TCP 参数配置

合理设置 TCP 参数在某些场景下对于性能的提升可以起到显著的效果, 例如 SO_RCVBUF 和 SO_SNDBUF.如果设置不当, 对性能的影响是非常大的.下面我们总结下对性能影响比较大的几个配置项:

  • SO_RCVBUF 和 SO_SNDBUF: 通常建议值为 128K 或者 256K
  • SO_TCPNODELAY:NAGLE 算法通过将缓冲区内的小封包自动相连, 组成较大的封包, 阻止大量小封包的发送阻塞网络, 从而提高网络应用效率.但是对于时延敏感的应用场景需要关闭该优化算法
  • 软中断: 如果 Linux 内核版本支持 RPS(2.6.35 以上版本), 开启 RPS 后可以实现软中断, 提升网络吞吐量.RPS 根据数据包的源地址, 目的地址以及目的和源端口, 计算出一个 hash 值, 然后根据这个 hash 值来选择软中断运行的 cpu, 从上层来看, 也就是说将每个连接和 cpu 绑定, 并通过这个 hash 值, 来均衡软中断在多个 cpu 上, 提升网络并行处理性能

Netty 在启动辅助类中可以灵活的配置 TCP 参数, 满足不同的用户场景, 相关配置接口定义如下

2. Netty 使用

Netty 是一个高性能 NIO 框架, 所以它是基于 NIO 基础上的封装, 本质上是提供高性能网络 IO 通信的功能

Netty 提供了上述三种 Reactor 模型的支持, 我们可以通过 Netty 封装好的 API 来快速完成不同 Reactor 模型的开发, Netty 相比于 NIO 原生 API, 它有以下特点:

  • 提供了高效的 I/O 模型, 线程模型和时间处理机制
  • 提供了非常简单易用的 API, 相比 NIO 来说, 针对基础的 Channel, Selector, Sockets, Buffffers 等 api 提供了更高层次的封装, 屏蔽了 NIO 的复杂性
  • 对数据协议和序列化提供了很好的支持
  • 稳定性, Netty 修复了 JDK NIO 较多的问题, 比如 select 空转导致的 cpu 消耗 100%, TCP 断线重连, keep-alive 检测等问题
  • 可扩展性在同类型的框架中都是做的非常好的, 比如一个是可定制化的线程模型, 用户可以在启动参数中选择 Reactor 模型, 可扩展的事件驱动模型, 将业务和框架的关注点分离
  • 性能层面的优化, 作为网络通信框架, 需要处理大量的网络请求, 必然就面临网络对象需要创建和销毁的问题, 这种对 JVM 的 GC 来说不是很友好, 为了降低 JVM 垃圾回收的压力, 引入了两种优化机制
  • 对象池复用
  • 零拷贝技术

2.1 Netty 的生态介绍

Netty 生态中提供的功能

xx

2.2 Netty 的基本使用

2.2.1 创建 Netty Server 服务

大部分场景中使用的主从多线程 Reactor 模型, Boss 线程是主 Reactor, Worker 是从 Reactor. 他们分别使用不同的 NioEventLoopGroup, 主 Reactor 负责处理 Accept, 然后把 Channel 注册到从 Reactor, 从 Reactor 主要负责 Channel 生命周期内的所有 I/O 事件

public class NettyBasicServerExample {

    /**
     * 一个主从多reactor多线程模型的服务
     */
    public static void main(String[] args) {
        // 主Reactor专门用来接收连接,处理accept事件
        // 从Reactor 关注除了accept之外的其它事件,处理子任务
        // 主Reactor一般设置一个线程,设置多个也只会用到一个,而且多个目前没有应用场景
        // 从Reactor 线程通常要根据服务器调优,如果不写默认就是cpu的两倍
        // 主Reactor
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // 从Reactor
        EventLoopGroup workGroup = new NioEventLoopGroup(4);
        //构建Netty Server的API
        ServerBootstrap bootstrap = new ServerBootstrap();
        //Bootstrap
        bootstrap.group(bossGroup, workGroup)
                //指定epoll模型
                .channel(NioServerSocketChannel.class)
                // childHandler表示给worker那些线程配置了一个处理器,
                // 配置初始化channel,也就是给worker线程配置对应的handler,当收到客户端的请求时,分配给指定的handler处理
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        //心跳的hander
                        //编解码
                        //协议处理
                        //消息处理
                        ch.pipeline()
                                .addLast("h1", new NormalMessageHandler())
                                .addLast("h2", new ChannelInboundHandlerAdapter() {
                                    @Override
                                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                        System.out.println("收到消息------------第二处理器");
                                    }
                                }); //处理IO事件
                    }
                });
        try {
            // 由于默认情况下是NIO异步非阻塞,所以绑定端口后,通过sync()方法阻塞直到连接建立
            // 绑定端口并同步等待客户端连接(sync方法会阻塞,直到整个启动过程完成)
            ChannelFuture channelFuture = bootstrap.bind(8080).sync();
            System.out.println("Netty Server Started Success:listener port:8080");
            // 同步等到服务端监听端口关闭
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放资源
            workGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

2.2.2 设置 Channel 类型

NIO 模型是 Netty 中最成熟也是被广泛引用的模型, 在使用 Netty 的时候, 会采用 NioServerSocketChannel 作为 Channel 类型

bootstrap.channel(NioServerSocketChannel.class)

还提供其他 ServerSocketChannel 实现

  • EpollServerSocketChannel, epoll 模型只有在 linux kernel 2.6 以上才能支持, 在 windows 和 mac 都是不支持的, 如果设置 Epoll 在 window 环境下运行会报错
  • KQueueServerSocketChannel, kqueue 模型是 Unix 中比较高效的 IO 复用技术

2.2.3 NormalMessageHandler

ServerHandler 继承了 ChannelInboundHandlerAdapter, 这是 netty 中的一个事件处理器, netty 中的处理器分为 Inbound (进站) 和 Outbound (出站) 处理器

public class NormalMessageHandler extends ChannelInboundHandlerAdapter {
    // channelReadComplete方法表示消息读完了的处理,writeAndFlush方法表示写入并发送消息
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        // 这里的逻辑就是所有的消息读取完毕了,在统一写回到客户端。Unpooled.EMPTY_BUFFER表示空消息
        // addListener(ChannelFutureListener.CLOSE)表示写完后,就关闭连接
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }
    
    // exceptionCaught 方法就是发生异常的处理
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
    
    // channelRead 方法表示读到消息以后如何处理,这里我们把消息打印出来
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in=(ByteBuf) msg;
        byte[] req=new byte[in.readableBytes()];
        // 把数据读到byte数组中
        in.readBytes(req); 
        String body=new String(req,"UTF-8");
        System.out.println("服务器端收到消息:"+body);
        //写回数据
        ByteBuf resp=Unpooled.copiedBuffer(("receive message:"+body+"").getBytes());
        // ctx.write 表示把消息再发送回客户端,但是仅仅是写到缓冲区,没有发送,flush才会真正写到网络上去
        ctx.write(resp);
    }
}

2.3 Netty 的整体工作机制

Netty 的整体工作机制如下, 整体设计是多线程 Reactor 模型, 分离请求监听和请求处理, 通过多线程分别执行具体的 Handler

xx

2.3.1 网络通信层

网络通信层主要的职责是执行网络的 IO 操作, 它支持多种网络通信协议和 I/O 模型的链接操作. 当网络数据读取到内核缓冲区后, 会触发读写事件, 这些事件在分发给时间调度器来进行处理

在 Netty 中, 网络通信的核心组件以下三个组件

  • Bootstrap: 客户端启动 api, 用来链接远程 netty server, 只绑定一个 EventLoopGroup
  • ServerBootStrap: 服务端监听 api, 用来监听指定端口, 会绑定两个 EventLoopGroup, bootstrap 组件可以非常方便快捷的启动 Netty 应用程序
  • Channel: 是网络通信的载体, Netty 自己实现的 Channel 是以 JDK NIO channel 为基础, 提供了更高层次的抽象, 同时也屏蔽了底层 Socket 的复杂性, 为 Channel 提供了更加强大的功能

Channel 的常用实现实现类关系图, AbstractChannel 是整个 Channel 实现的基类, 派生出了 AbstractNioChannel, AbstractEpollChannel, AbstractKQueueChannel, 每个子类代表了不同的 I/O 模型和协议类型

xx

随着连接和数据的变化, Channel 也会存在多种状态, 比如连接建立, 连接注册, 连接读写, 连接销毁. 随着状态的变化, Channel 也会处于不同的生命周期, 每种状态会绑定一个相应的事件回调. 以下是常见的事件回调方法

  • channelRegistered, channel 创建后被注册到 EventLoop 上
  • channelUnregistered, channel 创建后未注册或者从 EventLoop 取消注册
  • channelActive, channel 处于就绪状态, 可以被读写
  • channelInactive, Channel 处于非就绪状态
  • channelRead, Channel 可以从源端读取数据
  • channelReadComplete, Channel 读取数据完成

Bootstrap 和 ServerBootStrap 分别负责客户端和服务端的启动, Channel 是网络通信的载体, 它提供了与底层 Socket 交互的能力, 而当 Channel 生命周期中的事件变化, 就需要触发进一步处理, 这个处理是由 Netty 的事件调度器来完成

2.3.2 事件调度器

事件调度器是通过 Reactor 线程模型对各类事件进行聚合处理, 通过 Selector 主循环线程集成多种事件 (I/O 时间, 信号时间), 当这些事件被触发后, 具体针对该事件的处理需要给到服务编排层中相关的 Handler 来处理
事件调度器核心组件:

  • EventLoopGroup. 相当于线程池
  • EventLoop. 相当于线程池中的线程

EventLoopGroup 本质上是一个线程池, 主要负责接收 I/O 请求, 并分配线程执行处理请求

xx

从图中可知

  • 一个 EventLoopGroup 可以包含多个 EventLoop, EventLoop 用来处理 Channel 生命周期内所有的 I/O 事件, 比如 accept, connect, read, write 等
  • EventLoop 同一时间会与一个线程绑定, 每个 EventLoop 负责处理多个 Channel
  • 每新建一个 Channel, EventLoopGroup 会选择一个 EventLoop 进行绑定, 该 Channel 在生命周期内可以对 EventLoop 进行多次绑定和解绑

下图表示的是 EventLoopGroup 的类关系图, 可以看出 Netty 提供了 EventLoopGroup 的多种实现, 如 NioEventLoop, EpollEventLoop, NioEventLoopGroup 等

从图中可以看到, EventLoop 是 EventLoopGroup 的子接口, 我们可以把 EventLoop 等价于 EventLoopGroup, 前提是 EventLoopGroup 中只包含一个 EventLoop

xx

EventLoopGroup 是 Netty 的核心处理引擎, 与 Reactor 线程模型有什么关系呢?

可以简单的把 EventLoopGroup 当成是 Netty 中 Reactor 线程模型的具体实现, 我们可以通过配置不同的 EventLoopGroup 使得 Netty 支持多种不同的 Reactor 模型

  • 单线程模型, EventLoopGroup 只包含一个 EventLoop, Boss 和 Worker 使用同一个 EventLoopGroup
  • 多线程模型: EventLoopGroup 包含多个 EventLoop, Boss 和 Worker 使用同一个 EventLoopGroup
  • 主从多线程模型: EventLoopGroup 包含多个 EventLoop, Boss 是主 Reactor, Worker 是从 Reactor 模型. 他们分别使用不同的 EventLoopGroup, 主 Reactor 负责新的网络连接 Channel 的创建 (也就是连接的事件), 主 Reactor 收到客户端的连接后, 交给从 Reactor 来处理

2.3.3 服务编排层

服务编排层的职责是负责组装各类的服务, 就是 I/O 事件触发后, 需要有一个 Handler 来处理, 所以服务编排层可以通过一个 Handler 处理链来实现网络事件的动态编排和有序的传播

它包含三个组件

  1. ChannelPipeline

采用双向链表将多个 ChannelHandler 链接在一起, 当 I/O 事件触发时, ChannelPipeline 会依次调用组装好的多个 ChannelHandler, 实现对 Channel 的数据处理. ChannelPipeline 是线程安全的, 因为每个新的 Channel 都会绑定一个新的 ChannelPipeline. 一个 ChannelPipeline 关联一个 EventLoop, 而一个 EventLoop 只会绑定一个线程, 下图表示 ChannelPIpeline 结构图

xx

从图中可以看出, ChannelPipeline 中包含入站 ChannelInBoundHandler 和出站 ChannelOutboundHandler, 前者是接收数据, 后者是写出数据, 其实就是 InputStream 和 OutputStream

xx

  1. ChannelHandler, 针对 IO 数据的处理器, 数据接收后, 通过指定的 Handler 进行处理
  2. ChannelHandlerContext

ChannelHandlerContext 用来保存 ChannelHandler 的上下文信息, 当事件被触发后, 多个 handler 之间的数据, 是通过 ChannelHandlerContext 来进行传递的. ChannelHandler 和 ChannelHandlerContext 之间的关系

每个 ChannelHandler 都对应一个自己的 ChannelHandlerContext, 它保留了 ChannelHandler 所需要的上下文信息, 多个 ChannelHandler 之间的数据传递, 是通过 ChannelHandlerContext 来实现的

xx

2.3.4 组件关系及原理总结

如图所示, 表示 Netty 中关键的组件协调原理, 具体的工作机制描述如下

  • 服务单启动初始化 Boss 和 Worker 线程组, Boss 线程组负责监听网络连接事件, 当有新的连接建立时, Boss 线程会把该连接 Channel 注册绑定到 Worker 线程
  • Worker 线程组会分配一个 EventLoop 负责处理该 Channel 的读写事件, 每个 EventLoop 相当于一个线程. 通过 Selector 进行事件循环监听
  • 当客户端发起 I/O 事件时, 服务端的 EventLoop 讲就绪的 Channel 分发给 Pipeline, 进行数据的处理
  • 数据传输到 ChannelPipeline 后, 从第一个 ChannelInBoundHandler 进行处理, 按照 pipeline 链逐个进行传递
  • 服务端处理完成后要把数据写回到客户端, 这个写回的数据会在 ChannelOutboundHandler 组成的链中传播, 最后到达客户端

xx

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值