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 和 limitArrays.asList(byteBuffers).stream().map(buffer -> "position = " + buffer.position() + ", limit = " + buffer.limit()).forEach(System.out::println);}//将所有的 buffer 进行 flipArrays.asList(byteBuffers).forEach(buffer -> buffer.flip());//将数据读出显示到客户端long byteWirte = 0;while (byteWirte < messageLength) {long l = socketChannel.write(byteBuffers);byteWirte += l;}//将所有的buffer进行clearArrays.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介绍》