NIO组件Buffer,Channel和Selector

流是用来读写数据的。所有 I/O 都被视为单个的字节的移动,通过一个称为 Stream 的对象一次移动一个字节。流 I/O 用于与外部世界接触。它也在内部使用,用于将对象转换为字节,然后再转换回对象。

流与与 NIO 最重要的区别是数据打包和传输的方式,原来的 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。

流与块的比较

  • 面向流 的 I/O 系统一次一个字节地处理数据,我们很容易将其包装为处理流,完成想要的工作
  • 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性

一、Buffer

NIO中数据都是从通道读入缓冲区,从缓冲区写入到通道中的

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存

1. NIO中的Buffer有以下实现:
  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

这些Buffer类型代表了不同的数据类型。换句话说,就是可以通过char,short,int,long,float 或 double类型来操作缓冲区中的字节

最核心的是ByteBuffer,前面的一大串类只是包装了一下它而已,我们使用最多的通常也是 ByteBuffer,可以将Buffer理解为一个数组,IntBuffer、CharBuffer、DoubleBuffer 等分别对应 int[]、char[]、double[] 等

2. Buffer的重要属性:
  • position
  • limit
  • capacity

capacity代表缓冲区的容量,不可更改。比如 capacity 为 1024 的 IntBuffer,代表其一次可以存放 1024 个 int 类型的值。一旦 Buffer 的容量达到 capacity,需要清空 Buffer,才能重新写入值

首先,对读写操作这个概念先解释一下,否则可能会混淆。
在系统层面:

  • 读操作,从Channel读取数据到Buffer
  • 写操作,将数据从Buffer写入到Channel

对于Buffer而言:

  • 读操作,就是从Buffer起始位置读取它的数据
  • 写操作,就是向Buffer中写入或者说填充数据

下面读写操作中观察position和limit是站在Buffer角度而言的

position,代表下一次的写入位置。初始值是 0,每往 Buffer 中写入一个值,position 就自动加 1,每读一个值,position 就自动加 1

从写操作模式到读操作模式切换的时候(flip),position 都会归零

limit,写操作模式下,limit 代表的是最大能写入的数据。这个时候 limit 等于 capacity。写结束后,切换到读模式,此时的 limit 等于 Buffer 中实际的数据大小,因为 Buffer 不一定被写满了

3. Buffer对象分配

初始化Buffer对象

ByteBuffer byteBuf = ByteBuffer.allocate(1024);
IntBuffer intBuf = IntBuffer.allocate(1024);

向Buffer中写数据:

  • 通过Buffer的put()方法写到Buffer里
  • 从Channel写到Buffer

对于put方法,有以下函数定义

// 填充一个 byte 值
public abstract ByteBuffer put(byte b);
// 在指定位置填充一个 int 值
public abstract ByteBuffer put(int index, byte b);
// 将一个数组中的值填充进去
public final ByteBuffer put(byte[] src) {...}

这些方法需要自己控制 Buffer 大小,不能超过 capacity,超过会抛 java.nio.BufferOverflowException 异常

使用Channel写入数据到Buffer,在系统层面上,这个操作我们称为读操作,因为数据是从外部(文件或网络等)读到内存中

int bytesRead = inChannel.read(buf); //这里会返回写入Buffer数据的大小

从Buffer中读取数据。如果要读 Buffer 中的值,需要切换模式,从写入模式切换到读出模式,flip方法将Buffer从写模式切换到读模式,flip()方法会将position设回0,并将limit设置成之前position的值,也就是Buffer中实际数据大小

public final Buffer flip() {
    limit = position; // 将 limit 设置为实际写入的数据数量
    position = 0; // 重置 position 为 0
    mark = -1; 
    return this;
}
  1. 使用get()方法从Buffer中读取数据
  2. 从Buffer读取数据到Channel

get方法重载多个,允许以不同的方式从Buffer中读取数据

// 根据 position 来获取数据
public abstract byte get();
// 获取指定位置的数据
public abstract byte get(int index);
// 将 Buffer 中的数据写入到数组中
public ByteBuffer get(byte[] dst)

byte aByte = buf.get();

例如可以通过 FileChannel 将数据写入到文件中,通过 SocketChannel 将数据写入网络发送到远程机器等

int bytesWritten = channel.write(buf);
4. 相关重要方法
mark() and reset()

mark 用于临时保存 position 的值,每次调用 mark() 方法都会将 mark 设值为当前的 position,便于后续需要的时候使用。后面可以通过调用Buffer.reset()方法恢复到这个position

public final Buffer mark() {
    mark = position;
    return this;
}

public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}
rewind() clear() compact()

rewind(): 会重置 position 为 0,通常用于重新从头读写 Buffer

public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }

clear(): 重置 Buffer,相当于重新实例化,position将被设回0,limit被设置成 capacity的值,注意clear() 方法并不会将 Buffer 中的数据清空,只不过后续的写入会覆盖掉原来的数据,也就相当于清空了数据了

public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

compact(): 先处理还没有读取的数据,也就是 position 到 limit 之间的数据(还没有读过的数据),将所有未读的数据拷贝到Buffer起始处,然后将position设到最后一个未读元素的下一位置,在这个基础上再开始写入。此时 limit 还是等于 capacity,写入新数据不会覆盖原来数据

二、Channel

Channel是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流。Channel与Buffer交互,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中

NIO实现的Channel:

  • FileChannel:文件通道,用于文件的读和写
  • DatagramChannel:用于 UDP 连接的接收和发送
  • SocketChannel:把它理解为 TCP 连接通道,简单理解就是 TCP 客户端
  • ServerSocketChannel:TCP 对应的服务端,用于监听某个端口进来的请求

Channel使用

channel.read(buffer);
channel.write(buffer);
FileChannel

FileChannel 是不支持非阻塞
初始化

//使用流获取通道
FileInputStream inputStream = new FileInputStream(new File("/data.txt"));
FileChannel fileChannel = inputStream.getChannel();

//或者使用RandomAccessFile
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

读取文件内容到Buffer

ByteBuffer buffer = ByteBuffer.allocate(1024);

int num = fileChannel.read(buffer);

写入文件内容到Channel

ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("随机写入一些内容到 Buffer 中".getBytes());
// Buffer 切换为读模式
buffer.flip();
while(buffer.hasRemaining()) {
    // 将 Buffer 中的内容写入文件
    fileChannel.write(buffer);
}
SocketChannel ServerSocketChannel

TCP 连接通道

// 打开一个通道
SocketChannel socketChannel = SocketChannel.open();
// 发起连接
socketChannel.connect(new InetSocketAddress("https://www.javadoop.com", 80));

// 实例化
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 监听 8080 端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));

while (true) {
   // 一旦有一个 TCP 连接进来,就对应创建一个 SocketChannel 进行处理
   SocketChannel socketChannel = serverSocketChannel.accept();
}

ServerSocketChannel 不和 Buffer 打交道了,因为它并不实际处理数据,它一旦接收到请求后,实例化 SocketChannel,之后在这个连接通道上的数据传递它就不管了,因为它需要继续监听端口,等待下一个连接

三、Selector

选择器,Selector是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。一个单独的线程可以管理多个channel,从而管理多个网络连接。Selector是注册对各种 I/O 事件的兴趣的地方,而且当那些事件发生时,就是这个对象告诉您所发生的事件

//开启Selector
Selector selector = Selector.open();
// 将通道设置为非阻塞模式,因为默认都是阻塞模式的
channel.configureBlocking(false);

//将通道注册到Selector上
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

register 方法的第二个 int 型参数(使用二进制的标记位)用于表明需要监听哪些感兴趣的事件,共以下四种事件:

  • SelectionKey.OP_READ,对应 00000001,通道中有数据可以进行读取
  • SelectionKey.OP_WRITE,对应 00000100,可以往通道中写入数据
  • SelectionKey.OP_CONNECT,对应 00001000,成功建立 TCP 连接
  • SelectionKey.OP_ACCEPT,对应 00010000,接受 TCP 连接

可以同时监听一个 Channel 中的发生的多个事件,比如我们要监听 ACCEPT 和 READ 事件,那么指定参数为二进制的 00010001 即十进制数值 17 即可

注册方法返回值是 SelectionKey 实例,它包含了以下内容

  • Channel
  • Selector
  • Interest Set ,即我们设置的我们感兴趣的正在监听的事件集合
  • ready集合,即返回对应集合的boolean值
相关方法

select()方法
select()方法返回的int值表示有多少通道已经就绪。调用此方法,会将上次 select 之后的准备好的 channel 对应的 SelectionKey 复制到 selected set 中。如果没有任何通道准备好,这个方法会阻塞,直到至少有一个通道准备好

selectNow()
功能和 select 一样,区别在于如果没有准备好的通道,那么此方法会立即返回 0

select(long timeout)
如果没有通道准备好,此方法会等待一会

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值