NIO三大组件底层详解

目录

Buffer

基本流程

三大属性

postition

limit

常用方法

分配初始大小

写入数据

flip() 方法

读取数据

rewind() 方法

clear() 与 compact() 方法

mark() 和 reset() 方法

equals()和compareTo()

Channel

实现方式

FileChannel

DatagramChannel

Selector

SelectableChannel(可选择通道)

注册过程

SelectionKey(选择键)

实现方式


三大组件:Channel(通道),Buffer(缓冲区), Selector(选择器)

基本步骤:

1、创建 ServerSocketChannel 通道,设置为非阻塞模式

2、创建 Selector 选择器

3、Channel 注册到选择器中,监听连接事件

4、调用 Selector 中的 select 方法(循环调用),监听通道是否是就绪状态

5、调用 SelectKeys() 方法获取就绪 channel 集合

6、遍历就绪的 channel 集合,判断就绪事件类型,实现具体的业务操作。

7、根据业务流程,判断是否需要再次注册事件监听事件,重复执行。

Buffer

buffer 用来缓冲读写数据,数据是通道读取到缓冲区,从缓冲区写入到通道中的,本质是在内存上的一个数组,常见的 buffer 有

  • ByteBuffer
    • MappedByteBuffer
    • DirectByteBuffer
    • HeapByteBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • CharBuffer

基本流程

读写时一般遵循以下四个步骤:

(1)写数据到 Buffer

(2)调用flip() 方法

(3)从 Buffer 中读取数据

(4)调用 clear() 方法或者 compact() 方法

当向 buffer 写数据时,buffer 会记录下写了多少数据。一旦要读数据,需要通过 flip() 方法将 buffer 从写模式切换到读模式。在读模式下,可以读取到之前写入到 buffer 的所有数据。一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入

有两种方式能清空缓冲区:调用 clear() 或者 compact() 方法。clear() 方法会清空整个缓冲区。compact() 方法只会清除已经读过的数据。任何未读取的数据都被移动到了缓冲区的起始处,新写的数据将放到缓冲区未读数据的后面。

三大属性

  • capacity
  • ponstition
  • limit

position 和 limit 的含义取决于 Buffer 处在读模式还是写模式。不管 buffer 处于什么模式,capactity 的含义是此缓冲区的最大容量。

postition

均表示当前写或读的位置

写数据时

position 表示写入数据的当前位置,position 的初始值为 0 。当一个 byte,long,等数据写入到 buffer 后,position 会向下移动到下一个可插入的元素的 buffer 单元。position 最大可为 capacity-1 (position 的初始值为 0)

读数据时

position 表示读数据的当前位置,如 position = 2 时表示已经开始读了 3 个 byte, 或者从第三个 byte 开始读取,通过 ByteBuffer.flip() 切换到读模式 position 会被重置为 0, 当 Buffer 从 position 读入数据后,position 会下移到下一个可读入的数据 Buffer 单元。

limit

均表示最多可读或可写的数量

写数据时

limit 表示可以对 Buffer 最多写入多少个数据。写模式下,limit 等于 Buffer 的 capactiy

读数据时

limit 表示 Buffer 里有多少可读数据(not null 的数据),因此能读取到之前写入的所有数据(limit 被设置为已写数据的数量,这个值在写模式下就是 position)

常用方法

分配初始大小

  • alloacte(int size)
ByteBuffer buf = ByteBuffer.alloacte(48);

写入数据

  • 调用 channel 的 read 方法,从 channel 写到 Buffer
int byteRead = channel.read(buf); // read into buffer
  • 调用 buffer 自己的 put 方法,通过 Buffer 的 put 方法写到 Buffer 里
buf.put(100);

flip() 方法

flip 方法将 Buffer 从写模式切换到读模式。调用 flip() 方法将会 position 设置为 0 , 并且将 limt 设置为之前 position 的值。

position 现在用于标记读的位置,limit 表示之前写入了多少个 byte, char 等(现在能读取多少个 byte, char 等)。

读取数据

  • 调用 channel 的 write 方法,从 Buffer 中读取数据到 Channel
int bytesWritten = inChannel.write(buf);
  • 调用 buffer 自己的 get 方法,
byte aByte = buf.get();

get 方法会让 position 读指针向后走,如果想重复读取数据

    • 可以调用 rewind 方法将 position 重新置为 0
    • 或者调用 get(int i) 方法获取索引 i 的内容,它不会移动读指针

rewind() 方法

Buffer.rewind() 将 position 返回0, 所以你可以重读 Buffer 中的所有数据。limit 保持不变,仍然表示能从 Buffer 中读取到多少个元素(byte, char 等)

clear() 与 compact() 方法

一旦读完 Buffer 中的数据,需要让 Buffer 准备好再次被写入。可以通过 clear() 或 compact() 方法来完成:

clear()

  • 如果调用的是 clear () 方法,position 兼备设置为 0 , limit 被设置成 capactiy 的值。换句话说,Buffer 被清空了。 Buffer 中的数据并未清除,只是这些标记告诉我们从哪里开始往 Buffer 中写数据。
  • 如果 Buffer 中有些数未读的数据,调用 clear() 方法,数据将 “被遗忘”,意味着不在有任何标记会告诉你那些数据被读过,那些还没有。

compact()

  • 如果 Buffer 中依然有未读的数据,且后续还需要这些数据,但是此时想要先写这些数据,那么使用 compact() 方法。
  • compact() 方法将所有未读的数据拷贝到 Buffer 起始处。然后将 position 设置到最后一个未读元素正后面。 limit 属性依然像 clear() 方法一样。设置成 capacity. 现在 Buffer 准备好写数据了,但是不会覆盖未读的数据。

mark() 和 reset() 方法

mark() 是在读取时,做一个标记,即使 position 改变,只要调用 reset() 就能回到 mark() 的位置

rewind() 和 flip() 都会清除 mark 位置

equals()和compareTo()

equals()

  • 两个Buffer相等的条件:
    • 有相同的类型. 比如byte,char,int
    • Buffer中剩余的byte,char等元素个数相等
    • Buffer中所有剩余的byte,char等元素都相同
  • equals() 只是比较Buffer的一部分,不是Buffer的每一个元素都比较.只会比较Buffer中的剩余元素,也就是从position到limit之间的元素

compareTo()

  • compareTo() 方法比较两个Buffer的剩余的byte,char等元素,也就是从position到limit之间的元素
  • 一个Buffer小于另一个Buffer的条件:
    • 第一个不相等的元素小于另一个Buffer中对应的元素
    • 所有的元素都相等,但是第一个Buffer的元素个数比另一个少

Channel

全双工,比流更好的映射底层操作系统的 API,与流的区别在于

  • 既可以通道汇总读取数据,又可以写数据到通道,但流的读写通常是单向的。
  • 通道可以异步地读写。
  • 通道中的数据总是先读取到 buffer , 或者总是需要从一个 buffer 写入。

实现方式

  • FileChannel ,从文件中读写数据
  • DatagramChannel ,通过 UDP 读写网络中的数据
  • SocketChannel ,通过 TCP 写网络中的数据
  • ServerSocketChannel ,监听新进来的 TCP 连接,对每个新进来的连接都会创建一个 SocketChannel

FileChannel

使用步骤:打开 FileChannel,从 FileChannel 中读取数据,向 FileChannel 写数据,关闭 FileChannel

DatagramChannel

使用步骤:打开 DatagramChannel,绑定端口,通过 receive() 接受 UDP 包,通过 send() 发送 UDP 包,connect() 连接

Selector

本质:I/O多路复用,单个selector轮询多个channel

I/O多路复用:使用更少的线程就可以来处理通道了,相比使用多个线程,避免了线程上下文切换带来的开销

SelectableChannel(可选择通道)

  • 只有继承了一个抽象类 SelectableChannel才可以被复用,否则不能,如FileChannel就没有继承
  • 所有 socket 通道,都继承 SelectableChannel 类都是可选择的,
  • 通道和选择器之间的关系,使用注册的方式完成。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。SelectableChannel 可以被注册到 Selector 对象上,在注册时候,需要指定通道的哪些操作,是 Selector 感兴趣的。

注册过程

Channel.register(Selector sel, int pos) 方法,将一个通道注册到一个选择器。

  • 第一个参数,指定通道要注册的选择器
  • 第二个参数指定选择器需要查询的通道操作
    • 可读:SelectionKey.OP_READ
    • 可写:SelectionKey.OP_WRITE
    • 连接:SelectionKey.OP_CONNECT
    • 接收:SelectionKey.OP_ACCEPT
  • 选择器查询的不是通道的操作,而是通道的某个操作的一种就绪状态。即一旦通道具备完成某个操作的条件,表示该通道的某个操作已经就绪,就可以被 Selector 查询到,程序可以对通道进行对应的操作。

SelectionKey(选择键)

  • Channel 注册之后,并且一旦通道处于某种就绪状态,就可以被选择器查询到。select 方法的作用,对感兴趣的通道操作,进行就绪状态的查询。
  • Selector 可以不断的查询 Channel 中发生的操作的就绪状态。并且选择感兴趣的操作就绪状态。一旦通道有操作的就绪状态达成,并且是 Selecor 感兴趣的操作,就会被 Selector 选中,放入选择键集合中。
  • 一个选择键,首先包含了注册在 Selector 的通道操作的类型,比方说: SelectionKey.OP_READ也包含了特定的通道与特定的选择器之间的注册关系。
  • 选择键的概念,和事件的概念比较相似。一个选择键类似监听器模式里面的一个事件。由于 Selector 不是事件触发的模式,而是主动去查询的模式,所以不叫事件 Event, 而是叫 SelectionKey 选择键。

实现方式

  • 通过 Selector.open() 方法创建一个 Selector 对象
  • 调用通道的 register() 方法会将它注册到一个选择器上
  • 通过 Selector 的 select() 方法, 可以查询出已经就绪的通道操作
  • 停止选择
    • 通过调用 Selector 对象的 wakeup() 方法让处于阻塞状态的 select() 方法立刻返回
    • 通过 close() 方法关闭 selector

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值