深刻理解Netty中的Buffer

1. 引言

上一篇文章咱们概要介绍了Netty的原理及结构,下面几篇文章咱们开始对Netty的各个模块进行比较详细的分析。Netty的结构最底层是buffer模块,这部分也相对独立,咱们就先从buffer讲起。程序员

2. Netty的4W1H

2.1 What(什么是Buffer)

buffer中文名又叫缓冲区,按照维基百科的解释,是”在数据传输时,在内存里开辟的一块临时保存数据的区域”。它实际上是一种化同步为异步的机制,能够解决数据传输的速率不对等以及不稳定的问题。算法

根据这个定义,咱们能够知道涉及I/O(特别是I/O写)的地方,基本会有buffer的存在。就Java来讲,咱们很是熟悉的Old I/O–InputStream&OutputStream系列API,基本都是在内部使用到了buffer。Java课程老师就教过,outputStream.write()只将内容写入了buffer,必须调用outputStream.flush(),才能保证数据写入生效!编程

而NIO中则直接将buffer这个概念封装成了对象,其中最经常使用的大概是ByteBuffer了。因而使用方式变为了:将数据写入Buffer,flip()一下,而后将数据读出来。因而,buffer的概念更加深刻人心了!缓存

Netty中的buffer也不例外。不一样的是,Netty的buffer专为网络通信而生,因此它又叫ChannelBuffer(好吧其实没有什么因果关系…)。咱们下面就来说讲Netty中的buffer。固然,关于Netty,咱们必须讲讲它的所谓”Zero-Copy-Capable”机制。性能优化

2.2 When & Where(TCP/IP协议与buffer)

TCP/IP协议是目前的主流网络协议。它是一个多层协议,最下层是物理层,最上层是应用层(HTTP协议等),而在Java开发中,通常只接触TCP以上,即传输层和应用层的内容。这就是Netty的主要应用场景。网络

TCP报文有个比较大的特色,就是它传输的时候,会先把应用层的数据项拆开成字节,而后按照本身的传输须要,选择合适数量的字节进行传输。什么叫”本身的传输须要”?首先TCP包有最大长度限制,那么太大的数据项确定是要拆开的。其次由于TCP以及下层协议会附加一些协议头信息,若是数据项过小,那么可能报文大部分都是没有价值的头信息,这样传输是很不划算的。所以有了收集必定数量的小数据,并打包传输的Nagle算法(这个东东在HTTP协议里会很讨厌,Netty里能够用setOption(“tcpNoDelay”, true)关掉它)。app

这么说可能太抽象了一点,咱们举个例子吧:框架

发送时,咱们这样分3次写入(‘|’表示两个buffer的分隔):异步

+-----+-----+-----+
| ABC | DEF | GHI |
+-----+-----+-----+

接收时,可能变成了这样:tcp

+----+-------+---+---+
| AB | CDEFG | H | I |
+----+-------+---+---+

很好懂吧?但是,说了这么多,跟buffer有个什么关系呢?别急,咱们来看下面一部分。

2.3 Why(分层思想)

咱们先回到以前的messageReceived方法:

public void messageReceived(
        ChannelHandlerContext ctx, MessageEvent e) {
    // Send back the received message to the remote peer.
    transferredBytes.addAndGet(((ChannelBuffer) e.getMessage()).readableBytes());
    e.getChannel().write(e.getMessage());
}

这里MessageEvent.getMessage()默认的返回值是一个ChannelBuffer。咱们知道,业务中须要的”Message”,实际上是一条应用层级别的完整消息,而通常的buffer工做在传输层,与”Message”是不能对应上的。那么这个ChannelBuffer是什么呢?

来一个官方给的一段代码,我想这个答案就很明显了:

requestPart1 = buffer1.silice(OFFSET_PAYLOAD,buffer1.readableBytes() - OFFSET_PAYLOAD);

requestPart2 = buffer2.silice(OFFSET_PAYLOAD,buffer2.readableBytes() - OFFSET_PAYLOAD);

request = ChannelBuffers.wrappedBuffer(requestPart1,requestPart2);

输入图片说明

这里能够看到,TCP层HTTP报文被分红了两个ChannelBuffer,这两个Buffer对咱们上层的逻辑(HTTP处理)是没有意义的。可是两个ChannelBuffer被组合起来,就成为了一个有意义的HTTP报文,这个报文对应的ChannelBuffer,才是能称之为”Message”的东西。这里用到了一个词”Virtual Buffer”,也就是所谓的”Zero-Copy-Capable Byte Buffer”了。是否是顿时以为豁然开朗了?

我这里总结一下,若是要说NIO的Buffer和Netty的ChannelBuffer最大的区别的话,就是前者仅仅是传输上的Buffer,然后者实际上是传输Buffer和抽象后的逻辑Buffer的结合。延伸开来讲,NIO仅仅是一个网络传输框架,而Netty是一个网络应用框架,包括网络以及应用的分层结构。

固然,使用ChannelBuffer表示”Message”,不失为一个比较实用的方法,可是使用一个对象来表示解码后的Message可能更符合习惯一点。在Netty里,MessageEvent.getMessage()是能够存放一个POJO的,这样子抽象程度又高了一些,这个咱们在之后讲到ChannelPipeline的时候会说到。

2.4 How(ChannelBuffer实现原理)

好了,终于来到了代码实现部分。之因此啰嗦了这么多,由于我以为,关于”Zero-Copy-Capable Rich Byte Buffer”,理解为何须要它,比理解它是怎么实现的,可能要更重要一点。

关于代码阅读,我想可能不少朋友跟我同样,喜欢”顺藤摸瓜”式读代码–找到一个入口,而后顺着查看它的调用,直到理解清楚。很幸运,ChannelBuffers(注意有s!)就是这样一根”藤”,它是全部ChannelBuffer实现类的入口,它提供了不少静态的工具方法来建立不一样的Buffer,靠“顺藤摸瓜”式读代码方式,大体能把各类ChannelBuffer的实现类摸个遍。先列一下ChannelBuffer相关类图。

输入图片说明

此外还有WrappedChannelBuffer系列也是继承自AbstractChannelBuffer,图放到了后面。

3. Buffer源码解读

3.1 ChannelBuffer中的readerIndex和writerIndex

Netty中的buffer是彻底从新实现的,与NIO ByteBuffer与ByteBuffer不一样的是,它内部保存了一个读指针readerIndex和一个写指针writerIndex,能够同时进行读和写,而不须要使用flip()进行读写切换。AbstactChannelBuffer类里面包含了主要的读写逻辑,贴一段代码,让你们能看的更明白一点:

public void writeByte(int value) {
setByte(writerIndex ++, value);
}

public byte readByte() {
if (readerIndex == writerIndex) {
throw new IndexOutOfBoundsException("Readable byte limit exceeded: "
+ readerIndex);
}
return getByte(readerIndex ++);
}

public int writableBytes() {
return capacity() - writerIndex;
}

public int readableBytes() {
return writerIndex - readerIndex;
}

这里readerIndex老是小于writerIndex。我以为这样的方式很是天然,比单指针与flip()要更加好理解一些。AbstactChannelBuffer还有两个相应的mark指针markedReaderIndex和markedWriterIndex,跟NIO的原理同样,做标记用,这里再也不赘述了。

3.2 字节序Endianness与HeapChannelBuffer

HeapChannelBuffer是最经常使用的Buffer,跟NIO HeapByteBuffer做用至关,其底层也是一个byte[]。

HeapChannelBuffer有两个子类:BigEndianHeapChannelBuffer和LittleEndianHeapChannelBuffer。这里有个很基础的概念:字节序(ByteOrder/Endianness)。字节序规定了多于一个字节的数字(int啊long什么的),如何在内存中表示。BIG_ENDIAN(大端序)表示高位在前,按照大端序,整型数12会被存储为0 0 0 12这样四个字节,而LITTLE_ENDIAN则正好相反。可能搞C/C++的程序员对这个会比较熟悉,而Javaer则比较陌生一点,由于Java已经把内存给管理好了。可是在网络编程方面,根据协议的不一样,不一样的字节序也可能会被用到。目前大部分协议仍是采用大端序,可参考RFC1700。

了解了这些知识,咱们也很容易就知道为何会有BigEndianHeapChannelBuffer和LittleEndianHeapChannelBuffer了。

3.3 DynamicChannelBuffer

DynamicChannelBuffer是一个很方便的Buffer,之因此叫Dynamic是由于它的长度会根据内容的长度来扩充,你能够像使用ArrayList同样,无须关心其容量。DynamicChannelBuffer实现自动扩容的核心在于ensureWritableBytes方法,算法很简单:在写入前作容量检查,容量不够时,新建一个容量x2的buffer,跟ArrayList的扩容是相同的。贴一段代码吧(为了代码易懂,这里我删掉了一些边界检查,只保留主逻辑):

public void writeByte(int value) {
    ensureWritableBytes(1);
    super.writeByte(value);
}

public void ensureWritableBytes(int minWritableBytes) {
    if (minWritableBytes <= writableBytes()) {
        return;
    }

    int newCapacity = capacity();
    int minNewCapacity = writerIndex() + minWritableBytes;
    while (newCapacity < minNewCapacity) {
        newCapacity <<= 1;
    }

    ChannelBuffer newBuffer = factory().getBuffer(order(), newCapacity);
    newBuffer.writeBytes(buffer, 0, writerIndex());
    buffer = newBuffer;
}

3.4 CompositeChannelBuffer

CompositeChannelBuffer是由多个ChannelBuffer组合而成的,能够看作一个总体进行读写。这里有一个技巧:CompositeChannelBuffer并不会开辟新的内存并直接复制全部ChannelBuffer内容,而是直接保存了全部ChannelBuffer的引用,并在子ChannelBuffer里进行读写,从而实现了”Zero-Copy-Capable”。来段简略版的代码,应该更能说明其原理:

public class CompositeChannelBuffer{

    //components保存全部内部ChannelBuffer
    private ChannelBuffer[] components;
    //indices记录在整个CompositeChannelBuffer中,每一个components的起始位置
    private int[] indices;
    //缓存上一次读写的componentId
    private int lastAccessedComponentId;

    public byte getByte(int index) {
        //经过indices中记录的位置索引到对应第几个子Buffer
        int componentId = componentId(index);
        return components[componentId].getByte(index - indices[componentId]);
    }

    public void setByte(int index, int value) {
        int componentId = componentId(index);
        components[componentId].setByte(index - indices[componentId], value);
    }

}

查找componentId的算法再次不做介绍了,你们本身实现起来也不会太难。值得一提的是,基于ChannelBuffer连续读写的特性,使用了顺序查找(而不是二分查找),而且用lastAccessedComponentId来进行缓存。

3.5 ByteBufferBackedChannelBuffer

前面说ChannelBuffer是本身的实现的,其实只说对了一半。ByteBufferBackedChannelBuffer就是封装了NIO ByteBuffer的类,用于实现堆外内存的Buffer(使用NIO的DirectByteBuffer)。固然,其实它也能够放其余的ByteBuffer的实现类。代码实现就不说了,也没啥可说的。

3.6 WrappedChannelBuffer

输入图片说明

WrappedChannelBuffer都是几个对已有ChannelBuffer进行包装,完成特定功能的类。代码不贴了,实现都比较简单,列一下功能吧。

输入图片说明

至此Netty 3.7的buffer部分咱们基本了解了,相关内容仍是比较简单的,也没有太多费脑细胞的地方。

Netty 4.0以后就不一样了,ChannelBuffer更名ByteBuf,成为了单独项目buffer,而且为了性能优化,加入了BufferPool之类的机制,已经变得比较复杂了(本质倒没怎么变)。性能优化是个比较复杂的事情,研究源码时,建议先避开这些东西,了解其总体结构,等到须要深刻时再对算法进行细致研究。举个例子,Netty4.0里为了优化,将Map换成了Java 8里6000行的ConcurrentHashMapV8,大家感觉一下…

下篇文章咱们开始讲Channel。

相关文章
相关标签/搜索