1. 核心部分
- Buffer(缓冲区):缓存数据;
- Channel(管道):传输 Buffer 中的数据;
- Selector(选择器):通过检查一个或多个Channel的状态,判断是否处于可读、可写的状态,来实现单线程管理多个Channel。
1.1 Buffer
Buffer类维护了4个核心变量属性来提供关于其所包含的数组的信息:
其中 0 <= mark <= position <= limit <= capacity
- capacity(容量):Buffer能够容纳的数据元素的最大数量,底层是数组,在Buffer创建时被设定;
- position(位置):初始为0。当写数据时,表示当前位置写的位置,最大可为capacity-1;当读数据时,需要将Buffer从写模式切换成读模式,重置为0;
- limit(上界):缓冲区里的数据的总数。当写数据时,表示能容纳多少数据,limit等于capacity;当读数据时,需要将Buffer从写模式切换成读模式,表示能读取多少数据,limit等于写数据时的position;
- mark(标记):一个备忘位置。用于记录上一次读写的位置,包含pos,limit 和 cap 的值。
写模式
capacity和limit值相等,指向最大数量。通过改变position的值,记录写入位置。
读模式
通过 filp() 切换成读模式。limit指向position的位置,position指向头部,读取buffer中的值。
1.2 Channel
Channel类似流,但又有些不同:
- 可以从一个Channel中读取数据,再写入到另一个Channel中。而流的读写通常是单向的;
- Channel可以异步地读写;
- Channel中的数据总是要先读到一个Buffer,或者写入一个Buffer。
Channel负责传输数据,不直接操作数据的。操作数据都是通过Buffer来进行操作。
操作:从Channel读取数据后写入到Buffer,再从Buffer读取数据后写入到Channel。
非直接与直接缓冲区
非直接缓冲区需要经过一个 copy 的阶段的(从内核空间copy到用户空间);直接缓冲区不需要经过 copy 阶段,可理解成内存映射文件。
分散读取与聚集写入
- 分散读取(scatter):将一个通道中的数据分散读取并写入到多个缓冲区中;
- 聚集写入(gather):将多个缓冲区中的数据集中写入到一个通道中。
1.3 Selector
Selector 是选择器,可理解成多路复用器,能够仅用单线程来处理多个Channel。它通过检查一个或多个Channel的状态,判断是否处于可读、可写的状态,来实现单线程管理多个Channel,也就是可以管理多个网络连接。
2. 测试
2.1 文件传输
传统IO
@Test
public void transferFileByIO() {
File inputFile = new File("D:\\test.pdf");
File outputFile = new File("D:\\Downloads\\test_1.pdf");
try (
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(inputFile));
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(outputFile));
){
int len;
byte[] buffer = new byte[1024];
while ((len = bufferedInputStream.read(buffer)) != -1){
bufferedOutputStream.write(buffer,0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
}
NIO
@Test
public void transferFileByNio(){
File inputFile = new File("D:\\test.pdf");
File outputFile = new File("D:\\Downloads\\test_2.pdf");
try (
RandomAccessFile inputRandomAccessFile = new RandomAccessFile(inputFile, "rw");
RandomAccessFile outputRandomAccessFile = new RandomAccessFile(outputFile, "rw");
FileChannel inputFileChannel = inputRandomAccessFile.getChannel();
FileChannel outputFileChannel = outputRandomAccessFile.getChannel();
){
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while(inputFileChannel.read(byteBuffer) > 0) {
byteBuffer.flip();
outputFileChannel.write(byteBuffer);
byteBuffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
2.2 网络通信
服务端
步骤:
- 将Channel注册到Selector中,监听Accept、Connect、Read和Write事件;
- Selector轮询Channel,当事件就绪时,进行处理;
- 处理完事件,删除该SelectionKey。
ServerSocketChannel 注册接收事件,成功接收后的 SocketChannel 注册读事件,Selector 监听注册使得事件,并进行处理。
public class NioServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 非阻塞模式
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8001));
Selector selector = Selector.open();
// 通道注册到选择器中,注册接收事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 轮询Channel,直到某个注册的channel有事件就绪
while(selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("服务端已连接:" + socketChannel.getRemoteAddress());
// 非阻塞
socketChannel.configureBlocking(false);
// 通道注册到选择器中,注册读事件
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while(socketChannel.read(byteBuffer) > 0) {
byteBuffer.flip();
System.out.println("客户端 " + socketChannel.getRemoteAddress() +" 说:" + new String(byteBuffer.array()));
byteBuffer.clear();
}
}
// 删除已处理事件
iterator.remove();
}
}
}
}
客户端
这里等待用户输入信息,然后发送给服务端。
public class NioClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8001));
// 非阻塞模式
socketChannel.configureBlocking(false);
while(true) {
Scanner scanner = new Scanner(System.in);
String line = scanner.nextLine();
socketChannel.write(ByteBuffer.wrap(line.getBytes()));
}
}
}
测试
参考:
Java NIO Tutorial
Java NIO Tutorial 翻译
如何学习Java的NIO?
Java NIO之Selector(选择器)
Java NIO浅析