Netty自适应缓冲区和Channel

817 阅读5分钟

Channel都是使用Buffer作为数据的载体

  • 对于网络编程来说,一个Socket连接就对应一个Channel,信息要从Channel中拿出放入Buffer,才能操作数据。
    if (key.isReadable()) {//是否有数据可读 
       client = (SocketChannel) key.channel(); 
       ByteBuffer readBuffer = ByteBuffer.allocate(1024); 
       int count = client.read(readBuffer);//数据读入Buffer
    }
    
    • Buffer的大小要提前指定,不然和stream有啥区别。
    • Buffer则提供了比Stream更高效和可预测的IO,Stream提供了一个能够容纳任意长度数据的假象,但会增加系统开销和频繁的上下文切换。那么Buffer就是有限容量的。
    • Buffer将系统开销暴露给看程序员。(直接设置大小)。
  • 那么对Netty来说,使用的时候显然没有指定Buffer的大小,那它底层必是下了一番功夫。

AdaptiveRecvByteBufAllocator

  • Netty 的 AdaptiveRecvByteBufAllocator 类是一个动态调整接收缓冲区大小的分配器。它根据之前读取的数据量来调整缓冲区大小,以便更有效地处理不同大小的数据包。以下是 AdaptiveRecvByteBufAllocator 类的源码(基于 Netty 4.1.68.Final 版本)和相关注释:

    public class AdaptiveRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufAllocator {
      //预期缓冲区大小从1024开始,不会低于64 ,也不会高于65536 。
      private final int minimum;
      private final int initial;
      private final int maximum;
      
      public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum) {
          if (minimum <= 0) {
              throw new IllegalArgumentException("minimum: " + minimum);
          }
          if (initial < minimum) {
              throw new IllegalArgumentException("initial: " + initial);
          }
          if (maximum < initial) {
              throw new IllegalArgumentException("maximum: " + maximum);
          }
    
          this.minimum = minimum;
          this.initial = initial;
          this.maximum = maximum;
      }
    
      @Override
      protected Handle newHandle() {
          return new HandleImpl(minimum, initial, maximum);
      }
    
      private static final class HandleImpl extends MaxMessageHandle {
          private final int minimum; // 最小值
          private final int initial;// 初始时
          private final int maximum;
          private int index;
          private int nextReceiveBufferSize;
          private boolean decreaseNow;
    
          HandleImpl(int minimum, int initial, int maximum) {
              this.minimum = minimum;
              this.initial = initial;
              this.maximum = maximum;
              nextReceiveBufferSize = initial;
          }
    
          @Override
          public ByteBuf allocate(ByteBufAllocator alloc) {
              return alloc.ioBuffer(guess());
          }
    
          @Override
          public int guess() {
              return nextReceiveBufferSize;
          }
    
          @Override
          protected void observe(int bytesRead) {
              // 根据读取的字节数调整缓冲区大小
              if (bytesRead <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) {
                  if (decreaseNow) {
                      index = max(index - INDEX_DECREMENT, 0);
                      nextReceiveBufferSize = SIZE_TABLE[index];
                      decreaseNow = false;
                  } else {
                      decreaseNow = true;
                  }
              } else if (bytesRead >= nextReceiveBufferSize) {
                  index = min(index + INDEX_INCREMENT, SIZE_TABLE.length - 1);
                  nextReceiveBufferSize = SIZE_TABLE[index];
                  decreaseNow = false;
              }
          }
      }
    }
    
  • AdaptiveRecvByteBufAllocator 类的工作原理如下:

    1. 构造函数接收三个参数:minimum(最小缓冲区大小)、initial(初始缓冲区大小)和 maximum(最大缓冲区大小)。这些参数用于限制动态调整的范围。

    2. newHandle() 方法创建一个新的 HandleImpl 实例,该实例包含了调整缓冲区大小的逻辑。

    3. HandleImpl 类中的 observe() 方法根据读取的字节数(bytesRead)来调整缓冲区大小。如果读取的字节数小于当前索引减去 INDEX_DECREMENT 对应的 SIZE_TABLE 值,那么缓冲区大小将减小。如果读取的字节数大于或等于当前缓冲区大小,那么缓冲区大小将增加。

    4. 缓冲区大小的调整是通过修改 index 变量来实现的。index 变量表示 SIZE_TABLE 数组的索引,SIZE_TABLE 数组包含了预定义的缓冲区大小值。通过增加或减小 index,可以在 SIZE_TABLE 中选择不同的缓冲区大小。

      image.png

    • 这种动态调整缓冲区大小的方法可以帮助 Netty 更有效地处理不同大小的数据包,从而提高性能。
  • 该类还会决定使用DirectBuffer还是HeapBuffer: image.png

Netty中的Channel

描述一下Netty中的Channel概念

  • Channel是Netty中的一个核心组件,它代表一个网络套接字或能够执行I/O操作(如读、写、连接和绑定)的组件。

  • Channel为用户提供以下信息和功能:

    1. Channel的当前状态(如:是否打开?是否已连接?)
    2. Channel的配置参数(如:接收缓冲区大小)
    // 获取一个Channel实例
    Channel channel = ...
    // 获取Channel的配置参数
    ChannelConfig config = channel.config();
    // 设置接收缓冲区大小为1024
    config.setRecvByteBufAllocator(new FixedRecvByteBufAllocator(1024));
    
    1. Channel支持的I/O操作(如:读、写、连接和(bind)绑定)
    2. ChannelPipeline 处理与Channel关联的所有 IO 事件和请求。
    public void initChannel(SocketChannel ch) throws Exception { 
       ChannelPipeline p = ch.pipeline(); 
       p.addLast(new MyServerHandler()); //  添加一个自定义的处理器到 ChannelPipeline 中,
                                         //  处理IO 事件和请求
    }
    
  • Netty中的所有I/O操作都是异步的,这意味着任何I/O调用都会立即返回,而不保证在调用结束时请求的I/O操作已经完成。相反,将会获得一个ChannelFuture实例,当请求的I/O操作成功、失败或取消时,它会通知您。

    Channel channel = ...; // 获取一个Channel实例
    
    // 创建一个写入数据的ByteBuf
    ByteBuf data = Unpooled.copiedBuffer("Hello, world!", Charset.defaultCharset());
    
    // 异步写入数据到Channel中,返回一个ChannelFuture实例
    ChannelFuture future = channel.writeAndFlush(data);//立即返回
    
    // 添加一个监听器来处理异步操作结果
    future.addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) {//完成后得到通知,回调该方法
            if (future.isSuccess()) {
                // 异步操作成功,处理逻辑...
                System.out.println("写入数据成功");
            } else {
                // 异步操作失败,处理逻辑...
                System.out.println("写入数据失败:" + future.cause().getMessage());
            }
        }
    });
    
    // 继续执行其他逻辑,不会被阻塞
    System.out.println("执行其他操作");
    
  • Channel具有层次结构,一个Channel可以根据创建方式具有父级。例如,由ServerSocketChannel接受的SocketChannel将返回ServerSocketChannel作为其父级。

    // 创建一个ServerSocketChannel实例
    ServerSocketChannel serverChannel = ServerSocketChannel.open();
    
    // 绑定端口并开始监听连接
    serverChannel.socket().bind(new InetSocketAddress(8080));
    serverChannel.configureBlocking(false);
    
    while (true) {
        // 接受连接并获取SocketChannel实例
        SocketChannel socketChannel = serverChannel.accept();//ServerChannel会生出SocketChannel
        if (socketChannel != null) {
            // 获取SocketChannel的父级ServerSocketChannel实例
            ServerSocketChannel parentChannel = (ServerSocketChannel) socketChannel
            .provider()
            .openServerSocketChannel()
            .provider()
            .retrieveSocketChannel(serverChannel.socket());
          System.out.println("SocketChannel's parent channel: " + parentChannel);
          // ...其他操作
      }
      // ...其他操作
    }
    
  • 某些传输可能会暴露特定于传输的附加操作。可以将Channel向下转型为子类型以调用此类操作。例如,在旧的I/O数据报传输中,多播加入/离开操作由DatagramChannel提供。

    // 假设已经获取了一个Channel实例
    Channel channel = ...;
    
    // 将Channel向下转型为(UDP)DatagramChannel,以便进行多播加入操作
    if (channel instanceof DatagramChannel) {
        DatagramChannel datagramChannel = (DatagramChannel) channel;
        InetAddress groupAddress = InetAddress.getByName("224.0.0.1"); // 多播组地址
        NetworkInterface ni = NetworkInterface.getByName("eth0"); // 网络接口名
        datagramChannel.joinGroup(groupAddress, ni).sync(); // 加入多播组,并等待操作完成
    } else {
        // 如果不是DatagramChannel,则忽略多播加入操作
        System.out.println("该Channel不支持多播加入操作");
    }
    
    • TCP传输可能不支持多播操作,但UDP传输可能支持。因此,如果您要使用特定于传输的操作,您需要将Channel向下转换为其实际类型(如DatagramChannel)以调用这些操作。(不是直接将TCP转成了UDP)
  • 在完成Channel操作后,务必调用close()或close(ChannelPromise)以释放所有资源。这确保以适当的方式释放所有资源,例如文件句柄。