Java NIO 基本原理以及三大核心组件 |
您所在的位置:网站首页 › nio是什么品牌 › Java NIO 基本原理以及三大核心组件 |
I/O 模型
Java 共支持 3 种网络编程 I/O 模型:BIO、NIO、AIO。 Java BIO:同步阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理。Java AIO(NIO.2):异步非阻塞,AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。需要注意的是,Java 的 NIO 并不等同于操作系统层面上的 NIO,Java NIO 实际上是基于 IO 多路复用模型的,同时所用的 NIO 组件在 Linux 系统上是使用 epoll 系统调用实现的。这一点我一开始也弄混了,看了书才搞清楚。 BIO、NIO、AIO 使用场景分析 BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序简单易理解。NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4 开始支持。AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。 NIO 非阻塞I/O Java NIO 基本介绍 Java NIO 全称 Java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 NewIO),是同步非阻塞的。NIO 有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器) 。NIO 是面向缓冲区/块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配 50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个。HTTP 2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP 1.1 大了好几个数量级。NIO 是一种基于通道和缓冲区的 I/O 方式,它可以使用 native 函数库直接分配堆外内存(区别于 JVM 的运行时数据区),然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的直接引用进行操作。这样能在一些场景显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 在 Java NIO 中的通道(Channel)就相当于操作系统的内核空间(kernel space)的缓冲区,而缓冲区(Buffer)对应的相当于操作系统的用户空间(user space)中的用户缓冲区(user buffer)。 为什么使用 NIO传统 Socket 的 accept() 方法阻塞(等待客户端连接),输入流的 read() 方法阻塞(等待 OS 将数据从内核拷贝到用户空间)。也就是说 BIO 会让主线程进入阻塞状态,这就非常影响程序的性能,不能充分利用机器资源。但是这样就会有人提出疑问了,那我使用多线程不就可以了吗? 但是在高并发的情况下,会创建很多线程,线程会占用内存,线程之间的切换也会浪费资源开销。 而 NIO 只有在连接/通道真正有读写事件发生时(事件驱动),才会进行读写,就大大地减少了系统的开销。不必为每一个连接都创建一个线程,也不必去维护多个线程。避免了多个线程之间的上下文切换,导致资源的浪费。 NIO 和 BIO 的比较 BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。BIO 是阻塞的,NIO 则是非阻塞的。BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。Buffer 和 Channel 之间的数据流向是双向的。 NIO 三大核心组件在实现群聊系统之前,有必要介绍以下 Java NIO 的三大核心组件。分别是:Channel(通道),Buffer(缓冲区),Selector(选择器),也是 Reactor 模型在代码层面的体现。Selector 能让单线程同时处理多个客户端 Channel,非常适用于高并发,传输数据量较小的场景。 传统 IO 是基于字节流和字符流进行操作(面向流编程),而 NIO 基于 Channel 通道和 Buffer 缓冲区进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(如连接打开、数据到达)。因此单个线程可以监听多个数据通道。 图源:https://blog.csdn.net/leo187/article/details/116787166 每个 Channel 都会对应一个 Buffer。一个 Selector 对应一个线程,一个线程对应多个 Channel(连接)。该图反应了有三个 Channel 注册到该 Selector 。程序切换到哪个 Channel 是由事件决定的,Event 就是一个重要的概念。Selector 会根据不同的事件,在各个通道上切换。Buffer 就是一个内存块,底层是一个数组。NIO 数据的读取写入是通过 Buffer,这个和 BIO是不同的,BIO 中要么是输入流,要么是输出流,不能是双向的。但是 NIO 的 Buffer 可以读也可以写,需要 flip 方法切换。 Channel 是双向的,可以反映底层操作系统的情况,比如 Linux,底层的操作系统通道就是双向的。 缓冲区 BufferBuffer 缓冲区是一个用于存储特定基本类型数据的容器,缓冲区实质上是一个数组。该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。 在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类,除了 boolean 其他数据类型都有 Buffer: Channel 是一个对象,可以表示磁盘文件、Socket 套接字。Channel本身并不存储数据,只是负责数据的运输,当然所有数据都通过 Buffer 对象来处理。我们永远不会将字节直接写入通道,而是将数据写入包含一个或者多个字节的缓冲区。同样也不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。 Channel 与 Stream 流的区别BIO是面向流(Stream)编程的。NIO 的通道类似于流,但有些区别如下: Channel 可以同时支持读和写,而 Stream 只能支持单向的读或写(所以分成 InputStream 和 OutputStream)。 Channel 支持异步读写,Stream 通常只支持同步。 NIO 中的通道(Channel)是双向的,可以从缓冲读数据,也可以写数据到缓冲,当然还需要经过 Buffer。而 BIO 中的 Stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作。 常见的 Channel 有以下四种: FileChannel:读写文件中的数据。SocketChannel:通过 TCP 读写网络中的数据,类似 Socket。ServerSockectChannel:监听新进来的 TCP 连接,像 Web 服务器那样。对每一个新进来的连接都会创建一个 SocketChannel,类似 ServerSocket。DatagramChannel:通过 UDP 读写网络中的数据。 FileChannelFileChannel 主要用来对本地文件进行 IO 操作,常见的方法有: public int read(ByteBuffer dst),从通道读取数据并放到缓冲区中public int write(ByteBuffer src),把缓冲区的数据写到通道中public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道(零拷贝) public static void main(String[] args) throws Exception { File file = new File("D:\\file1.txt"); FileInputStream fileInputStream = new FileInputStream(file); FileOutputStream fileOutputStream = new FileOutputStream("D:\\file2.txt"); //获取输入流通道 FileChannel inputStreamChannel = fileInputStream.getChannel(); //获取输出流的通道 FileChannel outputStreamChannel = fileOutputStream.getChannel(); //创建一个缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); while (true) { //循环读取 byteBuffer.clear(); //重置标志位 //从通道读取数据写入缓冲区 int read = inputStreamChannel.read(byteBuffer); if (read == -1) break; //读取结束 //切换成读模式(Buffer既可读又可写) byteBuffer.flip(); //将buffer缓冲区数据写入通道channel outputStreamChannel.write(byteBuffer); } fileInputStream.close(); fileOutputStream.close(); }FileChannel 定义了 transferFrom() 和 transferTo() 两个抽象方法,它通过在通道和通道之间建立连接实现数据传输的。底层就是 sendfile() 系统调用函数。 如果你追溯 Kafka 文件传输的代码,你会发现,最终它调用了 Java NIO 库里的 transferTo 方法: @Overridepublic long transferFrom(FileChannel fileChannel, long position, long count) throws IOException{ return fileChannel.transferTo(position, count, socketChannel); }这就是 Kafka 为什么那么快的原因,使用了零拷贝技术。 SocketChannel通过ServerSocketChannel.open()方法可以获取服务器的通道,然后绑定一个地址端口号,接着accept()方法可获得一个SocketChannel通道,也就是客户端的连接通道。 public static void main(String[] args) throws Exception{ ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666); //绑定IP地址、端口号 serverSocketChannel.bind(address); //创建一个缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(10); while (true) { SocketChannel socketChannel = serverSocketChannel.accept(); while (socketChannel.read(byteBuffer) != -1) { System.out.println(new String(byteBuffer.array())); byteBuffer.clear(); //清空缓冲区,重置标志位 } } }实际上面的例子是阻塞式的,要做到非阻塞还需要使用选择器Selector。 选择器(Selector)Selector翻译成选择器,有些人也会翻译成多路复用器,实际上指的是同一样东西。只有网络IO才会使用选择器,文件IO是不需要使用的。 NIO 中实现非阻塞 I/O 的核心对象是 Selector,Selector 是注册各种 I/O 事件的地方,它可以监听通道的状态,当那些事件发生时,就是 Selector 告诉我们所发生的事件。换句话说就是事件驱动,以此实现**单线程管理多个 Channel **的目的。 Java 的 NIO 是非阻塞 IO。使用 Selector选择器可以让一个线程处理多个的客户端连接。Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。避免了多线程之间的上下文切换导致的开销。要使用 Selector,首先要将对应的 Channel 以及 IO 事件(读、写、连接)注册到 Selector,注册后会产生一个 SelectionKey 对象,用于关联 Selector 和 Channel,及后续的 IO 事件处理。 Selector可以通过它自己的open()方法创建,借助SelectorProvider类创建一个新的 Selector 选择器。也可以通过实现SelectorProvider类的抽象方法openSelector()来自定义实现一个Selector。Selector 一旦创建将会一直处于 open 状态,直到调用了close()方法为止。 Selector selector = Selector.open(); Selector 选择器绑定 Channel 管道 /** * 初始化 ServerSocketChannel */ private ServerSocketChannel getServerChannel(Selector selector) throws IOException { // 开辟一个Channel通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 通道设置为非阻塞模式 serverSocketChannel.configureBlocking(false); // 为了将Channel跟Selector绑定在一起,我们需要将Channel注册到Selector上,调用Channel的register()方法 // 通道中数据的事件类型为OP_ACCEPT(通道与选择器之间的桥梁 SelectionKey ) serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 通道绑定端口,开始监听 serverSocketChannel.socket().bind(new InetSocketAddress("127.0.0.1", 6666)); return serverSocketChannel; }管道 Channel 和 选择器 Selector 的关系: Selector 通过不断轮询的方式同时监听多个 Channel 的事件,注意这里是同时监听,一旦有 Channel 准备好了,它就会返回这些准备好了的 Channel,交给处理线程去处理。在NIO编程中,通过 Selector 我们就实现了一个线程同时处理多个连接请求的目标,也可以一定程序降低服务器资源的消耗。 通道与选择器之间的桥梁 SelectionKeySelector 与 Channel 的关系图如下: 在SelectionKey类中有四个常量表示四种事件类型: public abstract class SelectionKey { //读事件 public static final int OP_READ = 1 |
今日新闻 |
点击排行 |
|
推荐新闻 |
图片新闻 |
|
专题文章 |
CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭 |