Buffer
一般读写步骤
1、向 buffer 写入数据
(1)当向 buffer 写入数据时,buffer 会记录写了多少数据
2、调用 flip(),切换为读模式
(1)flip() 使 buffer 中的 limit 变为 position,position 变为 0
(2)读模式下,可以读取之前写入到 buffer 的所有数据
3、从 buffer 读取数据,例如调用 buffer.get()
4、调用 clear() 或 compact(),切换为写模式
(1)调用 clear() 时,清空整个缓冲区,position=0,limit 变为 capacity
(2)调用 compact() 时,只清除已经读过的数据,任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面
5、重复 1 ~ 4 步骤
父类 Buffer 的核心属性
1、四个属性必须满足以下要求
(1)mark <= position <= limit <= capacity
(2)position 和 limit 的含义,取决于 Buffer 处在读模式 / 写模式
(3)不管 Buffer 处在什么模式,capacity 含义总是一样的
2、capacity
private int capacity;
(1)缓冲区的容量
(2)通过构造函数赋予,一旦设置,无法更改
(3)写满 Buffer 后,需要将其清空(通过读数据或清除数据),才能继续写数据
3、limit
private int limit;
(1)缓冲区的界限
(2)位于 limit 后的数据不可读写
(3)缓冲区的限制不能为负,并且不能大于其容量
(4)写数据时,limit 表示可对 Buffer 最多写入多少个数据,写模式下,limit 等于 Buffer 的 capacity
(5)读数据时,limit 表示 Buffer 中有多少可读数据(not null 数据),因此能读到之前写入的所有数据;切换到读模式时,limit 设置为写模式下的 position
4、position
private int position = 0;
(1)下一个读写位置的索引
(2)缓冲区的位置不能为负,并且不能大于 limit
(3)写数据到 Buffer 中时,position 表示从第 position 个位置开始写入;当一个数据写到 Buffer 后, position 会下移到下一个可插入数据的 Buffer 单元;position 最大可为 capacity – 1(因为 position 的初始值为 0)
(4)在 Buffer 中读数据时,position 表示从第 position 个数据开始读取;通过 flip() 切换到读模式时,position 会被重置为 0;当从 Buffer 读入数据后,position 会下移到下一个可读入的数据 Buffer 单元
5、mark
private int mark = -1;
(1)记录当前 position 值
(2)position 被改变后,可以通过调用 reset(),恢复到 mark 位置
Buffer
1、将此缓冲区的 mark 设置在其 position
public final Buffer mark() {
mark = position;
return this;
}
2、将此缓冲区的 position 重置为先前 mark 的位置
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
(1)调用此方法既不会更改也不丢弃该标记的值
3、翻转缓冲区
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
(1)切换为读模式
(2)limit 设置为当前 position,然后将 position 设置为 0,如果 mark 被定义,则它被丢弃
4、倒带缓冲区
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
(1)重读 Buffer
(2)position 设置为 0,mark 被丢弃
5、清除此缓冲区
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
(1)重写 Buffer
(2)position 设置为 0,limit 设置为 capacity,mark 被丢弃
(3)实际上并不会清除缓冲区中的数据,但是它被命名为它的确是因为它最常用于情况也是如此
ByteBuffer
1、分配一个新的字节缓冲区
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
(1)新缓冲区的 position 将为零,其 limit 将为其 capacity,其 mark 将不定义,并且其每个元素将被初始化为 0
(2)它将有一个 backing array,其 array offset 将为 0
(3)capacity:新的缓冲区的容量,以字节为单位
2、读取该缓冲区当前 position 字节
public abstract byte get()
(1)返回缓冲区当前 position 的字节
(2)position + 1
3、读取给定索引处的字节
public abstract byte get(int index)
(1)index:读取字节的索引
(2)不会改变 position 值
4、将给定字节写入当前 position 的缓冲区
public abstract ByteBuffer put(byte b)
(1)b:要写入的字节
(2)position + 1
5、压缩缓冲区
public abstract ByteBuffer compact();
(1)缓冲区当前 position、limit(如果有的话)之间的字节,被复制到缓冲区的开头,将 position 设置为最后一个未读元素的后一位
(2)索引 p = position() 字节被复制到索引 0,索引 p + 1 处的字节被复制到索引 1,直到索引 limit() - 1 的字节被复制到索引 n = limit() - 1 - p,然后将缓冲区的 position 设置为 n + 1 ,并将其 limit 设置为 capacity,mark 如果被定义,则被丢弃
(3)缓冲区的 position 设置为复制的字节数,而不是 0
(4)在未完全读取之前,切换为写模式,保护未读数据
6、将一个字节数组包装到缓冲区中
public static ByteBuffer wrap(byte[] array) {
return wrap(array, 0, array.length);
}
(1)新的缓冲区将由给定的字节数组支持,即对缓冲区的修改将导致数组被修改,反之亦然
(2)新缓冲区的 capacity、limit 将为 array.length,其 position 将为 0,不定义其 mark,其 backing array 将是给定的数组,其 array offset 为 0
(3)array:构建缓冲区的数组
public static ByteBuffer wrap(byte[] array, int offset, int length) {
try {
return new HeapByteBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
throw new IndexOutOfBoundsException();
}
}
(4)新的缓冲区将由给定的字节数组支持,即对缓冲区的修改将导致数组被修改,反之亦然
(5)新增的缓冲区 capacity 将为 array.length,其 position 将为 offset,其 limit 将为 offset + length,不定义其 mark,其 backing array 将是给定的数组,其 array offset 将为 0
(6)array:构建新缓冲区的数组
(7)offset:要使用的子阵列的偏移量,必须是非负数,不得大于 array.length
(8)length:要使用的子阵列的长度,必须是非负数,不得大于 array.length - offset
缓冲区分片
1、在 NIO 中,除了分配或包装一个缓冲区对象外,还可以根据现有的缓冲区对象来创建一个子缓冲区
2、在现有缓冲区上切出一片来作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的
3、子缓冲区相当于是现有缓冲区的一个视图窗口
4、创建一个新的字节缓冲区,其内容是此缓冲区内容的共享子序列
public abstract ByteBuffer slice();
(1)新缓冲区的内容将从此缓冲区的当前 position 开始
(2)对这个缓冲区内容的更改将在新的缓冲区中可见,反之亦然
(3)两个缓冲区的 position、limit、mark 将是独立的
(4)新缓冲区的 position 将为 0,其 capacity 和 limit 将是此缓冲区中剩余的字节数,其 mark 将不定义
(5)当且仅当该缓冲区是直接缓冲区时,新缓冲区才是直接缓冲区
(6)当且仅当该缓冲区是只读缓冲区时,新缓冲区才是只读缓冲区
只读缓冲区
1、可以读取数据,但不能写入数据
2、创建一个新的只读字节缓冲区,共享原缓冲区的内容
public abstract ByteBuffer asReadOnlyBuffer()
(1)新缓冲区的内容将是原缓冲区的内容,原缓冲区内容的更改将在新的缓冲区中显示
(2)新的缓冲区本身将是只读的,不允许修改共享内容
(3)两个缓冲区的 position、limit、mark 将是独立的
(4)新缓冲区的 capacity、limit、position、mark 将与原缓冲区的 capacity、limit、position、mark 相同
(5)如果原缓冲区本身是只读的,则该方法的行为与 duplicate 方法完全相同
3、如果尝试修改只读缓冲区的内容,则会报 ReadOnlyBufferException 异常
4、只读缓冲区可以保护数据
(1)在将缓冲区传递给某个对象的方法时,无法知道这个方法是否会修改缓冲区中的数据
(2)创建一个只读的缓冲区,可以保证该缓冲区不会被修改
5、只可以把常规缓冲区转换为只读缓冲区,而不能将只读的缓冲区转换为可写的缓冲区
直接缓冲区
1、直接缓冲区是为加快 I/O 速度,使用一种特殊方式为其分配内存的缓冲区
2、JDK 文档描述
(1)给定一个直接字节缓冲区,JVM 将尽最大努力直接对它执行本机 I/O 操作
(2)它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中,或者从一个中间缓冲区中拷贝数据
3、使用方式与普通缓冲区并无区别
4、分配一个新的直接字节缓冲区
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
(1)新缓冲区的 position 将为 0,其 limit 将为其 capacity,其 mark 将不定义,并且其每个元素将被初始化为 0,是否有一个 backing array 是未指定的
(2)capacity:新的缓冲区的容量,以字节为单位
内存映射文件 I/O
1、一种读和写文件数据的方法,可以比常规的基于 Stream 或基于 Channel 的 I/O 快
2、并非将整个文件读到内存中,一般只有文件中实际读取或写入的部分才会映射到内存中
3、实现:MappedByteBuffer
获取 MappedByteBuffer:将此 FileChannel 文件的区域直接映射到内存中
public abstract MappedByteBuffer map(FileChannel.MapMode mode,
long position,
long size)
throws IOException
1、文件的区域可以以三种模式之一映射到存储器中
(1)只读:任何尝试修改生成的缓冲区将导致抛出 ReadOnlyBufferException(MapMode.READ_ONLY)
(2)读写:对结果缓冲区所做的更改将最终传播到文件中,它们可能也可能不会被映射到同一文件的其他程序可见(MapMode.READ_WRITE)
(3)私有:对结果缓冲区所做的更改不会传播到该文件,并且对于映射了相同文件的其他程序将不可见; 相反,它们将导致要创建的缓冲区的修改部分的私有副本(MapMode.PRIVATE)
2、对于只读映射,此通道必须已打开才能读取
3、对于读/写、私有映射,此通道必须已被打开以供读写
4、该方法返回的 mapped byte buffer 的 position 为 0,容量为 size,它的 mark 将是未定义的,缓冲区及其表示的映射将保持有效,直到缓冲区本身被垃圾回收为止
5、映射一旦建立,就不依赖于用于创建它的文件通道,关闭通道,特别是对映射的有效性没有影响
6、内存映射文件的许多细节固有地依赖于底层操作系统,因此是未指定的
(1)当请求的区域未完全包含在该通道的文件中时,此方法的行为是未指定的
(2)对该程序或另一个的底层文件的内容或大小进行的更改是否被传播到缓冲区是未指定的
(3)缓冲区的更改传播到文件的速率是未指定的
7、对于大多数操作系统,将文件映射到内存中比通过通常的 read 和 write 方法读取或写入几十千字节的数据更耗时,从性能的角度来看,通常只能将较大的文件映射到内存中
字符串、ByteBuffer 相互转换
1、方法一
(1)编码:字符串调用 getByte,获得 byte[],将 byte[] 放入 ByteBuffer 中
(2)解码:先调用 ByteBuffer 的 flip(),然后通过 StandardCharsets 的 decoder 解码
2、方法二
(1)编码:通过 StandardCharsets 的 encode,获得 ByteBuffer,此时获得 ByteBuffer 为读模式,无需通过 flip 切换模式
(2)解码:通过 StandardCharsets 的 decoder 解码
3、方法三
(1)编码:字符串调用 getByte,获得 byte[],将 byte[] 传给 ByteBuffer 的 wrap,获得 ByteBuffer 为读模式,无需调用 flip 切换模式
(2)解码:通过 StandardCharsets 的 decoder 解码
TCP 是流式协议,消息无边界
1、一个发送可能被多次接收,多个发送可能被一次接收
2、一个发送可能占用多个传输包,多个发送可能公用一个传输包
3、粘包
(1)发送方在发送数据时,并不是一条一条地发送数据,而是将数据整合在一起,当数据达到一定的数量后再一起发送,导致多条信息被放在一个缓冲区中被一起发送出去,在一条消息中读取到了另一条消息的部分数据
(2)发送方每次写入数据 < Socket 缓冲区大小
(3)接收方读取套接字缓冲区数据不够及时
4、半包
(1)接收方的缓冲区的大小是有限的,当接收方的缓冲区满了以后,就需要将信息截断,等缓冲区空后再继续放入数据,就会发生一段完整的数据最后被截断的现象
(2)发送方写入数据 > Socket 缓冲区大小
(3)发送的数据大于协议的 MTU(Maximum Transmission Unit,最大传输单元),导致必须拆包
5、解决:定义消息边界
(1)固定长度:服务端和客户端规定固定长度的缓冲区,当消息数据长度不足时,使用规定的填充字符进行填充,增加不必要的数据传输,造成网络传输负担,不建议使用
(2)结束标识:在包体尾部增加标识符,表示一条完整的消息数据已经结束,若消息体本身包含该标识符,需要做转义处理,效率不高
(3)长度信息:定义一个 Header + Body 格式,Header 中定义一个开始标记 + 一个内容的长度(Body 实际长度),Body 为消息内容,当接收方接收到数据流时,先根据 Header 中的特殊标记区分消息的开始,获取到 Header 中的内容长度描述时,再根据内容长度描述来截取 Body 部分
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战