让咱们来到微观世界重新认识 Netty
在后面 Netty 源码解析系列《聊聊 Netty 那些事儿》中,笔者率领大家从宏观世界具体分析了 Netty 的整个运行流程。从一个网络数据包在内核中的收发过程开始聊起,总体介绍了 Netty 的 IO 线程模型,前面咱们围绕着这个 IO 模型又具体介绍了整个 Reactor 模型在 Netty 中的实现。
这个宏观流程包含:Reactor 模型的创立,启动,运行架构,网络连接的接管和敞开,网络数据的接管和发送,利用 pipeline 对 IO 解决逻辑的编排,Netty 的优雅敞开。
Netty 的源码解析系列写到这里,笔者算是带着大家在 Netty 的宏观世界中飞翔了一圈,但笔者还是不舍得和大家说再见,于是决定在率领大家到 Netty 的微观世界中一探到底,这个系列的目标就是想让大家从内核层面深刻地搞透 Netty。
在 Netty 的微观世界系列中,笔者会为大家讲述 Netty 中的高性能组件的相干设计和实现以及利用。内容包含:
- Netty 中的网络数据容器 ByteBuf 的整个设计体系的实现。
- Netty 中的内存池设计与实现,在这个过程中,笔者会把 Linux 内核中内存管理子系统相干源码带大家走读一遍,让大家从内核层面到利用层面彻底搞透彻高性能内存调配的原理及其实现。
- Netty 中用于执行海量延时工作的工夫轮相干设计与实现,并与 Kafka 中的工夫轮设计做出具体比照。
- Netty 中用到的零拷贝技术在内核中的实现。
- Netty 中用到的 MPSC(多生产者单消费者)队列的设计与实现以及利用场景。
- Netty 中实现无锁化并发的要害组件 FastThreadLocal 的设计与实现,并具体比照 FastThreadLocal 到底比 JDK 中 ThreadLocal 快在了哪里。
- 实践讲完了,实际是必不可少的,最初笔者会带大家分析 Netty 在各个驰名中间件中是如何应用的,进一步加深大家对 Netty 的了解。
笔者的这个 Netty 微观世界系列会波及大量丰盛的细节形容,对于喜爱细节控的同学肯定不要错过~~
写在本文开始之前 …..
本文咱们开始 Netty 微观世界系列第一局部的内容,聊聊 Netty 中的网络数据容器 ByteBuf,对于 ByteBuf 我想大家肯定不会生疏,它曾多次呈现在后面的系列文章中,比方在《Netty 如何高效接管网络数据 | 一文聊透 ByteBuffer 动静自适应扩缩容机制》和《一文搞懂 Netty 发送数据全流程 | 你想晓得的细节全在这里》这两篇文章中提到的 Netty 接管网络数据和发送网络数据时用到的 ByteBuf。
ByteBuf 是 Netty 中的数据容器,Netty 在接管网络数据和发送网络数据时,都会首先将这些网络数据当时缓存在 ByteBuf 中,而后在将它们丢给 pipeline 解决或者发送给 Socket,这样做的目标是避免在接管网络数据的过程中网络数据始终积压在 Socket 的接收缓冲区中使得接收缓冲区的数据越来越多,导致对端 TCP 协定中的窗口敞开(滑动窗口),影响到了整个 TCP 通信的速度。而有了 ByteBuf,咱们能够先将读取的数据缓存在 ByteBuf 中,进步 TCP 的通信能力。
而在 Netty 发送数据的时候,也能够当时将数据缓存在 ByteBuf 中,如果 Socket 发送缓冲区已满变为不可写状态时,因为数据咱们曾经缓存在 ByteBuf 中了,用户的发送线程不须要阻塞期待,当 Socket 发送缓冲区再次变得可写时,Netty 会将 ByteBuf 中的数据写入到 Socket 中。这也是 Netty 实现异步发送数据的外围所在。
而 Netty 中的 ByteBuf 底层依赖了 JDK NIO 中的 ByteBuffer。家喻户晓 JDK NIO 中的 ByteBuffer 设计的非常复杂而且提供的相干 API 应用起来也很反人类,易用性不是很好,所以 Netty 的 ByteBuf 针对 JDK NIO ByteBuffer 进行了优化,再此基础上从新设计出了一套简洁易用的 API 进去。
相熟笔者写作格调的读者敌人都晓得,笔者一贯是喜爱把技术的脉络给大家铺展开来解说,一层一层地介绍技术的演变过程,力求给大家清晰地展现出整个技术的全貌。通过技术的演变过程,咱们不仅能够晓得这个技术点最后的样貌,它的优缺点是什么?瓶颈是什么?咱们还能够针对这些毛病和瓶颈触发本人的思考,如何优化?如何演变?通过这个过程的洗礼,咱们才可能对现有技术了解的清晰透彻。
依据这个思路,在介绍 Netty 的 ByteBuf 设计之前,笔者想专门用一篇文章来为大家介绍下 JDK NIO Buffer 的设计,看一下 NIO ByteBuffer 是如何设计的,它有哪些毛病。针对这些毛病,Netty 又是如何优化的。彻底了解 Netty 数据载体 ByteBuf 的前世今生。
1. JDK NIO 中的 Buffer
在 NIO 没有呈现之前,Java 传统的 IO 操作都是通过流的模式实现的(包含网络 IO 和文件 IO),也就是咱们常见的输出流 InputStream 和输入流 OutputStream。
然而 Java 传统 IO 的 InputStream 和 OutputStream 的相干操作全部都是阻塞的,比方咱们应用 InputStream 的 read 办法从流中读取数据时,如果此时流中没有数据,那么用户线程就必须阻塞期待。
还有一点就是传统的这些输入输出流在解决字节流的时候一次只能解决一个字节,这样在解决网络 IO 的时候读取 Socket 缓冲区中的数据效率就会很低,而且在操作字节流的时候只能线性的解决流中的字节,不能来回挪动字节流中的数据。这样导致咱们在解决字节流中的数据的时候就显得不是很灵便。
所以综上所述,Java 传统 IO 是面向流的,流的解决是单向,阻塞的,而且无论是从输出流中读取数据还是向输入流中写入数据都是一个字节一个字节来解决的。通常都是从输出流中边读取数据边解决数据,这样 IO 解决效率就会很低,
基于上述起因,JDK1.4 引入了 NIO,而 NIO 是面向 Buffer 的,在解决 IO 操作的时候,会一次性将 Channel 中的数据读取到 Buffer 中而后在做后续解决,向 Channel 中写入数据也是一样,也是须要一个 Buffer 做直达,而后将 Buffer 中的数据批量写入 Channel 中。这样一来咱们能够利用 Buffer 将外面的字节数据来回挪动并依据咱们想要的解决形式灵活处理。
除此之外,Nio Buffer 还提供了堆外的间接内存和内存映射相干的拜访形式,来防止内存之间的来回拷贝,所以即便在传统 IO 中用到了 BufferedInputStream 也还是没方法和 Nio Buffer 相匹敌。
那么接下来就让咱们正式进入 JDK NIO Buffer 如何设计与实现的相干主题
2. NIO 对 Buffer 的顶层形象
JDK NIO 提供的 Buffer 其实实质上是一块内存,大家能够把它简略设想成一个数组,JDK 将这块内存在语言层面封装成了 Buffer 的模式,咱们能够通过 Buffer 对这块内存进行读取或者写入数据,以及执行各种骚操作。
如下图中所示,Buffer 类是 JDK NIO 定义的一个顶层抽象类,对于缓冲区的所有基本操作和根底属性全副定义在顶层 Buffer 类中,在 Java 中一共有八种根本类型,JDK NIO 也为这八种根本类型别离提供了其对应的 Buffer 类,大家能够把这些 Buffer 类当做成对应根底类型的数组,咱们能够利用这些根底类型相干的 Buffer 类对数组进行各种操作。
在为大家解析具体的缓冲区实现之前,咱们先来看下这个缓冲区的顶层抽象类 Buffer 中到底定义标准了哪些形象操作,具备哪些属性,这些属性别离是用来干什么的?先带大家从总体上认识一下 JDK NIO 中的 Buffer 设计。
2.1 Buffer 中的属性
public abstract class Buffer {
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
.............
}
首先咱们先来介绍下 Buffer 中最重要的这三个属性,前面行将介绍的对于 Buffer 的各种骚操作均依赖于这三个属性的动态变化。
- capacity:这个很好了解,它规定了整个 Buffer 的容量,具体能够包容多少个元素。capacity 指针之前的元素均是 Buffer 可操作的空间。
- position:用于指向 Buffer 中下一个可操作性的元素,初始值为 0。在 Buffer 的写模式下,position 指针用于指向下一个可写地位。在读模式下,position 指针指向下一个可读地位。
- limit:示意 Buffer 可操作元素的下限。什么意思呢?比方在 Buffer 的写模式下,可写元素的下限就是 Buffer 的整体容量也就是 capacity,capacity – 1 即为 Buffer 最初一个可写地位。在读模式下,Buffer 中可读元素的下限即为上一次 Buffer 在写模式下最初一个写入元素的地位。也就是上一次写模式中的 position。
- mark:用于标记 Buffer 以后 position 的地位。这个字段在咱们对网络数据包解码的时候十分有用,在咱们应用 TCP 协定进行网络数据传输的时候常常会呈现粘包拆包的景象,所以为了应答粘包拆包的问题,在解码之前都须要先调用
mark 办法将 Buffer 的以后 position 指针保留至 mark 属性中,如果 Buffer 中的数据足够咱们解码为一个残缺的包,咱们就执行解码操作。如果 Buffer 中的数据不够咱们解码为一个残缺的包(也就是半包),咱们就调用 reset 办法,将 position 还原到原来的地位,期待剩下的网络数据到来。
在咱们了解了 Buffer 中这几个重要属性的含意之后,接下来咱们就来看一看 JDK NIO 在 Buffer 顶层设计类中定义标准的那些形象操作。
2.2 Buffer 中定义的外围形象操作
本大节中介绍的这几个对于 Buffer 的外围操作均是基于上大节中介绍的那些外围指针的动静调整实现的。
2.2.1 Buffer 的结构
结构 Buffer 的次要逻辑就是依据用户指定的参数来初始化 Buffer 中的这四个重要属性:mark,position,limit,capacity。它们之间的关系为:mark <= position <= limit <= capacity。其中 mark 初始默认为 -1,position 初始默认为 0。
public abstract class Buffer {
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
Buffer(int mark, int pos, int lim, int cap) {if (cap < 0)
throw new IllegalArgumentException("Negative capacity:" + cap);
this.capacity = cap;
limit(lim);
position(pos);
if (mark >= 0) {if (mark > pos)
throw new IllegalArgumentException("mark > position: ("
+ mark + ">" + pos + ")");
this.mark = mark;
}
}
public final Buffer limit(int newLimit) {if ((newLimit > capacity) || (newLimit < 0))
throw new IllegalArgumentException();
limit = newLimit;
if (position > limit) position = limit;
if (mark > limit) mark = -1;
return this;
}
public final Buffer position(int newPosition) {if ((newPosition > limit) || (newPosition < 0))
throw new IllegalArgumentException();
position = newPosition;
if (mark > position) mark = -1;
return this;
}
}
2.2.2 获取 Buffer 下一个可读取地位
当咱们在 Buffer 的读模式下,须要从 Buffer 中读取数据时,须要首先晓得以后 Buffer 中 position 的地位,而后依据 position 的地位读取 Buffer 中的元素。随后 position 向后挪动指定的步长 nb。
nextGetIndex()
办法首先获取 Buffer 以后 position 的地位作为 readIndex 返回给用户,而后 position 向后挪动一位。这里的步长 nb 默认为 1。
final int nextGetIndex() {if (position >= limit)
throw new BufferUnderflowException();
return position++;
}
nextGetIndex(int nb)
办法的逻辑和 nextGetIndex() 办法一样,惟一不同的是该办法指定了 position 向后挪动的步长 nb。
final int nextGetIndex(int nb) {if (limit - position < nb)
throw new BufferUnderflowException();
int p = position;
position += nb;
return p;
}
大家这里可能会感到好奇,为什么会减少一个指定 position 挪动步长的 nextGetIndex(int nb) 办法呢?
在《2. NIO 对 Buffer 的顶层形象》大节的开始,咱们介绍了 JDK NIO 中 Buffer 顶层设计体系,除了 boolean 这个根本类型,NIO 为简直所有的 Java 根本类型定义了对应的 Buffer 类。
如果咱们从一个 ByteBuffer 中读取一个 int 类型的数据时,咱们就须要在读取结束后将 position 的地位向后挪动 4 位。在这种状况下 nextGetIndex(int nb) 办法的步长 nb 就应该指定为 4.
public int getInt() {return getInt(ix(nextGetIndex((1 << 2))));
}
2.2.3 获取 Buffer 下一个可写入地位
同获取 readIndex 的过程一样,当咱们处于 Buffer 的写模式下,向 Buffer 写入数据时,首先也须要获取 Buffer 以后 position 的地位(writeIndex), 当写入元素后,position 向后挪动指定的步长 nb。
同样的情理,咱们能够向 ByteBuffer 中写入一个 int 型的数据,这时候指定的步长 nb 也是 4。
final int nextPutIndex() {if (position >= limit)
throw new BufferOverflowException();
return position++;
}
final int nextPutIndex(int nb) {if (limit - position < nb)
throw new BufferOverflowException();
int p = position;
position += nb;
return p;
}
2.2.4 Buffer 读模式的切换
当咱们在 Buffer 的写模式下向 Buffer 写入数据之后,接下来咱们就须要从 Buffer 中读取刚刚写入的数据。因为 NIO 在对 Buffer 的设计中读写模式是混用一个 position 属性,所以咱们须要做读模式的切换。
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
咱们看到 flip()
办法是对 Buffer 中的这四个指针做了一些调整达到了读模式切换的目标:
- 将下一个可写入地位 position 作为读模式下的下限 limit。
- position 设置为 0。这样使得咱们能够从头开始读取 Buffer 中写入的数据。
2.2.5 Buffer 写模式的切换
有读模式的切换必定就会有对应的写模式切换,当咱们在读模式下以将 Buffer 中的数据读取结束之后,这时候如果再次向 Buffer 写入数据的话,就须要切换到 Buffer 的写模式下。
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
咱们看到调用 clear()
办法之后,Buffer 中各个指针的状态又回到了最后的状态:
- position 地位从新指向起始地位 0 处。写入下限 limit 从新指向了 capacity 的地位。
- 这时向 Buffer 中写入数据时,就会从 Buffer 的结尾处顺次写入,新写入的数据就会把曾经读取的那局部数据笼罩掉。
然而这里就会有一问题,当咱们在读模式下将 Buffer 中的数据全副读取结束时,调用 clear() 办法开启写模式,是没有问题的。
如果咱们只是读取了 Buffer 中的局部数据,然而还有一部分数据没有读取,这时候,调用 clear() 办法开启写模式向 Buffer 中写入数据的话,就会出问题,因为这会笼罩掉咱们还没有读取的数据局部。
针对这种状况,咱们就不能简略粗犷的设置 position 指针了,为了保障未读取的数据局部不被笼罩,咱们就须要先将不可笼罩的数据局部挪动到 Buffer 的最前边,而后将 position 指针指向可笼罩数据区域的第一个地位。
因为 Buffer 是顶层设计只是负责定义 Buffer 相干的操作标准,并未定义具体的数据存储形式,因为 compact()
波及到挪动数据,所以实现在了 Buffer 具体子类中,这里咱们以 HeapByteBuffer 举例说明:
class HeapByteBuffer extends ByteBuffer {
//HeapBuffer 中底层负责存储数据的数组
final byte[] hb;
public ByteBuffer compact() {System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
position(remaining());
limit(capacity());
discardMark();
return this;
}
public final int remaining() {return limit - position;}
final void discardMark() {mark = -1;}
}
2.2.6 从新读取 Buffer 中的数据 rewind
rewind()
办法能够帮忙咱们从新读取 Buffer 中的数据,它会将 position 的值从新设置为 0,并抛弃 mark。
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
3. NIO Buffer 背地的存储机制
在《2. NIO 对 Buffer 的顶层形象》大节的结尾提到咱们能够把 Buffer 简略的看做是一个数组,而后基于前边介绍的四个指针:mark,position,limit,capacity 的动静调整来实现对 Buffer 的各种操作。
同时咱们也提到了除了 boolean 这种根本类型之外,NIO 为其余几种 Java 根本类型都提供了其对应的 Buffer 类。
而针对每一种根本类型的 Buffer,NIO 又依据 Buffer 背地的数据存储内存不同分为了:HeapBuffer,DirectBuffer,MappedBuffer。
HeapBuffer 顾名思义它背地的存储内存是在 JVM 堆中调配,在堆中调配一个数组用来寄存 Buffer 中的数据。
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
// 在堆中应用一个数组寄存 Buffer 数据
final byte[] hb;}
DirectBuffer 背地的存储内存是在堆外内存中调配,MappedBuffer 是通过内存文件映射将文件中的内容间接映射到堆外内存中,其本质也是一个 DirectBuffer。
因为 DirectBuffer 和 MappedBuffer 背地的存储内存是在堆外内存中调配,不受 JVM 治理,所以不能用一个 Java 根本类型的数组示意,而是间接记录这段堆外内存的起始地址。
public abstract class Buffer {
// 堆外内存地址
long address;
}
笔者前面还会为大家具体解说 DirectBuffer 和 MappedBuffer。这里提前引出只是让大家了解这三种不同类型的 Buffer 背地内存区域的不同。
综上所述,HeapBuffer 背地是有一个对应的根本类型数组作为存储的。而 DirectBuffer 和 MappedBuffer 背地是一块堆外内存做存储。并没有一个根本类型的数组。
hasArray() 办法 就是用来判断一个 Buffer 背地是否有一个 Java 根本类型的数组做撑持。
public abstract boolean hasArray();
如果 hasArray() 办法返回 true,咱们就能够调用 Object array() 办法获取 Buffer 背地的撑持数组。
public abstract Object array();
其中 Buffer 中还有一个不太好了解的属性是 offset,而这个 offset 到底是用来干什么的呢?
4. Buffer 的视图
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
// 在堆中应用一个数组寄存 Buffer 数据
final byte[] hb;
// 数组中的偏移,用于指定数组中的哪一段数据是被 Buffer 包装的
final int offset;
}
事实上咱们能够依据一段间断的内存地址或者一个数组创立出不同的 Buffer 视图进去。
如上图所示,咱们能够依据原生 Buffer 中的局部数据(比方图中的未解决数据局部)创立出一个新的 Buffer 视图进去。
这个新的视图 Buffer 实质上也是一个 Buffer,领有独立的 mark,position,limit,capacity 指针。这个四个指针会在新的 Buffer 视图下从新被创立赋值。所以在新的视图 Buffer 下和操作一般 Buffer 是一样的,也能够应用《2.2 Buffer 中定义的外围形象操作》大节中介绍的那些办法。只不过操作的数据范畴不一样罢了。
新的视图 Buffer 和原生 Buffer 共享一个存储数组或者一段间断内存。
站在新的视图 Buffer 角度来说,它的存储数组范畴:0 - 6
,所以再此视图下 position = 0,limit = capacity = 7。这其实是一个障眼法,真实情况是新的视图 Buffer 其实是复用原生 Buffer 中的存储数组中的 6 - 12
这块区域。
所以在新视图 Buffer 中拜访元素的时候,就须要加上一个偏移 offset:position + offset
能力正确的拜访到实在数组中的元素。这里的 offset = 6。
咱们能够通过 arrayOffset() 办法获取视图 Buffer 中的 offset。
public abstract int arrayOffset();
以上内容就是笔者要为大家介绍的 NIO Buffer 的顶层设计,上面咱们来看下 Buffer 下具体的这些实现类。对于 Buffer 视图相干的创立和操作,笔者会把这部分内容放到具体的 Buffer 实现类中为大家介绍,这里大家只须要了解 Buffer 视图的概念即可~~~
5. 形象 Buffer 的具体实现类 ByteBuffer
通过后面大节内容的介绍,咱们晓得了 JDK NIO Buffer 为 Java 中每种根本类型都设计了对应的 Buffer 实现(除了 boolean 类型)。
而咱们本系列的主题是 Netty 网络通讯框架的源码解析,在网络 IO 解决中出镜率最高的当然是 ByteBuffer,所以在上面的例子中笔者均已 ByteBuffer 作为解说主线。置信大家在了解了 ByteBuffer 的整体脉络设计之后,在看其余根本类型的 Buffer 实现就能非常容易了解,基本上大同小异。
上面咱们就来正式开始 ByteBuffer 的介绍~~~
在前边《3. NIO Buffer 背地的存储机制》大节的介绍中,咱们晓得 NIO 中的 ByteBuffer 依据其背地内存调配的区域不同,分为了:HeapByteBuffer,MappedByteBuffer,DirectByteBuffer 这三种类型。
而这三种类型的 ByteBuffer 必定会有一些通用的属性以及办法,所以 ByteBuffer 这个类被设计成了一个抽象类,用来封装这些通用的属性和办法作为 ByteBuffer 这个根本类型 Buffer 的顶层标准。
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
// Buffer 背地的数组
final byte[] hb;
// 数组 offset,用于创立 Buffer 视图
final int offset;
// 标识 Buffer 是否是只读的
boolean isReadOnly;
ByteBuffer(int mark, int pos, int lim, int cap,
byte[] hb, int offset)
{super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
ByteBuffer(int mark, int pos, int lim, int cap) {this(mark, pos, lim, cap, null, 0);
}
}
ByteBuffer 中除了之前介绍的 Buffer 类中定义的四种重要属性之外,又额定定义了三种属性;
- byte[] hb:ByteBuffer 中背地依赖的用于存储数据的数组,该字段只实用于 HeapByteBuffer,而 DirectByteBuffer 和 MappedByteBuffer 背地依赖于堆外内存。这块堆外内存的起始地址存储于 Buffer 类中的 address 字段中。
- int offset:ByteBuffer 中的内存偏移,用于创立新的 ByteBuffer 视图。详情可回看《4. Buffer 的视图》大节。
- boolean isReadOnly:用于标识该 ByteBuffer 是否是只读的。
5.1 创立具体存储类型的 ByteBuffer
创立 DirectByteBuffer:
public static ByteBuffer allocateDirect(int capacity) {return new DirectByteBuffer(capacity);
}
创立 HeapByteBuffer:
public static ByteBuffer allocate(int capacity) {if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
因为 MappedByteBuffer 背地波及到的原理比较复杂(尽管 API 简略),所以笔者前面会有一篇专门解说 MappedByteBuffer 的文章,为了不使本文过于简单,这里就不列出了。
5.2 将字节数组映射成 ByteBuffer
通过前边的介绍,咱们晓得 Buffer 其实实质上就是一个数组,在 Buffer 中封装了一些对这个数组的便当操作方法。既然 Buffer 曾经为数组操作提供了便当,所以大家根本都不会违心去间接操作原生字节数组。这样一来将一个原生字节数组映射成一个 ByteBuffer 的需要就诞生了。
public static ByteBuffer wrap(byte[] array, int offset, int length) {
try {return new HeapByteBuffer(array, offset, length);
} catch (IllegalArgumentException x) {throw new IndexOutOfBoundsException();
}
}
ByteBuffer 中的 wrap 办法提供了这样的映射实现,该办法能够将字节数组全副映射成一个 ByteBuffer,或者将字节数组中的局部字节数据灵便映射成一个 ByteBuffer。
- byte[] array:须要映射成 ByteBuffer 的原生字节数组 array。
- int offset:用于指定映射之后 Buffer 的 position。position = offset。留神此处的 offset 并不是 Buffer 视图中的 offset。
- int length:用于计算映射之后 Buffer 的 limit。limit = offset + length,capacity = array,length。
映射后的 ByteBuffer 中 Mark = -1,offset = 0。此处的 offset 才是 Buffer 视图中的 offset。
HeapByteBuffer(byte[] buf, int off, int len) { // package-private
super(-1, off, off + len, buf.length, buf, 0);
}
以上介绍的 wrap 映射办法是依据用户本人指定的 position 和 limit 对原生字节数组进行灵便映射。当然 NIO 中还提供了一个办法是间接对原生字节数组 array 进行默认全副映射。映射之后的 Buffer:position = 0,limit = capacity = array.length。
public static ByteBuffer wrap(byte[] array) {return wrap(array, 0, array.length);
}
5.3 定义 ByteBuffer 视图相干操作
在前边《4. Buffer 的视图》大节的介绍中,笔者介绍顶层抽象类 Buffer 中定义的 offset 属性的时候,咱们提到过这个 offset 属性就是用来创立 Buffer 视图的。在该大节中笔者其实曾经将 Buffer 创立视图的相干原理和过程曾经给大家具体的介绍完了。而视图创立的相干操作就定义在 ByteBuffer 这个抽象类中,别离为 slice() 办法和 duplicate() 办法。
这里还是须要再次和大家强调的是咱们基于原生 ByteBuffer 创立进去新的 ByteBuffer 视图其实是 NIO 设计的一个障眼法。原生的 ByteBuffer 和它的视图 ByteBuffer 其实实质上共用的是同一块内存。对于 HeapByteBuffer 来说这块共用的内存就是 JVM 堆上的一个字节数组,而对于 DirectByteBuffer 和 MappedByteBuffer 来说这块共用的内存是堆外内存中的同一块内存区域。
ByteBuffer 的视图实质上也是一个 ByteBuffer,原生的 ByteBuffer 和它的视图 ByteBuffer 领有各自独立的 mark,position,limit,capacity 指针。只不过背地依附的内存空间是一样的。所以在视图 ByteBuffer 做的任何内容上的改变,原生 ByteBuffer 是看得见的。同理在原生 ByteBuffer 上做的任何内容改变,视图 ByteBuffer 也是看得见的。它们是相互影响的,这点大家须要留神。
5.3.1 slice()
public abstract ByteBuffer slice();
调用 slice()
办法创立进去的 ByteBuffer 视图内容是从原生 ByteBufer 的以后地位 position 开始始终到 limit 之间的数据。也就是说通过 slice() 办法创立进去的视图里边的数据是原生 ByteBuffer 中还未解决的数据局部。
如上图所属,调用 slice() 办法创立进去的视图 ByteBuffer 它的存储数组范畴:0 – 6,所以再此视图下 position = 0,limit = capacity = 7。这其实是一个障眼法,真实情况是新的视图 ByteBuffer 其实是复用原生 ByteBuffer 中的存储数组中的 6 – 12 这块区域(未解决的数据局部)。
所以在视图 ByteBuffer 中拜访元素的时候,就须要 position + offset 来拜访能力正确的拜访到实在数组中的元素。这里的 offset = 6。
上面是 HeapByteBuffer 中对于 slice() 办法的具体实现:
class HeapByteBuffer extends ByteBuffer {public ByteBuffer slice() {
return new HeapByteBuffer(hb,
-1,
0,
this.remaining(),
this.remaining(),
this.position() + offset);
}
}
5.3.2 duplicate()
而由 duplicate()
办法创立进去的视图相当于就是齐全复刻原生 ByteBuffer。它们的 offset,mark,position,limit,capacity 变量的值全副是一样的,这里须要留神尽管值是一样的,然而它们各自之间是互相独立的。用于对同一字节数组做不同的逻辑解决。
public abstract ByteBuffer duplicate();
上面是 HeapByteBuffer 中对于 duplicate()
办法的具体实现:
class HeapByteBuffer extends ByteBuffer {public ByteBuffer duplicate() {
return new HeapByteBuffer(hb,
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
offset);
}
}
5.3.3 asReadOnlyBuffer()
public abstract ByteBuffer asReadOnlyBuffer();
通过 asReadOnlyBuffer()
办法咱们能够基于原生 ByteBuffer 创立出一个只读视图。对于只读视图的 ByteBuffer 只能读取不能写入。对只读视图进行写入操作会抛出 ReadOnlyBufferException 异样。
上面是 HeapByteBuffer 中对于 asReadOnlyBuffer() 办法的具体实现:
class HeapByteBuffer extends ByteBuffer {public ByteBuffer asReadOnlyBuffer() {
return new HeapByteBufferR(hb,
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
offset);
}
}
NIO 中专门设计了一个只读 ByteBufferR 视图类。它的 isReadOnly 属性为 true。
class HeapByteBufferR extends HeapByteBuffer {protected HeapByteBufferR(byte[] buf,
int mark, int pos, int lim, int cap,
int off)
{super(buf, mark, pos, lim, cap, off);
this.isReadOnly = true;
}
}
5.4 定义 ByteBuffer 读写相干操作
ByteBuffer 中定义了四种针对 Buffer 读写的基本操作办法,因为 ByteBuffer 这个抽象类是一个顶层设计类,只是标准定义了针对 ByteBuffer 操作的根本行为,它并不负责具体数据的存储,所以这四种基本操作办法会在其具体的实现类中实现,这个咱们前面会一一介绍。这里只是向大家展现 NIO 针对 ByteBuffer 的顶层设计。
// 从 ByteBuffer 中读取一个字节的数据,随后 position 的地位向后挪动一位
public abstract byte get();
// 向 ByteBuffer 中写入一个字节的数据,随后 position 的地位向后挪动一位
public abstract ByteBuffer put(byte b);
// 依照指定 index 从 ByteBuffer 中读取一个字节的数据,position 的地位放弃不变
public abstract byte get(int index);
// 依照指定 index 向 ByteBuffer 中写入一个字节的数据,position 的地位放弃不变
public abstract ByteBuffer put(int index, byte b);
ByteBuffer 类中除了定义了这四种根本的读写操作,还根据这四个基本操作衍生出了几种通用操作,上面笔者来为大家介绍下这几种通用的操作:
1. 将 ByteBuffer 中的字节转移到指定的字节数组 dst 中:
- offset:dst 数组寄存转移数据的起始地位。
- length:从 ByteBuffer 中转移字节数。
public ByteBuffer get(byte[] dst, int offset, int length) {
// 查看指定 index 的边界,确保不能越界
checkBounds(offset, length, dst.length);
// 查看 ByteBuffer 是否有足够的转移字节
if (length > remaining())
throw new BufferUnderflowException();
int end = offset + length;
// 从以后 ByteBuffer 中 position 开始转移 length 个字节 到 dst 数组中
for (int i = offset; i < end; i++)
dst[i] = get();
return this;
}
2. 将指定字节数组 src 中的数据转移到 ByteBuffer 中:
- offset:从字节数组中的 offset 地位处开始转移。
- length:向 ByteBuffer 转移字节个数。
public ByteBuffer put(byte[] src, int offset, int length) {
// 查看指定 index 的边界,确保不能越界
checkBounds(offset, length, src.length);
// 查看 ByteBuffer 是否可能容纳得下
if (length > remaining())
throw new BufferOverflowException();
int end = offset + length;
// 从字节数组的 offset 处,转移 length 个字节到 ByteBuffer 中
for (int i = offset; i < end; i++)
this.put(src[i]);
return this;
}
在为大家介绍完 ByteBuffer 的形象设计之后,笔者置信大家当初曾经对 NIO 的 ByteBuffer 有了一个整体上的意识。
接下来的内容,笔者将会为大家具体介绍之前屡次提到的这三种 ByteBuffer 的具体实现类型:
让咱们从 HeapByteBuffer 开始,HeapByteBuffer 的相干实现最简略最容易了解的,咱们会在 HeapByteBuffer 的介绍中,具体介绍 Buffer 操作的实现。了解了 HeapByteBuffer 的相干实现,剩下的 Buffer 实现类就更容易了解了,都是大同小异。
6. HeapByteBuffer 的相干实现
通过前边几个大节的介绍,大家应该对 HeapByteBuffer 的构造很分明了,HeapByteBuffer 背地次要是依赖于 JVM 堆中的一个字节数组 byte[] hb。
在这个 JVM 堆中的字节数组的根底上,实现了在 Buffer 类和 ByteBuffer 类中定义的形象办法。
6.1 HeapByteBuffer 的结构
在 HeapByteBuffer 的结构过程中首先就会依据用户指定的 Buffer 容量 cap,在 JVM 堆中创立一个容量大小为 cap 的字节数组进去作为 HeapByteBuffer 底层存储数据的容器。
class HeapByteBuffer extends ByteBuffer {HeapByteBuffer(int cap, int lim) {super(-1, 0, lim, cap, new byte[cap], 0);
}
}
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
ByteBuffer(int mark, int pos, int lim, int cap,
byte[] hb, int offset)
{super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
}
还有咱们《5.2 将字节数组映射成 ByteBuffer》大节介绍的用于将原生字节数组映射成 ByteBuffer 的 wrap 办法中用到的构造函数:
public static ByteBuffer wrap(byte[] array, int offset, int length) {
try {return new HeapByteBuffer(array, offset, length);
} catch (IllegalArgumentException x) {throw new IndexOutOfBoundsException();
}
}
HeapByteBuffer(byte[] buf, int off, int len) {super(-1, off, off + len, buf.length, buf, 0);
}
以及咱们在《5.3 定义 ByteBuffer 视图相干操作》大节介绍的用于创立 ByteBuffer 视图的两个办法 slice() 和 duplicate() 办法中用到的构造函数:
protected HeapByteBuffer(byte[] buf,
int mark, int pos, int lim, int cap,
int off)
{super(mark, pos, lim, cap, buf, off);
}
6.2 从 HeapByteBuffer 中读取字节
6.2.1 依据 position 的地位读取一个字节
- 首先会通过《2.2.2 获取 Buffer 下一个可读取地位》大节介绍的 nextGetIndex() 办法获取以后 HeapByteBuffer 中的 position 地位,依据 position 的地位读取字节。
- 为了兼容 Buffer 视图的相干操作,定位读取地位 position 都会加上 offset。原生 Buffer 中的 offset = 0。
- 通过 position + offset 确定好拜访 Index 之后,就是数组的一般操作了,间接通过这个 Index 从 hb 字节数组中获取字节。随后 Buffer 中的 position 向后挪动一个地位。
class HeapByteBuffer extends ByteBuffer {protected final byte[] hb;
protected final int offset;
public byte get() {return hb[ix(nextGetIndex())];
}
// 确定拜访 index
protected int ix(int i) {return i + offset;}
}
6.2.2 依据指定的 Index 读取一个字节
咱们除了能够依据 Buffer 的 position 地位读取字节,还能够指定具体的 Index 来从 Buffer 中读取字节:
- 查看 Index 是否超出 Buffer 的边界范畴,通过查看之后 Index + offset 确定读取地位。
留神这个办法读取字节之后,position 的地位是不会扭转的。
public byte get(int i) {return hb[ix(checkIndex(i))];
}
6.2.3 将 HeapByteBuffer 中的字节转移到指定的字节数组中
这个办法其实笔者在《5.4 定义 ByteBuffer 读写相干操作》大节中介绍 ByteBuffer 的顶层标准设计时曾经提到过了,因为 ByteBuffer 只是一个抽象类负责顶层操作标准的定义,自身并不具备具体存储数据的能力,所以在 ByteBuffer 中只是提供了一个通用的实现。ByteBuffer 中的实现是通过在一个for () {....}
循环中不停的依据原生 Buffer 中的 position 指针(前边介绍的 get() 办法)遍历底层数组并一个一个的拷贝到指标字节数组 dst 中。这样的拷贝操作无疑是效率低下的。
而在 HeapByteBuffer 这个具体的 ByteBuffer 实现类中曾经定义了具体的存储形式,所以依据具体的存储形式可能做一下拷贝上的优化:
public ByteBuffer get(byte[] dst, int offset, int length) {checkBounds(offset, length, dst.length);
if (length > remaining())
throw new BufferUnderflowException();
System.arraycopy(hb, ix(position()), dst, offset, length);
position(position() + length);
return this;
}
HeapByteBuffer 中对于拷贝字节数组中的数据应用了 System.arraycopy
办法,该办法在 JVM 中是一个 intrinsic method
,是通过 JVM 编译器非凡优化的,比通过 JNI 调用 native 办法的性能还要高。
利用 System.arraycopy
办法将 HeapByteBuffer 中的字节数据从 position 开始,拷贝 length 个字节到指标字节数组 dst 中。
6.3 向 HeapByteBuffer 中写入字节
6.3.1 依据 position 的地位写入一个字节
- 首先会通过《2.2.3 获取 Buffer 下一个可写入地位》大节中介绍的 nextPutIndex() 办法获取以后 HeapByteBuffer 中的 position 地位,依据 position 的地位写入字节。
- 通过 position + offset 定位到写入地位 Index,而后向 HeapByteBuffer 底层的字节数组 hb 间接写入字节数据。随后 position 向后挪动一个地位。
public ByteBuffer put(byte x) {hb[ix(nextPutIndex())] = x;
return this;
}
protected int ix(int i) {return i + offset;}
6.3.2 依据指定的 Index 写入一个字节
留神通过这个办法依据指定 Index 写入字节之后,position 的地位是不会扭转的。
public ByteBuffer put(int i, byte x) {hb[ix(checkIndex(i))] = x;
return this;
}
6.3.3 将指定字节数组转移到 HeapByteBuffer 中
同理和《6.2.3 将 HeapByteBuffer 中的字节转移到指定的字节数组中》大节中介绍的相干办法一样,HeapByteBuffer 也是采纳了 JVM 中的 System.arraycopy
办法(intrinsic method)从而更加高效地进行字节数组的拷贝操作。
从字节数组 src 中的 offset 地位开始拷贝 length 个字节到 HeapByteBuffer 中
public ByteBuffer put(byte[] src, int offset, int length) {checkBounds(offset, length, src.length);
if (length > remaining())
throw new BufferOverflowException();
System.arraycopy(src, offset, hb, ix(position()), length);
position(position() + length);
return this;
}
HeapByteBuffer 背地依附的字节数组存储的是一个一个的字节,以上操作全副针对的是单个字节来的,所以并不需要思考字节序的影响,然而如果咱们想从 HeapByteBuffer 中读取写入一个 int 或者一个 double 类型的数据,那么咱们就须要思考字节序的问题了。
在介绍如何从 HeapByteBuffer 中读取或者写入一个指定根本类型数据之前,笔者先来为大家介绍一下:
- 到底什么是字节序?
- 为什么会有字节序的存在?
- 字节序对 Buffer 的操作会有什么影响?
7. 字节序
谈起字节序来大家可能都会有这样的感触就是记了忘,忘了记,记了又忘。所以为了让大家清晰地了解字节序并且深深地刻入脑海中,笔者处心积虑终于想出了一个生存中的例子来为大家阐明字节序。
笔者平时有健身的习惯,曾经保持撸铁四年多了,为了给身材补充蛋白质减少肌肉量,每天打底至多 15 个鸡蛋,所以剥鸡蛋就成为了笔者日常的一个重要工作。
那么问题来了,在咱们剥鸡蛋的时候,咱们到底是该从鸡蛋大的一端剥起还是从鸡蛋小的的一端剥起呢?
这还真是一个问题,有的人喜爱从小端剥起,然而笔者习惯从大端开始剥起。于是就有了 大端 - 小端 的剥法。
既然剥鸡蛋有大端 - 小端的一致在,那么在计算机网络传输数据时也会存在这样的问题,计算机中是怎么扯出大端 - 小端的一致呢?请急躁听笔者接着讲下去~~
咱们都晓得在计算机中存储数据,字符编码以及网络中传输数据时都是通过一个 bit 一个 bit 组成的 010101 这样的二进制模式传输存储的。因为本系列的主题是对于网络 IO 的解决,所以笔者这里以网络传输中的字节序举例:
比方当初咱们要传输一个 int 型的整数 5674 到对端主机中。int 型的变量 5674 对应的二进制是 1011000101010。如下图所示:
剥鸡蛋的一致在于是从大的一端开始剥还是从小的一端开始剥,从大的一端开始剥咱们叫做大端剥法,而从小的一端开始剥咱们叫做小端剥法。
同样的情理,咱们在网络传输二进制数据的时候也有一致:咱们是从二进制的高位开始传输呢(图中绿色区域)?还是从二进制的低位开始传输呢(图中黄色区域)?
如果咱们从二进制数据的高位(类比鸡蛋的大端)开始传输咱们就叫 大端字节序 ,如果咱们从二进制的低位(类比鸡蛋的小端)开始传输就叫 小端字节序。
网络协议采纳的是 大端字节序 传输
好了,当初对于网络传输字节的程序问题,咱们论述分明了,那么接下来咱们看下当网络字节传输到对端时,对端如何接管?
当网络字节依照大端字节序传输到对端计算机时,对端会在操作系统的堆中开拓一块内存用来接管网络字节。而在操作系统的虚拟内存布局中,堆空间的地址增长方向是从低地址向高地址增长,而栈空间的地址是从高地址向低地址增长。
当初咱们假如如果当网络字节传输到对端计算机中,咱们在对端应用 HeapByteBuffer 去接管网络字节(这里只是假如,实际上都是应用 DirectByteBuffer),通过前边内容的介绍咱们晓得,HeapByteBuffer 背地其实依附一个字节数组来存储字节。如图中所示,字节数组从索引 0 开始到索引 6 它们在内存中的地址是从低地址到高地址。
了解了这些,上面咱们就来看下字节在不同字节序下是如何接管存储的。
7.1 大端字节序
如图中所示,在 大端字节序 下 int 型变量 5674 它的字节 高位 被存储在了字节数组中的 低地址 中,字节的 低位 被存储在字节数组的 高地址 中。这就是大端字节序,也是比拟合乎人类的直观感触。
7.2 小端字节序
然而在 小端字节序 下,int 型变量 5674 它的字节 高位 被存储在了字节数组中的 高地址 中,字节的 低位 被存储在字节数组的 低地址 中。这就是小端字节序,正好和失常人类直观感触是相同的。
到当初,我想大家应该最起码从概念上晓得什么是大端字节序?什么是小端字节序了吧?
上面笔者在带大家到实战中,再去体验一把大端字节序和小端字节序的不同。彻底让大家了解分明。
8. 向 HeapByteBuffer 中写入指定根本类型
HeapByteBuffer 背地是一个在 JVM 堆中开拓的一个字节数组,里边寄存的是一个一个的字节,当咱们以单个字节的模式操作 HeapByteBuffer 的时候并没有什么问题,可是当咱们向 HeapByteBuffer 写入一个指定的根本类型数据时,比方写入一个 int 型(占用 4 个字节),写入一个 double 型(占用 8 个字节),就必须要思考字节序的问题了。
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
boolean bigEndian = true;
boolean nativeByteOrder = (Bits.byteOrder() == ByteOrder.BIG_ENDIAN);
}
咱们能够强制网络协议传输应用大端字节序,然而咱们无奈强制主机中采纳的字节序,所以咱们须要常常在网络 IO 场景下做一些字节序的转换工作。
JDK NIO ByteBuffer 默认的字节序为 大端模式,咱们能够通过 NIO 提供的操作类 Bits
获取主机字节序 Bits.byteOrder()
,或者间接获取 NIO ByteBuffer 中的 nativeByteOrder 字段判断主机字节序:true 示意主机字节序为大端模式,false 示意主机字节序为小端模式。
当然咱们也能够通过 ByteBuffer 中的 order 办法来指定咱们想要的字节序:
public final ByteBuffer order(ByteOrder bo) {bigEndian = (bo == ByteOrder.BIG_ENDIAN);
nativeByteOrder =
(bigEndian == (Bits.byteOrder() == ByteOrder.BIG_ENDIAN));
return this;
}
上面笔者就带大家别离从大端模式和小端模式下来看一下如何向 HeapByteBuffer 写入一个指定根本类型的数据。咱们以 int 型数据举例,假如要写入的 int 值 为 5674。
8.1 大端字节序
class HeapByteBuffer extends ByteBuffer {public ByteBuffer putInt(int x) {Bits.putInt(this, ix(nextPutIndex(4)), x, bigEndian);
return this;
}
}
首先咱们会获取以后 HeapByteBuffer 的写入地位 position,因为咱们须要写入的是一个 int 型的数据,所以当写入结束之后 position 的地位须要向后挪动 4 位。nextPutIndex 办法的逻辑笔者在之前的内容中曾经具体介绍过了,这里不在赘述。
class Bits {static void putInt(ByteBuffer bb, int bi, int x, boolean bigEndian) {if (bigEndian)
// 采纳大端字节序写入 int 数据
putIntB(bb, bi, x);
else
// 采纳小端字节序写入 int 数据
putIntL(bb, bi, x);
}
static void putIntB(ByteBuffer bb, int bi, int x) {bb._put(bi , int3(x));
bb._put(bi + 1, int2(x));
bb._put(bi + 2, int1(x));
bb._put(bi + 3, int0(x));
}
}
大家看到了吗,这里就是依照咱们之前介绍的大端字节序,从 int 值 5674 的二进制高位字节到低位字节顺次写入 HeapByteBuffer 中字节数组的低地址中。
这里的 int3(x)
办法就是负责获取写入数据 x 的最高位字节,并将最高位字节(下图中绿色局部)写入字节数组中的低地址中(下图中对应绿色局部)。
同理 int2(x),int1(x),int0(x) 办法顺次获取 x 的次高位字节,顺次写入字节数组中的低地址中。
那么咱们如何顺次取得一个 int 型数据的高位字节呢?大家接着跟着笔者往下走~
8.1.1 int3(x) 获取 int 型最高位字节
class Bits {private static byte int3(int x) {return (byte)(x >> 24); }
}
8.1.2 int2(x) 获取 int 型次高位字节
class Bits {private static byte int2(int x) {return (byte)(x >> 16); }
}
8.1.3 int1(x) 获取 int 型第三高位字节
class Bits {private static byte int1(int x) {return (byte)(x >> 8); }
}
8.1.4 int0(x) 获取 int 型最低位字节
class Bits {private static byte int0(int x) {return (byte)(x); }
}
最终 int 型变量 5764 依照大端字节序写入到 HeapByteBuffer 之后的字节数组构造如下:
8.2 小端字节序
在咱们彻底了解了大端字节序的操作之后,小端字节序的相干操作就很好了解了。
static void putIntL(ByteBuffer bb, int bi, int x) {bb._put(bi + 3, int3(x));
bb._put(bi + 2, int2(x));
bb._put(bi + 1, int1(x));
bb._put(bi , int0(x));
}
依据咱们之前介绍的小端字节序的定义,在小端模式下二进制数据的高位是存储在字节数组中的高地址中,二进制数据的低位是存储在字节数组中的低地址中。
9. 从 HeapByteBuffer 中读取指定根本类型
当咱们分明了在不同的字节序下如何向 HeapByteBuffer 中写入指定根本类型数据的过程之后,那么在不同字节序下向 HeapByteBuffer 读取指定根本类型数据的过程,我想大家就能很容易了解了。
咱们还是以 int 型数据举例,假如要从 HeapByteBuffer 中读取一个 int 型的数据。
首先咱们还是获取以后 HeapByteBuffer 中的读取地位 position,从 position 地位开始读取四个字节进去,而后通过这四个字节组装成一个 int 数据返回。
class HeapByteBuffer extends ByteBuffer {public int getInt() {return Bits.getInt(this, ix(nextGetIndex(4)), bigEndian);
}
}
class Bits {static int getInt(ByteBuffer bb, int bi, boolean bigEndian) {return bigEndian ? getIntB(bb, bi) : getIntL(bb, bi) ;
}
}
咱们还是先来介绍大端模式下的读取过程:
9.1 大端字节序
class Bits {static int getIntB(ByteBuffer bb, int bi) {return makeInt(bb._get(bi),
bb._get(bi + 1),
bb._get(bi + 2),
bb._get(bi + 3));
}
}
因为在大端模式下,二进制数据的高位是寄存于字节数组中的低地址中,咱们须要从字节数组中的低地址中顺次读取二进制数据的高位进去。
而后咱们从高位开始顺次组装 int 型数据,正好和写入过程相同。
static private int makeInt(byte b3, byte b2, byte b1, byte b0) {return (((b3) << 24) |
((b2 & 0xff) << 16) |
((b1 & 0xff) << 8) |
((b0 & 0xff) ));
}
9.2 小端字节序
class Bits {static int getIntL(ByteBuffer bb, int bi) {return makeInt(bb._get(bi + 3),
bb._get(bi + 2),
bb._get(bi + 1),
bb._get(bi));
}
}
而在小端模式下,咱们则须要先从字节数组中的高地址中将二进制数据的高位顺次读取进去,而后在从高位开始顺次组装 int 型数据。
在笔者介绍完了对于 int 数据的读写过程之后,置信大家能够很轻松的了解其余根本类型在不同字节序下的读写操作过程了。
10. 将 HeapByteBuffer 转换成指定根本类型的 Buffer
在《2. NIO 对 Buffer 的顶层形象》大节一开始就介绍到,NIO 其实为咱们提供了多种根本类型的 Buffer 实现。
NIO 容许咱们将 ByteBuffer 转换成任意一种根本类型的 Buffer,这里咱们以转换 IntBuffer 为例阐明:
class HeapByteBuffer extends ByteBuffer {public IntBuffer asIntBuffer() {int size = this.remaining() >> 2;
int off = offset + position();
return (bigEndian
? (IntBuffer)(new ByteBufferAsIntBufferB(this,
-1,
0,
size,
size,
off))
: (IntBuffer)(new ByteBufferAsIntBufferL(this,
-1,
0,
size,
size,
off)));
}
}
IntBuffer 底层其实依靠了一个 ByteBuffer,当咱们向 IntBuffer 读取一个 int 数据时,其实是从底层依靠的这个 ByteBuffer 中读取 4 个字节进去而后组装成 int 数据返回。
class ByteBufferAsIntBufferB extends IntBuffer {
protected final ByteBuffer bb;
public int get() {return Bits.getIntB(bb, ix(nextGetIndex()));
}
}
class Bits {static int getIntB(ByteBuffer bb, int bi) {return makeInt(bb._get(bi),
bb._get(bi + 1),
bb._get(bi + 2),
bb._get(bi + 3));
}
static private int makeInt(byte b3, byte b2, byte b1, byte b0) {return (((b3) << 24) |
((b2 & 0xff) << 16) |
((b1 & 0xff) << 8) |
((b0 & 0xff) ));
}
}
同理,咱们向 IntBuffer 中写入一个 int 数据时,其实是想底层依靠的这个 ByteBuffer 写入 4 个字节。
IntBuffer 底层依靠的这个 ByteBuffer,会依据字节序的不同分为:ByteBufferAsIntBufferB(大端实现)和 ByteBufferAsIntBufferL(小端实现)。
在咱们具体介绍完 HeapByteBuffer 的实现之后,笔者这里就不在为大家具体介绍 ByteBufferAsIntBufferB 和 ByteBufferAsIntBufferL 了。操作全副是一样的,感兴趣的大家能够自行查看一下。
总结
本文咱们以 JDK NIO Buffer 中最简略的一个实现类 HeapByteBuffer 为主线从 NIO 对 Buffer 的顶层形象设计开始从整体上为大家介绍了 Buffer 的设计。
在这个过程中,咱们能够领会到 NIO 对 Buffer 的设计还是比较复杂的,尤其是咱们针对裸 NIO 进行编程的时候会有十分多的反人类操作,一不小心就会出错。
比方:用于 Buffer 读模式切换 flip() 办法,写模式切换的 clear() 办法和 compact() 办法以及用于重新处理 Buffer 中数据的 rewind() 办法。在咱们应用这些办法解决字节数据的时候须要时刻分明 Buffer 中的数据分布状况,一不小心就会造成数据的笼罩和失落。
前面咱们又介绍了 Buffer 中视图的概念和相干操作 slice() 办法和 duplicate() 办法,以及对于视图 Buffer 和原生 Buffer 之间的区别和分割。
咱们以 HeapByteBuffer 为例,介绍了 NIO Buffer 相干顶层形象办法的实现,并再次根底上更进一步介绍了在不同字节序下 ByteBuffer 相干的读取写入操作的具体过程。
最初咱们介绍了 ByteBuffer 与相干指定根本类型 Buffer(比方 IntBuffer,LongBuffer)在不同字节序下的转换。
另外咱们还交叉介绍了:到底什么是字节序? 为什么会有字节序的存在? 字节序对 Buffer 的操作会有什么影响?
因为 HeapByteBuffer 足够简略,所以利用它可能把整个 NIO 对 Buffer 的设计与实现串联起来,然而依据 Buffer 背地的存储机制不同,还有 DirectByteBuffer 和 MappedByteBuffer,它们的 API 在应用上根本和 HeapByteBuffer 是统一的。然而它们背地波及到的原理却是非常复杂的(尤其是 MappedByteBuffer)。
所以笔者前面会独自写两篇文章来具体别离为大家介绍 DirectByteBuffer 和 MappedByteBuffer 背地波及到的简单原理,目标是让大家不仅会应用而且还要把它们背地波及到的简单原理彻底搞透彻弄清楚,要知其然并且还要知其所以然~~~