NIO
Java NIO 基本介绍
Java NIO
全称Java non-blocking IO
,是指JDK
提供的新API
。从JDK1.4
开始,Java
提供了一系列改进的输入/输出的新特性,被统称为NIO
(即NewIO
),是同步非阻塞的。NIO
相关类都被放在java.nio
包及子包下,并且对原java.io
包中的很多类进行改写。NIO
有三大核心部分:Channel
(通道)、Buffer
(缓冲区)、Selector
(选择器) 。NIO
是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。- Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
- 通俗理解:
NIO
是可以做到用一个线程来处理多个操作的。假设有10000
个请求过来,根据实际情况,可以分配50
或者100
个线程来处理。不像之前的阻塞IO
那样,非得分配10000
个。 HTTP 2.0
使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP 1.1
大了好几个数量级。
NIO 三大核心原理示意图
一张图描述 NIO
的 Selector
、Channel
和 Buffer
的关系。
- 每个
Channel
都会对应一个Buffer
。 Selector
对应一个线程,一个线程对应多个Channel
(连接)。- 该图反应了有三个
Channel
注册到该Selector
//程序 - 程序切换到哪个
Channel
是由事件决定的,Event
就是一个重要的概念。 Selector
会根据不同的事件,在各个通道上切换。Buffer
就是一个内存块,底层是有一个数组。- 数据的读取写入是通过 Buffer,这个和 BIO是不同的,BIO 中要么是输入流,或者是输出流,不能双向,但是 NIO 的 Buffer 是可以读也可以写,需要 flip 方法切换 Channel 是双向的,可以返回底层操作系统的情况,比如 Linux,底层的操作系统通道就是双向的。
NIO 和 BIO 的比较
-
BIO
以流的方式处理数据,而NIO
以块的方式处理数据,块I/O
的效率比流I/O
高很多。 -
BIO
是阻塞的,NIO
则是非阻塞的。 -
BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
-
Buffer和Channel之间的数据流向是双向的
缓冲区(Buffer)
缓冲区(Buffer
):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个**容器对象(含数组)**该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel
提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer
,
通道(Channel)
NIO
的通道类似于流,但有些区别如下:
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读数据,也可以写数据到缓冲:
BIO
中的Stream
是单向的,例如FileInputStream
对象只能进行读取数据的操作,而NIO
中的通道(Channel
)是双向的,可以读操作,也可以写操作。Channel
在NIO
中是一个接口public interface Channel extends Closeable{}
- 常用的
Channel
类有:FileChannel
、DatagramChannel
、ServerSocketChannel
和SocketChannel
。【ServerSocketChanne
类似ServerSocket
、SocketChannel
类似Socket
】 FileChannel
用于文件的数据读写,DatagramChannel
用于UDP
的数据读写,ServerSocketChannel
和SocketChannel
用于TCP
的数据读写。
NIO
还支持通过多个 Buffer
(即 Buffer
数组)完成读写操作,即 Scattering
和 Gathering
【举例说明】
/**
* @Author:jiangdw7
* @date: 2023/8/9 10:04
*/
public class ScatteringAndGatheringTest {
public static void main(String[] args) throws Exception {
//使用 ServerSocketChannel 和 SocketChannel 网络
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
//绑定端口到 socket,并启动
serverSocketChannel.socket().bind(inetSocketAddress);
//创建 buffer 数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(5);
byteBuffers[1] = ByteBuffer.allocate(3);
//等客户端连接 (telnet)
SocketChannel socketChannel = serverSocketChannel.accept();
int messageLength = 8; //假定从客户端接收 8 个字节
//循环的读取
while (true) {
int byteRead = 0;
while (byteRead < messageLength) {
long l = socketChannel.read(byteBuffers);
byteRead += l; //累计读取的字节数
System.out.println("byteRead = " + byteRead);
//使用流打印,看看当前的这个 buffer 的 position 和 limit
Arrays.asList(byteBuffers).stream().map(buffer -> "position = " + buffer.position() + ", limit = " + buffer.limit()).forEach(System.out::println);
}
//将所有的 buffer 进行 flip
Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
//将数据读出显示到客户端
long byteWirte = 0;
while (byteWirte < messageLength) {
long l = socketChannel.write(byteBuffers);
byteWirte += l;
}
//将所有的buffer进行clear
Arrays.asList(byteBuffers).forEach(buffer -> {
buffer.clear();
});
System.out.println("byteRead = " + byteRead + ", byteWrite = " + byteWirte + ", messagelength = " + messageLength);
}
}
}
Selector(选择器)
Java
的NIO
,用非阻塞的IO
方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector
(选择器)。Selector
能够检测多个注册的通道上是否有事件发生(注意:多个Channel
以事件的方式可以注册到同一个Selector
),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。- 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
- 避免了多线程之间的上下文切换导致的开销。
注意事项
NIO
中的ServerSocketChannel
功能类似ServerSocket
、SocketChannel
功能类似Socket
。Selector
相关方法说明selector.select();
//阻塞selector.select(1000);
//阻塞 1000 毫秒,在 1000 毫秒后返回selector.wakeup();
//唤醒 selectorselector.selectNow();
//不阻塞,立马返还
public class NIOClient {
private static Selector selector;
public static void main(String[] args) throws Exception {
selector = Selector.open();
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress("127.0.0.1", 8081));
sc.register(selector, SelectionKey.OP_READ);
ByteBuffer bf = ByteBuffer.allocate(1024);
bf.put("Hi,server,i'm client".getBytes());
if (sc.finishConnect()) {
bf.flip();
while (bf.hasRemaining()) {
sc.write(bf);
}
while (sc.isConnected()) {
selector.select();
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
if (key.isReadable()) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bf.clear();
SocketChannel othersc = (SocketChannel) key.channel();
while (othersc.read(bf) > 0) {
bf.flip();
while (bf.hasRemaining()) {
bos.write(bf.get());
}
bf.clear();
}
System.out.println("服务端返回的数据:" + bos.toString());
Thread.sleep(5000);
sc.close();
System.out.println("客户端关闭...");
}
}
selector.selectedKeys().clear();
}
}
}
}
public class NIOServer {
private static Selector selector;
private static ServerSocketChannel serverSocketChannel;
private static ByteBuffer bf = ByteBuffer.allocate(1024);
public static void main(String[] args) throws Exception {
init();
while (true) {
int select = selector.select(10000);
if (select == 0) {
System.out.println("等待连接10秒...");
continue;
}
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
if (key.isAcceptable()) {
System.out.println("连接准备就绪");
ServerSocketChannel server = (ServerSocketChannel) key.channel();
System.out.println("等待客户端连接中........................");
SocketChannel channel = server.accept();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
System.out.println("读准备就绪,开始读.......................");
SocketChannel channel = (SocketChannel) key.channel();
System.out.println("客户端的数据如下:");
int readLen = 0;
bf.clear();
StringBuffer sb = new StringBuffer();
while ((readLen = channel.read(bf)) > 0) {
bf.flip();
byte[] temp = new byte[readLen];
bf.get(temp, 0, readLen);
sb.append(new String(temp));
bf.clear();
}
if (-1 == readLen) {
System.out.println(channel.hashCode()+"号客户端关闭。");
channel.close();
}else {
channel.write(ByteBuffer.wrap(("客户端,你传过来的数据是:" + sb.toString()).getBytes()));
System.out.println(sb.toString()+"132123");
}
}
it.remove();
}
}
}
private static void init() throws Exception {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(8081));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
}
参考连接
https://www.cnblogs.com/xdouby/p/8942083.html
https://www.zhihu.com/question/22524908
https://blog.csdn.net/ArtAndLife/article/details/121001656
AIO
-
JDK7
引入了AsynchronousI/O
,即AIO
。在进行I/O
编程中,常用到两种模式:Reactor
和Proactor
。Java
的NIO
就是Reactor
,当有事件触发时,服务器端得到通知,进行相应的处理 -
AIO
即NIO2.0
,叫做异步不阻塞的IO
。AIO
引入异步通道的概念,采用了Proactor
模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用 -
目前
AIO
还没有广泛应用,Netty
也是基于NIO
,而不是AIO
,因此我们就不详解AIO
了,有兴趣的同学可以参考《Java新一代网络编程模型AIO原理及Linux系统AIO介绍》