Java NIO 由以下几个核心部分组成:

  • Channels
  • Buffers
  • Selectors

channel与buffer

Channel

  • FileChannel:从文件中读写数据
  • DatagramChannel:能通过UDP读写网络中的数据
  • SocketChannel:能通过TCP读写网络中的数据
  • ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel

Buffer

使用Buffer读写数据一般遵循以下四个步骤:

  1. 写入数据到Buffer

    1
    2
    3
    4
    
    // 读取channel中的数据到buf中
    int bytesRead = inChannel.read(buf);
    // 直接向buf写入数据
    buf.put(127);
    
  2. 调用flip()方法

  3. 从Buffer中读取数据

    1
    2
    3
    4
    
    // 将buf中的数据写入到channel
    int bytesWritten = inChannel.write(buf);
    // 直接从buf读取数据
    byte aByte = buf.get();
    
  4. 调用clear()方法或者compact()方法

Buffer的capacity,position和limit

screenshot-20210617004210

capacity

容量

position

当将Buffer从写模式切换到读模式,position会被重置为0。当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

limit

写模式下,limit等于Buffer的capacity;读模式下,limit表示最多能读到的位置。当切换Buffer到读模式时,limit会被设置成写模式下的position值。

Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。

通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。

Scatter/Gather

分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。

1
channel.read(bufferArray);

聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。

1
channel.write(bufferArray);

Selector

通过调用Selector.open()方法创建一个Selector:

1
2
/ 创建Selector
Selector selector = Selector.open();

为了将Channel和Selector配合使用,必须将channel注册到selector上:

1
2
3
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

注意register()方法的第二个参数。这是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:

  1. Connect
  2. Accept
  3. Read
  4. Write

如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来。

可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。

1
2
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

select()方法返回的int值表示有多少通道已经就绪。亦即,自上次调用select()方法后有多少通道变成就绪状态。

selectedKeys()

一旦调用了select()方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用selector的selectedKeys()方法,访问“已选择键集(selected key set)”中的就绪通道。可以遍历这个已选择的键集合来访问就绪的通道。

注意每次迭代末尾的keyIterator.remove()调用。Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。

SelectionKey.channel()方法返回的通道需要转型成你要处理的类型,如ServerSocketChannelSocketChannel等。

Pipe

Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 创建管道
Pipe pipe = Pipe.open();

// sink通道
Pipe.SinkChannel sink = pipe.sink();

// 准备缓冲区,并向缓冲区写入数据
ByteBuffer buf = ByteBuffer.allocate(1024);
// bytes -> buf
buf.put("hello world!".getBytes());
buf.flip();
//          buf -> channel
while (buf.hasRemaining()) {
    sink.write(buf);
}

// source通道
Pipe.SourceChannel source = pipe.source();
// channel -> buf
buf.clear();
// 读取数据
source.read(buf);
buf.flip();
//            buf -> print
while (buf.hasRemaining()) {
    System.out.print((char) buf.get());
}

参考