Java学习总结之NIO

概述

NIO (Non-blocking I/O,也称New I/O) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。

它支持面向缓冲的,基于通道的I/O操作方法。 随着JDK 7的推出,NIO系统得到了扩展,为文件系统功能和文件处理提供了增强的支持。 由于NIO文件类支持的这些新的功能,NIO被广泛应用于文件处理。

NIO为Java程序员实现高速I/O,而不使用自定义本机代码。 NIO将填充、排放缓冲区等的时间性I/O活动移回操作系统,从而大大提高了操作速度。

NIO包

NIO类包含在java.nio包中。要了解NIO子系统不会取代java.io包中可用的基于流的I/O类。
按不同类别分组的一些NIO类,如下所示:


Java学习总结之NIO_第1张图片

Java学习总结之NIO_第2张图片

Java IO与NIO比较

在学习NIO之前,有必要将它与Java IO进行比较,以了解两个包之间的差别。

下面表格列出了Java IO和NIO之间的主要区别:


Java学习总结之NIO_第3张图片

阻塞与非阻塞I/O

阻塞I/O
阻塞IO等待数据写入或返回前的读取。Java IO的各种流是阻塞的。这意味着当线程调用write()或read()时,线程会被阻塞,直到有一些数据可用于读取或数据被完全写入。

非阻塞I/O
非阻塞IO不等待返回前读取或写入数据。 Java NIO非阻塞模式允许线程请求向通道写入数据,但不等待它被完全写入。允许线程继续进行,并做其他事情。

面向流与面向缓冲

面向流
Java IO是面向流的I/O,这意味着我们需要从流中读取一个或多个字节。它使用流来在数据源/槽和java程序之间传输数据。使用此方法的I/O操作较慢。

下面来看看在Java程序中使用输入/输出流的数据流图:


Java学习总结之NIO_第4张图片

面向缓冲

Java NIO是面向缓冲的I/O。 将数据读入缓冲区,使用通道进一步处理数据。 在NIO中,使用通道和缓冲区来处理I/O操作,因此支持非阻塞IO。

下面看看通道,缓冲区,java程序,数据源和数据接收器之间的相互作用:


Java学习总结之NIO_第5张图片

流与块

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. 通道(Channel)

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

通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。(读指从通道读取数据到缓冲区,写指从缓冲区写数据到通道。读写都是针对缓冲区的
也就是说,流是单工的,通道是全双工的。

让我们来看看java.nio.channels类的层次结构:


Java学习总结之NIO_第6张图片

通道包括以下类型:

  • FileChannel:文件通道,用于从文件读取数据。它只能通过调用getChannel()方法来创建对象。不能直接创建FileChannel对象。
    下面是一个创建FileChannel对象的例子:
FileInputStream fis = new FileInputStream("D:\\file-read.txt"); 
// Path of Input text file  
ReadableByteChannel rbc = fis.getChannel();
  • DatagramChannel:通过 UDP 读写网络中数据,它使用工厂方法来创建新对象。
    下面是打开DatagramChannel的语法:
DatagramChannel ch = DatagramChannel.open();

用于关闭DatagramChannel的语法:

ch.close();
  • SocketChannel:通过 TCP 读写网络中数据,它还使用工厂方法来创建新对象。
    用于打开SocketChannel的语法:
SocketChannel ch = SocketChannel.open();  
ch.connect(new InetSocketAddress("somehost", someport));

用于关闭SocketChannel的语法:

ch.close();
  • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。(和ServerSocket一样监听客户端的TCP连接)
    下面是打开ServerSocketChannel的语法:
ServerSocketChannel ch = ServerSocketChannel.open();  
ch.socket().bind (new InetSocketAddress (somelocalport));

下面是关闭ServerSocketChannel的语法:

ServerSocketChannel ch = ServerSocketChannel.close();  
ch.socket().bind (new InetSocketAddress (somelocalport));

上述通道涵盖UDP(用户数据报协议)+ TCP(传输控制协议)网络I/O和文件I/O。

2. 缓冲区(Buffer)

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

数据从通道读入缓冲区:

从缓冲区将数据写入通道

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

缓冲区包括以下类型:

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

上述缓冲区覆盖了通过I/O发送的基本数据类型:char,double,int,long,byte,short和float。

缓冲区状态变量

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

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

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


Java学习总结之NIO_第7张图片

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


Java学习总结之NIO_第8张图片

③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
Java学习总结之NIO_第9张图片

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


Java学习总结之NIO_第10张图片

⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
Java学习总结之NIO_第11张图片

理解:在通道输入时,position指向下一个字节将输入的位置,limit指向capacity处,也就是通道最多能容纳的字节的后一个位置,即position的输入上界;在通道输出时,position指向下一个将被输出通道的字节存放的位置,而limit指向通道最后一个字节的下一位置,即position的输出下界。

文件 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();
    }
}

分散/聚集或向量I/O

在Java NIO中,通道提供了称为分散/聚集或向量I/O的重要功能。 这是一种简单但功能强大的技术,通过这种技术,使用单个write()函数将字节从一组缓冲区写入流,并且可以使用单个read()函数将字节从流读取到一组缓冲区中。

Java NIO已经内置了分散/聚集支持。它可以用于从通道读取和写入通道。

分散读取

“分散读取”用于将数据从单个通道读取多个缓冲区中的数据。

下面来看看分散原理的说明:


Java学习总结之NIO_第12张图片

下面是执行分散读取操作的代码示例:

public interface ScatteringByteChannel extends ReadableByteChannel  
{  
    public long read (ByteBuffer [] argv) throws IOException;  
    public long read (ByteBuffer [] argv, int length, int offset) throws IOException;  
}

聚集写入

“聚集写入”用于将数据从多个缓冲区写入单个通道。

下面来看看聚集原则的简单说明:


Java学习总结之NIO_第13张图片

下面来看看看执行聚集写入操作的代码示例:

public interface GatheringByteChannel extends WritableByteChannel  
{  
    public long write(ByteBuffer[] argv) throws IOException;  
    public long write(ByteBuffer[] argv, int length, int offset) throws IOException;  
}

分散读取/聚集写入示例

package com.yiibai;

import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ScatteringByteChannel;
import java.nio.channels.GatheringByteChannel;

public class ScatterGatherIO {
    public static void main(String params[]) {
        String data = "Scattering and Gathering example shown in yiibai.com";
        gatherBytes(data);
        scatterBytes();
    }

    /*
     * gatherBytes() is used for reading the bytes from the buffers and write it
     * to a file channel.
     */
    public static void gatherBytes(String data) {
        String relativelyPath = System.getProperty("user.dir");
        // The First Buffer is used for holding a random number
        ByteBuffer buffer1 = ByteBuffer.allocate(8);
        // The Second Buffer is used for holding a data that we want to write
        ByteBuffer buffer2 = ByteBuffer.allocate(400);
        buffer1.asIntBuffer().put(420);
        buffer2.asCharBuffer().put(data);
        GatheringByteChannel gatherer = createChannelInstance(relativelyPath+"/test.txt", true);
        // Write the data into file
        try {
            gatherer.write(new ByteBuffer[] { buffer1, buffer2 });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /*
     * scatterBytes() is used for reading the bytes from a file channel into a
     * set of buffers.
     */
    public static void scatterBytes() {
        String relativelyPath = System.getProperty("user.dir");
        // The First Buffer is used for holding a random number
        ByteBuffer buffer1 = ByteBuffer.allocate(8);
        // The Second Buffer is used for holding a data that we want to write
        ByteBuffer buffer2 = ByteBuffer.allocate(400);
        ScatteringByteChannel scatter = createChannelInstance(relativelyPath+"/test.txt", false);
        // Reading a data from the channel
        try {
            scatter.read(new ByteBuffer[] { buffer1, buffer2 });
        } catch (Exception e) {
            e.printStackTrace();
        }
        // Read the two buffers seperately
        buffer1.rewind();
        buffer2.rewind();

        int bufferOne = buffer1.asIntBuffer().get();
        String bufferTwo = buffer2.asCharBuffer().toString();
        // Verification of content
        System.out.println(bufferOne);
        System.out.println(bufferTwo);
    }

    public static FileChannel createChannelInstance(String file, boolean isOutput) {
        FileChannel FChannel = null;
        try {
            if (isOutput) {
                FChannel = new FileOutputStream(file).getChannel();
            } else {
                FChannel = new FileInputStream(file).getChannel();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return FChannel;
    }
}

在上述程序中,第一个缓冲区在控制台上打印随机输出,第二个缓冲区在控制台上打印“Scattering and Gathering example shown in yiibai.com”。

它还用“Scattering and Gathering example shown in yiibai.com”替换test.txt文件的内容。

NIO数据传输

在Java NIO中,可以非常频繁地将数据从一个通道传输到另一个通道。批量传输文件数据是非常普遍的,因为几个优化方法已经添加到FileChannel类中,使其更有效率。

通道之间的数据传输在FileChannel类中的两种方法是:

  • FileChannel.transferTo()方法
  • FileChannel.transferFrom()方法

FileChannel.transferTo()方法

transferTo()方法用来从FileChannel到其他通道的数据传输。

下面来看一下transferTo()方法的例子:

public abstract class Channel extends AbstractChannel  
{    
   public abstract long transferTo (long position, long count, WritableByteChannel target);  
}

FileChannel.transferFrom()方法

transferFrom()方法允许从源通道到FileChannel的数据传输。

下面来看看transferFrom()方法的例子:

public abstract class Channel extends AbstractChannel  
{    
    public abstract long transferFrom (ReadableByteChannel src, long position, long count);  
}

基本通道到通道数据传输示例

下面来看看从4个不同文件读取文件内容的简单示例,并将它们的组合输出写入第五个文件:

package com.yiibai;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.nio.channels.WritableByteChannel;
import java.nio.channels.FileChannel;

public class TransferDemo {
    public static void main(String[] argv) throws Exception {
        String relativelyPath = System.getProperty("user.dir");
        // Path of Input files
        String[] iF = new String[] { relativelyPath + "/input1.txt", relativelyPath + "/input2.txt",
                relativelyPath + "/input3.txt", relativelyPath + "/input4.txt" };
        // Path of Output file and contents will be written in this file
        String oF = relativelyPath + "/combine_output.txt";
        // Acquired the channel for output file
        FileOutputStream output = new FileOutputStream(new File(oF));
        WritableByteChannel targetChannel = output.getChannel();
        for (int j = 0; j < iF.length; j++) {
            // Get the channel for input files
            FileInputStream input = new FileInputStream(iF[j]);
            FileChannel inputChannel = input.getChannel();

            // The data is tranfer from input channel to output channel
            inputChannel.transferTo(0, inputChannel.size(), targetChannel);

            // close an input channel
            inputChannel.close();
            input.close();
        }
        // close the target channel
        targetChannel.close();
        output.close();
        System.out.println("All jobs done...");
    }
}

在上述程序中,将4个不同的文件(即input1.txt,input2.txt,input3.txt和input4.txt)的内容读取并将其组合的输出写入第五个文件,即:combine_output.txt文件的中。

选择器

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

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

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

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

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

Java学习总结之NIO_第14张图片

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;

详见 NIO操作类型与就绪条件

3. 监听事件

int num = selector.select();

使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。
可用于选择通道的各种select()方法有:

  • int select()
    由select()方法返回的整数值通知有多少个通道准备好进行通信。
  • int select(long TS)
    方法与select()相同,除了阻塞最大TS(毫秒)时间的输出。
  • int selectNow()
    它不阻止输出并立即返回任何准备好的通道。

4. 获取到达的事件

selectedKeys()
当调用了任何一个select()方法后,它将返回一个值,表示一个或多个通道准备就绪,那么我们可以通过使用选择的键集合来访问就绪通道:

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

套接字 NIO 实例

NIOServer.java

public class NIOServer {

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

        Selector selector = Selector.open(); //创建选择器

        ServerSocketChannel ssChannel = ServerSocketChannel.open(); //创建并注册ServerSocketChannel通道
        ssChannel.configureBlocking(false);
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

        ServerSocket serverSocket = ssChannel.socket(); //从ServerSocketChannel得到ServerSocket
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888); 
        serverSocket.bind(address); //绑定IP地址

        while (true) { //循环监听事件
            selector.select();
            Set keys = selector.selectedKeys();
            Iterator 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();
    }
}

NIOClient.java

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();
    }
}

你可能感兴趣的:(Java学习总结之NIO)