乐趣区

关于java:Java-NIO-上手教程

程序读取数据模型

传统 IO 和 NIO 的不同

传统 IO 特点

  • 面向流。
  • 单向(只能读或只能写)。
  • 程序读写间接和操作系统交互。
  • 读写线程阻塞。

NIO 特点

  • 面向缓冲区。
  • 双向(既能读,又能写)。
  • 程序读写通过 Channel,程序和 Channel 之间的交互通过缓冲区。
  • 读写线程不阻塞。(然而 selector 的 select() 会阻塞)。

NIO 读取数据模型

NIO 在读取数据时,是数据通过 Channel 写入缓冲区,程序从缓冲区读取数据。写数据也是同理。

NIO 四大外围组件

NIO 有 4 个外围组件,它们是:

  • Buffer:为了读或写,存储数据的容器。
  • Channel:与某些组件建设连贯,相似 Java IO 中的流,进行读或写操作,然而和 IO 流不同的是,Channel 是双向的。
  • Charsets:蕴含字符集(charsets)、解码器(decoders)、编码器(encoders),能够用来进行 byte 与 unicode 的转换。
  • Selector:通过 Selector,能够与多个 Channel 一起工作。

Buffer

Buffer 是一个存储固定数据大小、存储 Java 根本数据类型数据的容器。一个 Buffer 由上面这些组成:

• Capacity:容器容量
• Limit: 不能读或写的索引
• Position: 下一个读或写的元素索引
• Flip: 切换 IO 操作(读写操作)• Rewind: 设置 position = 0,limit 放弃不变
• Mark: 在 Buffer 标记一个地位
• Reset: 将 position 重置为 mark 的地位 

间接字节缓冲区

Java 能够应用 ByteBuffer 的 allocateDirect 办法创立间接缓冲区:

ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

相比拟于非间接缓冲区。IO 效率会更高。因为咱们在应用非间接缓冲区时,操作系统会创立一个长期间接缓冲区,将非间接缓冲区中的内容读入创立的长期间接缓冲区再进行操作。如果单从这点来看,应用间接缓冲区是 IO 最好的抉择。

然而在 Java 中创立间接缓冲区,是间接调用操作系统的 API 办法来创立的,会绕过 JVM 的内存治理,而且 JVM 针对缓冲区也有优化,所以可能应用间接缓冲区不是最好的形式。

往 ByteBuffer 写入数据

public class ByteBufferReadDemo {
    /**
     * 读取文件门路
     */
    private static final String INPUT_FILE = "./resources/input.txt";
    /**
     * 缓冲区大小
     */
    private static final int BYTE_BUFFER_LENGTH = 1024;

    public static void main(String[] args) {
        // 通过 IO 流获取 Channel
        try (FileChannel fileChannel = new FileInputStream(INPUT_FILE).getChannel()) {
            // 创立 ByteBuffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(BYTE_BUFFER_LENGTH);
            StringBuilder content = new StringBuilder();
            int read = 0;
            // 数据从通道读出(写入到 ByteBuffer)while ((read = fileChannel.read(byteBuffer)) != -1) {content.append(new String(byteBuffer.array(), 0, read));
                // 让 ByteBuffer 复原如初:position = 0, limit = capacity, 丢掉 mark
                byteBuffer.clear();}
            System.out.println(content);
        } catch (IOException e) {throw new RuntimeException("Unable to read file", e);
        }
    }
}

Buffer 读写原理

ByteBuffer 继承 Buffer 抽象类,很重要的三个属性是:

    # 读或者写的开始地位
    private int position = 0;
    # 限度读或写的地位
    private int limit;
    # 容量
    private int capacity;

当新创建一个容量为 10 的 ByteBuffer 对象时,它们三个的地位别离为:

此时 ByteBuffer 为写模式,能够从 position 开始写,limit 是最多能写到的地位,此时 limit = capacity。

随后咱们往 buffer 写入 1234,buffer 内部结构变为:

position 挪动到最小的一个可写地位 5(position = 5)。

此时咱们想要将 buffer 数据读出,就要转成读模式(调用 flip() 办法):

position 挪动到 buffer 中第一个可读的地位,limit 挪动到 buffer 中最初一个可读的地位。

咱们将数据全副读出,此时 buffer 外部变为:

此时 position = limit,证实曾经没有可读。

此时咱们应用 clear(),将 ByteBuffer 复原,此时 buffer 回到写模式(默认是写模式):

compact()和 clear()的区别就是,clear() 会间接让 buffer 回到刚创立的状态,而 compact() 会压缩还没被读的数据。比方咱们只读了 1 和 2:

此时调用 compact(),buffer 内部结构会变为:

Channel

能够通过 Channel 对 Buffer 读写。

创立一个 FileChannel:

        // 创立一个 File 文件对象
        final File file = new File(FileChannelReadExample.class.getClassLoader().getResource(path).getFile());  
        // 依据须要读或者须要写创立不同的 FileChannel              
        return fileOperation == FileOperation.READ ? new FileInputStream(file).getChannel() : new FileOutputStream(file).getChannel();

Charsets

蕴含 charsets、decoders、encoders。通过 Charsets 能够进行 unicode 与 byte 的转换。

编码与解码

编码(Encoding):一序列字符 -> 字节。

解码(Decoding):字节 -> 字符。

Charset 应用

        // 拿到 UTF-8 字符集    
        Charset utf8Charset = Charset.forName("UTF-8");
        // 编码
        ByteBuffer byteBuffer = utf8Charset.encode("zhangsan");
        // 解码
        System.out.println(utf8Charset.decode(byteBuffer));

Selector

Selector 能够多路复用 SelectableChannel,当 SelectableChannel 有 IO 事件产生时,Selector 就会告诉咱们的程序。

这里的 IO 事件(SelectableChannel 连贯 Selectors 的事件)包含:

  1. Connect
  2. Accept
  3. Read
  4. Write

SelectableChannel:能够多路复用的连贯 Selecotr 的通道。

Server/Client Demo

应用 Java NIO 实现一个简略的 Server、Client 示例:

Server

public class Server {
    protected static final String HOST = "127.0.0.1";

    protected static final int PORT = 8899;

    public static void main(String[] args) {
        try {ServerSocketChannel serverSocket = ServerSocketChannel.open();
            InetSocketAddress hostAddress = new InetSocketAddress(HOST, PORT);
            // 绑定端口
            serverSocket.bind(hostAddress);
            // 设置为非阻塞
            serverSocket.configureBlocking(false);
            // 创立 selector
            Selector selector = Selector.open();
            // Channel 注册到 selector(ACCEPT 事件)serverSocket.register(selector, serverSocket.validOps(), null);
            while (true) {
                // select 是阻塞的
                int selectKeyNums = selector.select();
                if (selectKeyNums > 0) {Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
                    while (selectionKeyIterator.hasNext()) {SelectionKey selectionKey = selectionKeyIterator.next();
                        if (selectionKey.isAcceptable()) {
                            // 注册读事件
                            registerRead(serverSocket, selector);
                        } else if (selectionKey.isReadable()) {
                            // 解决写事件
                            dealRead(selectionKey);
                        } else {System.out.println("Invalid selection key");
                        }
                        // 每次事件操作做完要删除,否则会持续读,报错
                        selectionKeyIterator.remove();}
                }
            }
        } catch (Exception e) {e.printStackTrace();
        }
    }

    public static void registerRead(ServerSocketChannel channel, Selector selector) throws IOException {SocketChannel client = channel.accept();
        client.configureBlocking(false);
        client.register(selector, SelectionKey.OP_READ, null);
        System.out.println(client.getRemoteAddress() + "connected!");
    }

    public static void dealRead(SelectionKey selectionKey) {
        try {SocketChannel channel = (SocketChannel) selectionKey.channel();
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            int read = channel.read(byteBuffer);
            if (read == -1) {selectionKey.channel();
                channel.close();} else {byteBuffer.flip();
                System.out.println("from" + channel.getRemoteAddress() + "reve a message:" + StandardCharsets.UTF_8.decode(byteBuffer));
            }
        } catch (Exception e) {e.printStackTrace();
        }
    }
}

Client

public class Client {public static void main(String[] args) {
        try {InetSocketAddress hostAddress = new InetSocketAddress(Server.HOST, Server.PORT);
            SocketChannel client = SocketChannel.open(hostAddress);
            Scanner sc = new Scanner(System.in);
            while (sc.hasNextLine()) {client.write(StandardCharsets.UTF_8.encode(sc.next()));
            }
        } catch (Exception e) {e.printStackTrace();
        }
    }
}

总结

IO 与 NIO 比照:

NIO 四大外围组件:

  1. ByteBuffer(存储数据的容器)
  2. Charsets(不便字符和字节的转化)
  3. Channels(真正 IO 的中央)
  4. Selectors(多路复用 SelectableChannel)

它们四个互相配合,形成 NIO 的流程。

退出移动版