感兴趣的朋友可以去我的语雀平台进行查看更多的知识。
https://www.yuque.com/ambition-bcpii/muziteng
1. NIO 概述
1.1 IO 概述
IO 的操作方式通常分为几种:同步阻塞 BIO、同步非阻塞 NIO、异步非阻塞 AIO。
-
在 JDK1.4 之前,我们建立网络连接的时候采用的是 BIO 模式
-
Java NIO(New IO 或 Non Blocking IO)是从 Java 1.4 版本开始引入的一个新的 IO API,可以替代标准的 Java IO API。NIO 支持面
向缓冲区的、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。BIO 与 NIO 一个比较重要的不同是,我们使用
BIO 的时候往往会引入多线程,每个连接对应一个单独的线程;而 NIO 则是使用单线程或者只使用少量的多线程,让连接共用一个线
程。
-
AIO 也就是 NIO 2,在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。
1.2 阻塞 IO (BIO)
阻塞 IO(BIO)是最传统的一种 IO 模型,即在读写数据过程中会发生阻塞现象,直至有可供读取的数据或者数据能够写入。
特点:
- 服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求,当客户较多时,使得占用资源较大。
- 虽然可以使用线程池,但是当线程池中的线程都被占用时,再有客户端请求连接时,也会导致没有线程来进行处理。
模型图:
1.3 非阻塞 IO(NIO)
NIO 采用非阻塞模式,基于 Reactor 模式的工作方式,I/O 调用不会被阻塞。
它的实现过程是:会先对每个客户端注册感兴趣的事件,然后有一个线程专门去轮询每个客户端是否有事件发生,当有事件发生时,便顺
序处理每个事件,当所有事件处理完之后,便再转去继续轮询,如下图所示:
NIO 中实现非阻塞 I/O 的核心对象就是 Selector
,Selector 就是注册各种 I/O 事件地方,而且当我们感兴趣的事件发生时,就是这个对
象告诉我们所发生的事件,如下图所示:
NIO 的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,一个选择器线程可以同时处理成
千上万个连接,系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。
1.4 异步非阻塞 IO(AIO)
异步 IO 是基于事件和回调机制实现的,也就是说 AIO 模式不需要selector 操作,而是是事件驱动形式,也就是当客户端发送数据之后,
会主动通知服务器,接着服务器再进行读写操作。
1.5 NIO 核心组件
Java NIO 由以下几个核心部分组成:
- Channel
- Buffer
- Selector
1.5.1 Channel
首先说一下 Channel,可以翻译成“通道”。Channel 和 IO 中的 Stream(流)是差不多一个等级的。只不过 Stream 是单向的,譬如:
InputStream, OutputStream。而 Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作,**可以从 channel 将数据读入 **
buffer,也可以将 buffer 的数据写入channel
常见的 Channel 有
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
ServerSocketChannel 可以监听新进来的 TCP 连接,像 Web 服务器那样。对每一个新进来的连接都会创建一个 SocketChannel
1.5.2 Buffer
Buffer 则用来缓冲读写数据,常见的 buffer 有
- ByteBuffer
- MappedByteBuffer
- DirectByteBuffer
- HeapByteBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
- CharBuffer
1.5.3 Selector
Selector 运行单线程处理多个 Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用 Selector 就会很方便。例如在
一个聊天服务器中。要使用 Selector, 得向 Selector 注册 Channel,然后调用它的 select()方法。这个方法会一直阻塞到某个注册的通
道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等
1.5.4 三者关系
- 一个 Channel 就像一个流,只是 Channel 是双向的,Channel 读数据到 Buffer,Buffer 写数据到 Channel
- 一个 selector 允许一个线程处理多个 channel
2. FileChannel
2.1 Channel 概述
Java NIO 的通道类似流,但又有些不同
- 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
- 通道可以异步地读写。
- 通道中的数据总是要先读到一个 Buffer,或者总是要从一个 Buffer 中写入
从通道读取数据到缓冲区,从缓冲区写入数据到通道
2.2 FileChannel详解
常用方法
read() 方法返回的 int 值表示了有多少字节被读到了 Buffer 中。如果返回-1,表示到了文件末尾
2.2.1 从 FileChannel 读取数据
try (FileChannel channel = new RandomAccessFile("data.txt", "rw").getChannel()) {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 1024);
channel.read(byteBuffer);
} catch (Exception e) {
throw new RuntimeException(e);
}
2.2.2 向 FileChannel 写入数据
try (FileChannel channel = new RandomAccessFile("data.txt", "rw").getChannel()) {
ByteBuffer byteBuffer = ByteBuffer.allocate(48);
String newData = "New String to write to file..." + System.currentTimeMillis();
byteBuffer.put(newData.getBytes(StandardCharsets.UTF_8));
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
channel.write(byteBuffer);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
注意,用完 FileChannel 后需要将 FileChannel 进行关闭
2.2.3 其他方法
position
有时可能需要在 FileChannel 的某个特定位置进行数据的读/写操作。可以通过调用position()
方法获取 FileChannel 的当前位置。也可
以通过调用 position(long pos)
方法设置 FileChannel 的当前位置。
size
FileChannel 实例的 size()方法将返回该实例所关联文件的大小。
truncate
可以使用 FileChannel.truncate()方法截取一个文件。截取文件时,文件将中指定长度后面的部分将被删除。
force
FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。
force()方法有一个 boolean 类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。\
transferTo 和 transferFrom
这两个方法用来通道之间的数据传输。如果两个通道中有一个是 FileChannel,那你可以直接将数据从一个 channel 传输到另外一个
channel
FileChannel 的 transferFrom()
方法可以将数据从源通道传输到 FileChannel 中
transferTo()
方法将数据从 FileChannel 传输到其他的 channel 中
try (FileChannel from = new RandomAccessFile("data.txt", "rw").getChannel();
FileChannel to = new RandomAccessFile("out.txt", "rw").getChannel()) {
from.transferTo(0, from.size(), to);
} catch (Exception e) {
e.printStackTrace();
}
2.3 Scatter/Gather
Java NIO 开始支持 scatter/gather,scatter/gather 用于描述从 Channel 中读取或者写入到 Channel 的操作。
分散(scatter)从 Channel 中读取是指在读操作时将读取的数据写入多个 buffer 中。因此,Channel 将从 Channel 中读取的数据“分
散(scatter)”到多个 Buffer 中。
聚集(gather)写入 Channel 是指在写操作时将多个 buffer 的数据写入同一个Channel,因此,Channel 将多个 Buffer 中的数据“聚
集(gather)”后发送到 Channel。
scatter / gather 经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头
分散到不同的 buffer 中,这样你可以方便的处理消息头和消息体。
2.3.1 Scattering Reads
Scattering Reads 是指数据从一个 channel 读取到多个 buffer 中
read()方法按照 buffer 在数组中的顺序将从 channel 中读取的数据写入到 buffer,当一个 buffer 被写满后,channel 紧接着向另一个
buffer 中写。
Scattering Reads 在移动下一个 buffer 前,必须填满当前的 buffer,这也意味着它不适用于动态消息(译者注:消息大小不固定)。换句话
说,如果存在消息头和消息体,消息头必须完成填充(例如 128byte),Scattering Reads 才能正常工作。
public static void main(String[] args) {
ByteBuffer byteBuffer1 = ByteBuffer.allocate(128);
ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024);
ByteBuffer[] byteBuffers = {byteBuffer1, byteBuffer2};
try (FileChannel channel = new RandomAccessFile("data.txt", "rw").getChannel()) {
channel.read(byteBuffers);
} catch (Exception e) {
e.printStackTrace();
}
byteBuffer1.flip();
byteBuffer2.flip();
System.out.println(Charset.defaultCharset().decode(byteBuffer1));
System.out.println("==========");
System.out.println(Charset.defaultCharset().decode(byteBuffer2));
}
输出:
data.txt文件如下:共129个字符
1234567890abcdasdasdasdasdasdasdasdasdasdasdasdasddgjsknfdkgljnsdfkljgnkjsfdbgkahjdbfkljabdfdFKJCLbnkljzshdbfljsndfkjnlzjkdfnsZDf
输出:
1234567890abcdasdasdasdasdasdasdasdasdasdasdasdasddgjsknfdkgljnsdfkljgnkjsfdbgkahjdbfkljabdfdFKJCLbnkljzshdbfljsndfkjnlzjkdfnsZD
==========
f
2.3.2 Gathering Writes
Gathering Writes 是指数据从多个 buffer 写入到同一个 channel。
buffers 数组是 write()方法的入参,write()方法会按照 buffer 在数组中的顺序,将数据写入到 channel,注意**只有 position 和 limit **
之间的数据才会被写入。因此,如果一个 buffer 的容量为 128byte,但是仅仅包含 58byte 的数据,那么这 58byte 的数据将被写入到
channel 中。因此与 Scattering Reads 相反,Gathering Writes 能较好的处理动态消息
public static void main(String[] args) {
ByteBuffer byteBuffer1 = ByteBuffer.allocate(128);
ByteBuffer byteBuffer2 = ByteBuffer.allocate(128);
byteBuffer1.put("Hi".getBytes(StandardCharsets.UTF_8));
byteBuffer2.put("Hello".getBytes(StandardCharsets.UTF_8));
ByteBuffer[] byteBuffers = {byteBuffer1, byteBuffer2};
try (FileChannel channel = new FileOutputStream("out.txt").getChannel()) {
byteBuffer1.flip(); // 切换为读模式
byteBuffer2.flip();
channel.write(byteBuffers);
System.out.println(channel.size()); // 7
} catch (Exception e) {
e.printStackTrace();
}
}
3. SocketChannel
3.1 概述
SocketChannel 就是 NIO 对于非阻塞 socket 操作的支持的组件,其在 socket 上封装了一层,主要是支持了非阻塞的读写。同时改进了
传统的单向流 API,,Channel同时支持读写。
3.2 ServerSocketChannel
ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉的 java.net.ServerSocket 执行相同的任务,不过它增加了通
道语义,因此能够在非阻塞模式下运行。
同 java.net.ServerSocket 一样,ServerSocketChannel 也有 accept( )方法。ServerSocketChannel 的 accept()方法会返回
SocketChannel 类型对象,SocketChannel 可以在非阻塞模式下运行。
非阻塞下的 accept() 演示:
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 设置非阻塞模式
ssc.bind(new InetSocketAddress(8080));
ByteBuffer byteBuffer = ByteBuffer.wrap("Hello".getBytes(StandardCharsets.UTF_8));
while (true) {
SocketChannel sc = ssc.accept(); // 监听新的连接
if (sc == null) {
System.out.println("null.....");
Thread.sleep(2000);
} else {
System.out.println("Incoming connection from:" + sc.getRemoteAddress());
sc.write(byteBuffer);
sc.close();
}
}
3.3 SocketChannel
Java NIO 中的 SocketChannel 是一个连接到 TCP 网络套接字的通道。
SocketChannel 是一种面向流连接 sockets 套接字的可选择通道,有以下特点
-
SocketChannel 是用来连接 Socket 套接字
-
SocketChannel 主要用途用来处理网络 I/O 的通道
-
SocketChannel 是基于 TCP 连接传输
-
SocketChannel 实现了可选择通道,可以被多路复用的
-
SocketChannel 支持两种 I/O 模式:阻塞式和非阻塞式
演示
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost", 8080));
sc.configureBlocking(false); // 设置未非阻塞模式
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
// SocketChannel 收到数据写入 Buffer中
sc.read(byteBuffer); // 当为阻塞模式时,read 执行时会导致线程阻塞
sc.close();
System.out.println("Over");
3.4 DatagramChannel
DatagramChannel 则模拟包导向的无连接协议(如 UDP/IP)。DatagramChannel 是无连接的,每个数据报(datagram)都是一个自
包含的实体,拥有它自己的目的地址及不依赖其他数据报的数据负载。与面向流的的 socket 不同,DatagramChannel 可以发送单独的
数据报给不同的目的地址。同样,DatagramChannel 对象也可以接收来自任意地址的数据包。每个到达的数据报都含有关于它来自何处
的信息(源地址)。
DatagramChannel server = DatagramChannel.open();
server.socket().bind(new InetSocketAddress(8898)); // 绑定端口
System.out.println("waiting...");
ByteBuffer buffer = ByteBuffer.allocate(32);
// SocketAddress 可以获得发包的 ip、端口等信息
SocketAddress socketAddress = server.receive(buffer);// 接收数据
ByteBuffer sendBuffer = ByteBuffer.wrap("Hello".getBytes(StandardCharsets.UTF_8));
server.send(sendBuffer, new InetSocketAddress("localhost", 8898)); // 发送数据
// UDP 不存在真正意义上的连接,这里的连接是向特定服务地址用 read 和 write 接收发送数据包
// read()和 write()只有在 connect()后才能使用,不然会抛NotYetConnectedException 异常
server.connect(new InetSocketAddress("localhost", 8080));
server.write(sendBuffer);
演示:
@Test
public void sendDatagram() throws Exception {
DatagramChannel sendChannel = DatagramChannel.open();
while (true) {
sendChannel.send(ByteBuffer.wrap("发包".getBytes(StandardCharsets.UTF_8))
, new InetSocketAddress("localhost", 9999));
System.out.println("发包端发包");
Thread.sleep(1000);
}
}
@Test
public void receive() throws Exception {
DatagramChannel receiveChannel = DatagramChannel.open();
receiveChannel.bind(new InetSocketAddress(9999));
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
while (true) {
SocketAddress socketAddress = receiveChannel.receive(byteBuffer);
byteBuffer.flip();
System.out.println(socketAddress + " ");
System.out.println(Charset.defaultCharset().decode(byteBuffer));
}
}