javaIO2

Wesley13
• 阅读 576

NIO

新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。

1.1 流与块

I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。

面向流的 I/O 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。

面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

I/O 包和 NIO 已经很好地集成了,java.io.* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。

1.2 通道与缓冲区

1. 通道

通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。

通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。

通道包括以下类型:

  • FileChannel:从文件中读写数据;
  • DatagramChannel:通过 UDP 读写网络中数据;
  • SocketChannel:通过 TCP 读写网络中数据;
  • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。

2. 缓冲区

发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。

缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

缓冲区包括以下类型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

1.3 缓冲区状态变量

  • capacity:最大容量;
  • position:当前已经读写的字节数;
  • limit:还可以读写的字节数。

状态变量的改变过程举例:

① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。

 javaIO2

② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 移动设置为 5,limit 保持不变。

 javaIO2

③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。

 javaIO2

④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。

javaIO2

⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。

 javaIO2

1.4 文件 NIO 实例

以下展示了使用 NIO 快速复制文件的实例:

public static void fastCopy(String src, String dist) throws IOException {

 

    /* 获得源文件的输入字节流 */

    FileInputStream fin = new FileInputStream(src);

 

    /* 获取输入字节流的文件通道 */

    FileChannel fcin = fin.getChannel();

 

    /* 获取目标文件的输出字节流 */

    FileOutputStream fout = new FileOutputStream(dist);

 

    /* 获取输出字节流的文件通道 */

    FileChannel fcout = fout.getChannel();

 

    /* 为缓冲区分配 1024 个字节 */

    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

 

    while (true) {

 

        /* 从输入通道中读取数据到缓冲区中 */

        int r = fcin.read(buffer);

 

        /* read() 返回 -1 表示 EOF */

        if (r == -1) {

            break;

        }

 

        /* 切换读写 */

        buffer.flip();

 

        /* 把缓冲区的内容写入输出文件中 */

        fcout.write(buffer);

 

        /* 清空缓冲区 */

        buffer.clear();

    }

}

1.5 选择器

NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。

NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。

通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。

因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。

应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。

 javaIO2

1. 创建选择器

Selector selector = Selector.open();

2. 将通道注册到选择器上

ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。

在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

它们在 SelectionKey 的定义如下:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

3. 监听事件

int num = selector.select();

使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。

4. 获取到达的事件

Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()) {
        // ...
    } else if (key.isReadable()) {
        // ...
    }
    keyIterator.remove();
}

5. 事件循环

因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。

while (true) {
    int num = selector.select();
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = keys.iterator();
    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if (key.isAcceptable()) {
            // ...
        } else if (key.isReadable()) {
            // ...
        }
        keyIterator.remove();
    }
}

1.6 套接字 NIO 实例

public class NIOServer {

 

    public static void main(String[] args) throws IOException {

 

        Selector selector = Selector.open();

 

        ServerSocketChannel ssChannel = ServerSocketChannel.open();

        ssChannel.configureBlocking(false);

        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

 

        ServerSocket serverSocket = ssChannel.socket();

        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);

        serverSocket.bind(address);

 

        while (true) {

 

            selector.select();

            Set<SelectionKey> keys = selector.selectedKeys();

            Iterator<SelectionKey> keyIterator = keys.iterator();

 

            while (keyIterator.hasNext()) {

 

                SelectionKey key = keyIterator.next();

 

                if (key.isAcceptable()) {

 

                    ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();

 

                    // 服务器会为每个新连接创建一个 SocketChannel

                    SocketChannel sChannel = ssChannel1.accept();

                    sChannel.configureBlocking(false);

 

                    // 这个新连接主要用于从客户端读取数据

                    sChannel.register(selector, SelectionKey.OP_READ);

 

                } else if (key.isReadable()) {

 

                    SocketChannel sChannel = (SocketChannel) key.channel();

                    System.out.println(readDataFromSocketChannel(sChannel));

                    sChannel.close();

                }

 

                keyIterator.remove();

            }

        }

    }

 

    private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {

 

        ByteBuffer buffer = ByteBuffer.allocate(1024);

        StringBuilder data = new StringBuilder();

 

        while (true) {

 

            buffer.clear();

            int n = sChannel.read(buffer);

            if (n == -1) {

                break;

            }

            buffer.flip();

            int limit = buffer.limit();

            char[] dst = new char[limit];

            for (int i = 0; i < limit; i++) {

                dst[i] = (char) buffer.get(i);

            }

            data.append(dst);

            buffer.clear();

        }

        return data.toString();

    }

}

public class NIOClient {

 

    public static void main(String[] args) throws IOException {

        Socket socket = new Socket("127.0.0.1", 8888);

        OutputStream out = socket.getOutputStream();

        String s = "hello world";

        out.write(s.getBytes());

        out.close();

    }

}

1.7 内存映射文件

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。

向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。

下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。

MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);

1.8 对比

NIO 与普通 I/O 的区别主要有以下两点:

  • NIO 是非阻塞的;
  • NIO 面向块,I/O 面向流
点赞
收藏
评论区
推荐文章
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
NIO入门
1、I/O输入输出,所有的IO都被视作是单个字节的移动,通过stream对象一次移动一个字节。流IO负责把对象转换为字节,然后再转换为对象。NIO提供了二套NIO,一套是针对标准输入输出NIO,另一套是网络编程NIO2、流与块的比较NIO和IO最大的区别是数据打包和传输方式,IO是以流的方式来处理数据,而NIO是以块的方式处理数据。面向块的IO
Wesley13 Wesley13
3年前
Java IO输入输出
学前知道Java的IO使用“流”的概念来表示。IO流涉及到数据源和目的地。流,是从源“流向”目的的数据流。Java将各种数据源和目标之间数据的传输统一抽象为流,通过对流对象的操作来完成I/O功能。输入输出实际都是对内存而言的。数据源可以是键盘、文件、应用程序、鼠标、网络连接。
Wesley13 Wesley13
3年前
Java NIO和IO的区别
IO               NIO面向流           面向缓冲阻塞IO           非阻塞IO无               选择器面向流与面向缓冲JavaNIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。JavaIO面向流意味着每次从流中
Stella981 Stella981
3年前
Netty(二)
一、先讲下NIO编程。NIO(NonblockI/O),亦叫做非阻塞I/O与Socket类和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。1 缓冲区Buffer这里首先介绍缓冲区的概念,NIO和原I/O的一个重要区别就是NIO库中,所有数据都是用缓
Wesley13 Wesley13
3年前
Java NIO:IO与NIO的区别
 一、概念    NIO即NewIO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。在JavaAPI中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。二、NIO和IO的主要区别下表总结了JavaIO和NIO之
Wesley13 Wesley13
3年前
Java输入输出流
1.什么是IO   Java.io是大多数面向数据流的输入/输出类的主要软件包。此外,Java也对块传输提供支持,在核心库java.nio中采用的便是块IO。  流IO的好处是简单易用,缺点是效率较低。块IO效率很高,但编程比较复杂。   JavaIO模型 :   Java的IO
Wesley13 Wesley13
3年前
Java NIO
简介引入NIO的原因1.因为BIO都是阻塞的IO,为了使Java能支持非阻塞I/O,JDK引入了NIO,可以将NIO理解成是NonblockI/O.(也有书说是newIO)2.BIO编程中,每当有一个新的客户端请求过来时,服务器端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接,在并发量
Wesley13 Wesley13
3年前
Java8 数据流
一、基本知识\\数据流(stream)\\是对集合(collection)功能的增强,更专注于对集合对象的各种便利、高效的聚合,大批量数据操作。数据流的特点:元素序列流提供了一组特定类型的以顺序方式元素。源流使用集合,数组或I/O资源为输入源。聚合操作数据流支持如filter
Wesley13 Wesley13
3年前
Java NIO之缓冲区
JavaNIO之Buffer  Java传统的I/O模型是面向单个字节的,它将输入输出抽象为字节流或字符流。这种单个字节的读取或写入模型的效率比较低,而且不符合操作系统的I/O特点。操作系统的IO是面向字节块的,通常是直接从磁盘中读取一块数据到内存或写入一块数据到磁盘。JavaNIO提供了缓冲区来实现字节块的读写。