解读Netty之接收缓冲区
来源:互联网 发布:json 对日期格式化 编辑:程序博客网 时间:2024/06/10 01:40
概述
用过netty,大家都知道在请求处理之前,会有一个缓冲区用于接受数据,不同场景对缓冲的大小都不太一样。
比如UDP协议的DatagramChannel,默认缓冲区大小只给了2048,而假如开发一个SyslogUdp的协议服务,大小其实就不止这么点。
因此,缓冲区怎么用,怎么设置就非常关键啦,很不小心就会踩坑,本文主要给大家讲解下netty4下接受缓冲区的原理及对源码进行解读…
结构分解
Netty的接收缓冲区,是由接口类RecvByteBufAllocator实现而来,该类有个子接口Handle,包含了三个方法:
interface Handle { /** * 创建一个合适大小的接收缓冲区(大到足够读取所有inbound数据,小到不会存在数据浪费) */ ByteBuf allocate(ByteBufAllocator alloc); /** * 猜测这个缓冲区的容量,之所有是猜测,是因为存在动态扩容的一种缓冲区 */ int guess(); /** * 记录上一次读操作实际读取的字节数,以便接收缓冲区能动态调整一个合适的容量 * * @param actualReadBytes the actual number of read bytes in the previous read operation */ void record(int actualReadBytes); }
了解完基类,应该可以知道设计者的意图,提供多种灵活方式来创建接收缓冲区,比如固定空间大小的,空间动态扩容缩的等…
这个有什么好处呢,对比起JDK原生的NIO类库使用的java.nio.ByteBuffer,实际是一个固定长度的byte数组,这也说明原生buffer无法动态扩容,相关代码如下:
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>{ // These fields are declared here rather than in Heap-X-Buffer in order to // reduce the number of virtual method invocations needed to access these // values, which is especially costly when coding small buffers. // final byte[] hb; // Non-null only for heap buffers final int offset; boolean isReadOnly; // Valid only for heap buffers
讲好处前先踩踩原生ByteBuffer固定空间的坏处,例如开发人员一开始很难预测到每条消息报文的长度,或者消息堆积空间所需大小,当然你可以说干脆直接分配一个比较大的ByteBuffer,这通常没问题,不过对于海量推送、高并发等场景,这会给服务器带来沉重的内存负担,也算是一种资源浪费啊。
举个详细点例子:例如海量推送服务,单条消息若最大上限是16K,消息平均大小是6K,为了满足消息支持16K的处理,我们需要把buffer设置成16K,这样子的话,因是海量链路推送,那么假如并发链接数为100w,每个链路都有独立的ByteBuffer接收缓冲区,那将会额外损耗的总内存为:100,0000 * (16K-6K) = 9764M。是不是吓一跳,竟然快要消耗一个G的内存了,大内存不仅增加硬件成本,而且会导致长时间的FGC,对系统的维护和稳定保障带来非常大的冲击。
那讲完坏处,相比好处就显而易见啦,能灵活调整内存空间大小,是一件多么棒的事情。实际上RecvByteBufAllocator提供了两种实现,分别是:AdaptiveRecvByteBufAllocator和FixedRecvByteBufAllocator
我们先来看看简单的FixedRecvByteBufAllocator,代码逻辑如下:
public class FixedRecvByteBufAllocator implements RecvByteBufAllocator { private static final class HandleImpl implements Handle { private final int bufferSize; HandleImpl(int bufferSize) { this.bufferSize = bufferSize; } @Override public ByteBuf allocate(ByteBufAllocator alloc) { return alloc.ioBuffer(bufferSize); } @Override public int guess() { return bufferSize; } @Override public void record(int actualReadBytes) { } } private final Handle handle; /** * Creates a new predictor that always returns the same prediction of * the specified buffer size. */ public FixedRecvByteBufAllocator(int bufferSize) { if (bufferSize <= 0) { throw new IllegalArgumentException( "bufferSize must greater than 0: " + bufferSize); } handle = new HandleImpl(bufferSize); } @Override public Handle newHandle() { return handle; }}
实现非常简单,FixedRecvByteBufAllocator提供了一个固定长度的接收缓冲区,就好比与JDK原生的ByteBuffer,跳过吧…
PS:这里担心大家理解有误,追加下说明,虽然其ByteBuf长度是固定,但当容量不足时,因Netty-ByteBuf本身的特性,还是会进行动态扩展的。
重点是另外一个,AdaptiveRecvByteBufAllocator,看官方说法,是一个能根据以往接受的消息进行计算,动态调整内存,利用CPU资源来换取内存资源,具体的实现策略如下:根据之前Channel接收到的数据包大小进行计算,如果连续填充满接收缓冲区的可写空间,则动态扩展容量。如果连续2次接收到的数据包都小于指定值,则收缩当前的容量,以节约内存。具体使用时,代码如下:
Bootstrap serverBootstrap = new Bootstrap(); serverBootstrap.group(nioEventLoopGroup); serverBootstrap.channel(NioDatagramChannel.class); serverBootstrap.option(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(DEFAULT_MIN_SIZE, DEFAULT_INITIAL_SIZE, DEFAULT_MAX_SIZE)); //或者使用如下的方式,用默认的分配器 //option(ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator.DEFAULT);
值得注意的是,无论是接收缓冲区还是接收缓冲区,大小都建议设置为消息的平均大小,不要设置成消息的最大上限,通过如下方式可设置缓冲区的初始大小:
/** * Creates a new predictor with the specified parameters. * * @param minimum the inclusive lower bound of the expected buffer size * @param initial the initial buffer size when no feed back was received * @param maximum the inclusive upper bound of the expected buffer size */ public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum) {
从如上AdaptiveRecvByteBufAllocator的构造方法,可得知其需要三个参数,当然该接收缓冲区也有默认的实现:
static final int DEFAULT_MINIMUM = 64; static final int DEFAULT_INITIAL = 1024; static final int DEFAULT_MAXIMUM = 65536; public static final AdaptiveRecvByteBufAllocator DEFAULT = new AdaptiveRecvByteBufAllocator(); /** * Creates a new predictor with the default parameters. With the default * parameters, the expected buffer size starts from {@code 1024}, does not * go down below {@code 64}, and does not go up above {@code 65536}. */ private AdaptiveRecvByteBufAllocator() { this(DEFAULT_MINIMUM, DEFAULT_INITIAL, DEFAULT_MAXIMUM); }
那具体内部的实现逻辑是怎么做到的呢,让我们来一步步看下源码逻辑:
首先第一步,需要进行静态代码块初始化:
private static final int INDEX_INCREMENT = 4; private static final int INDEX_DECREMENT = 1; private static final int[] SIZE_TABLE; static { List<Integer> sizeTable = new ArrayList<Integer>(); for (int i = 16; i < 512; i += 16) { sizeTable.add(i); } for (int i = 512; i > 0; i <<= 1) { sizeTable.add(i); } SIZE_TABLE = new int[sizeTable.size()]; for (int i = 0; i < SIZE_TABLE.length; i ++) { SIZE_TABLE[i] = sizeTable.get(i); } }
分配了一个int类型的数组,并进行该数组的初始化处理,从实现来看,该数组的长度是53,前32位是16的倍数,value值是从16开始,到512;从第33位开始,值是前一位的两倍,即从1024、2048、到最大值1073741824。
那我们来看下,AdaptiveRecvByteBufAllocator的构造方法是怎样的:
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); } int minIndex = getSizeTableIndex(minimum); if (SIZE_TABLE[minIndex] < minimum) { this.minIndex = minIndex + 1; } else { this.minIndex = minIndex; } int maxIndex = getSizeTableIndex(maximum); if (SIZE_TABLE[maxIndex] > maximum) { this.maxIndex = maxIndex - 1; } else { this.maxIndex = maxIndex; } this.initial = initial; }
构造器需要三个参数,第一个是缓冲区预期的最小长度下限;第二个是初始化时的大小,最后一个是缓冲区期望的最大长度上限
从代码逻辑可见,minimum不能大于初始化长度initial,而初始化长度initial不能超过maximum
上面逻辑中,还关乎了一个关键方法,getSizeTableIndex,让我们进一步看看该方法主要干了啥:
private static int getSizeTableIndex(final int size) { for (int low = 0, high = SIZE_TABLE.length - 1;;) { if (high < low) { return low; } if (high == low) { return high; } int mid = low + high >>> 1; int a = SIZE_TABLE[mid]; int b = SIZE_TABLE[mid + 1]; if (size > b) { low = mid + 1; } else if (size < a) { high = mid - 1; } else if (size == a) { return mid; } else { return mid + 1; } } }
入参是一个大小,然后利用二分查找法对该数组进行size的定位,目标是为了找出该size值在数组中的下标位置,该构造方法,最终初始化了如下是三个参数:
private final int minIndex; private final int maxIndex; //注意最后一个参数没有进行下标查找 private final int initial;
到目前为止,我们大致了解了,该AdaptiveRecvByteBufAllocator的内部的一些数据结构,那具体的处理过程是怎样的,详见如下的Hander:
private static final class HandleImpl implements Handle { private final int minIndex; private final int maxIndex; private int index; private int nextReceiveBufferSize; private boolean decreaseNow; HandleImpl(int minIndex, int maxIndex, int initial) { this.minIndex = minIndex; this.maxIndex = maxIndex; index = getSizeTableIndex(initial); nextReceiveBufferSize = SIZE_TABLE[index]; } @Override public ByteBuf allocate(ByteBufAllocator alloc) { return alloc.ioBuffer(nextReceiveBufferSize); } @Override public int guess() { return nextReceiveBufferSize; } ... }
该实现类,有一个关键的参数nextReceiveBufferSize,在其构造器中可见,用initial获取一开始初始化缓冲的下标,在根据SIZE_TABLE查找应分配的BufferSize。
进一步往下看,在实际进行分配时,分配大小传递的是nextReceiveBufferSize这个参数,由此可见,该参数是每次调整容量的关键。
那具体是谁在更新这个参数呢,我们进一步看HanderImpl类的最后一个方法:
@Override public void record(int actualReadBytes) { if (actualReadBytes <= SIZE_TABLE[Math.max(0, index - INDEX_DECREMENT - 1)]) { if (decreaseNow) { index = Math.max(index - INDEX_DECREMENT, minIndex); nextReceiveBufferSize = SIZE_TABLE[index]; decreaseNow = false; } else { decreaseNow = true; } } else if (actualReadBytes >= nextReceiveBufferSize) { index = Math.min(index + INDEX_INCREMENT, maxIndex); nextReceiveBufferSize = SIZE_TABLE[index]; decreaseNow = false; } }
该方法的参数是一次读取操作中实际读取到的数据大小,将其与nextReceiveBufferSize 进行比较,如果实际字节数actualReadBytes大于等于该值,则立即更新nextReceiveBufferSize ,其更新后的值与INDEX_INCREMENT有关。INDEX_INCREMENT为默认常量,值为4。也就是说在扩容时会一次性增大多一些,以保证下次有足够空间可以接收数据。而相对扩容的策略,缩容策略则实际保守些,常量为INDEX_INCREMENT,值为1,同样也是进行对比, 但不同的是,若实际字节小于所用nextReceiveBufferSize,并不会立马进行大小调整,而是先把 decreaseNow 设置为true,如果下次仍然小于,则才会减少nextReceiveBufferSize的大小。
看到这里,我们大概了解了动态接收缓冲区的实现逻辑,本质是利用了预先分配好的SIZE_TABLE数组来进行空间大小的分配。
如何实战
讲到具体的使用,我们先从请求协议的角度来看待Netty4对于接收缓冲区是如何做默认处理的。
先来看看UDP协议,来一段bootstrap的最基本逻辑:
EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) .channel(NioDatagramChannel.class) .handler(new XXXXXXServerHandler()); b.bind(PORT).sync().channel().closeFuture().await(); } finally { group.shutdownGracefully(); }
如上代码,就是一般最常见的创建基于UDP协议服务端的启动代码,进一步看看NioDatagramChannel,其是如何创建接收缓冲区的:
具体如何运行到该类的创建,有兴趣的同学,具体可以跟着代码逻辑从bind->doBind->initAndRegister->newChannel->newInstance。
private static final RecvByteBufAllocator DEFAULT_RCVBUF_ALLOCATOR = new FixedRecvByteBufAllocator(2048); /** * Create a new instance which will use the Operation Systems default {@link InternetProtocolFamily}. */ public NioDatagramChannel() { this(newSocket(DEFAULT_SELECTOR_PROVIDER)); } public NioDatagramChannel(DatagramChannel socket) { super(null, socket, SelectionKey.OP_READ); config = new NioDatagramChannelConfig(this, socket); }
如上可见,创建UDP协议服务端时,其底层默认就是使用了FixedRecvByteBufAllocator,并且长度上只分配了2048的空间,用的时候要注意啦!
好,那TCP协议的流程又是怎样的,同样让我们看一段TCP协议的boostrap代码:
EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new SocksServerInitializer()); b.bind(PORT).sync().channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); }
可见看到的是,其会创建两个group,对reactor模型有认识的,应该会知道两个group对应着两种Channel通道
bossGroup对应NioServerSocketChannel,而workerGroup对应NioSocketChannel,那NioSocketChannel通道对应的缓冲区又是怎样搞的呢?
那首先从workerGroup的线程run方法起,当然这里不展开,具体会运行到processSelectedKey,并进行请求的读取
@Override public final void read() { final ChannelConfig config = config(); if (!config.isAutoRead() && !isReadPending()) { // ChannelConfig.setAutoRead(false) was called in the meantime removeReadOp(); return; } final ChannelPipeline pipeline = pipeline(); final ByteBufAllocator allocator = config.getAllocator(); final int maxMessagesPerRead = config.getMaxMessagesPerRead(); RecvByteBufAllocator.Handle allocHandle = this.allocHandle; if (allocHandle == null) { this.allocHandle = allocHandle = config.getRecvByteBufAllocator().newHandle(); } ByteBuf byteBuf = null; int messages = 0; boolean close = false; try { int totalReadAmount = 0; boolean readPendingReset = false; do { byteBuf = allocHandle.allocate(allocator);
如上可得知,实际的接受ByteBuf分配器是在config方法中获取的,我们进入该方法:
private static final RecvByteBufAllocator DEFAULT_RCVBUF_ALLOCATOR = AdaptiveRecvByteBufAllocator.DEFAULT; private volatile RecvByteBufAllocator rcvBufAllocator = DEFAULT_RCVBUF_ALLOCATOR; @Override public RecvByteBufAllocator getRecvByteBufAllocator() { return rcvBufAllocator; }
实际中发现,该接收缓冲区的分配器,是采用了默认的AdaptiveRecvByteBufAllocator的默认策略。
大致了解了TCP和UDP的不同默认分配规则,若我们有场景需要自己创建,则可用如下代码来实现
.option(ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator.DEFAULT); .option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(DEFAULT_SIZE)); .option(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(DEFAULT_MIN_SIZE, DEFAULT_INITIAL_SIZE, DEFAULT_MAX_SIZE));
- 解读Netty之接收缓冲区
- Netty源码解读之线程
- Netty源码解读之线程
- Netty精粹之玩转NIO缓冲区
- netty学习之二:ByteBuf解读
- netty 解读
- Netty缓冲区动态扩容
- 接收缓冲区数据包
- Socket接收字节缓冲区
- 接收和发送缓冲区
- Socket接收字节缓冲区
- Netty框架解读
- Netty源码解读
- Netty源码解读Promise
- Netty源码解读EventExecutorGroup
- Netty源码解读DefaultPromise
- [netty核心类]--缓冲区ByteBuf
- Netty源码解读 Netty中的buffer
- WebCollector Maven支持(含所有依赖jar包)
- 市场需求分析(MRD)模板
- ArcGIS Engine中如何往已有要素类中插入数据
- css梯形、六角星、五角星、六边形
- 默认情况下从svn checkout项目报错解决方法
- 解读Netty之接收缓冲区
- runtime 实现nscoding
- Linux命令行
- ?c#把一行数据插入到datatable里面去?
- TOKEN+签名验证
- 寒假第一次录一个小小的模拟慕课视频吧~<关于函数定义和调用>
- ubuntu windows远程桌面连接xrdp相关问题
- select的if选择
- 【工作笔记】互斥锁