IO(2)- NIO

95 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情

基本介绍

NIO 是从java1.4版本开始引入的一个全新的IO API,NIO是面向缓冲区、基于通道的IO操作。NIO有三大核心部分:Channel、Buffer、Selector。

NIO与BIO的比较

  • BIO以流的方式处理数据,NIO以块的方式处理数据
  • BIO是阻塞的,NIO是非阻塞的
  • BlO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道

通道、缓冲区、选择器

通道

通道Channel是对原I/O包中的流的模拟,可以通过它来读取或写入数据。
通道和流不同的是,流是单向的,而通道是双向的,既可以读也可以写

通道的类型:

  • FileChannel:和文件相关读写
  • DatagramChannel:通过UDP读写
  • SocketChannel:通过TCP读写
  • ServerSocketChannel:可以监听新进来的TCP链接,对每个新链接创建一个SocketChannel

缓冲区

缓冲区的作用是,发送给通道的数据都必须先放入到缓冲区中,同样的,读取数据也一样,的先把通道中的数据放到缓冲区中。

缓冲区类型

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

缓冲区重要的变量

  • capacity:最大容量
  • position:当前已经读写的字节数
  • limit:剩余可读写字节数

以上三个属性值之间有一些相对大小的关系:0 <= position <= limit <= capacity

举个例子:
初始化一个容量大小为10的XXBuffer对象,初始化的时候position为0,limit和capacity为10,在后续使用时capacity值固定不变,而另外两个值是会发生变化。

image.png

假设现在从channel中读取5个数据,此时position为4,而limit仍然是10

image.png

下面将读取的数据写入到缓冲区,首先需要调用flip()方法,这个方法会做两件事
1、将limit值设置为position
2、将position值设置为0

image.png

在将缓冲区的数据写入到通道中,此时position的值增加,limit保持不变,但是position不会超过limit

image.png

从缓冲区读取数据完毕后,调用clear()方法能够把状态值设为初始化值

image.png

代码演示\

package com.example.io.nio;

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class demo {

    public static void main(String[] args) throws IOException {
        FileInputStream fin = new FileInputStream("/Users/victor/workspace/demo/src/main/java/com/example/io/nio/test.txt");
        FileChannel fc = fin.getChannel();

        ByteBuffer buffer = ByteBuffer.allocate(10);
        output("初始化", buffer);

        fc.read(buffer);
        output("调用read()", buffer);

        buffer.flip();
        output("调用flip()", buffer);

        while (buffer.remaining() > 0) {
            byte b = buffer.get();
        }
        output("调用get()", buffer);

        buffer.clear();
        output("调用clear()", buffer);

        fin.close();
    }
    public static void output(String step, Buffer buffer) {
        System.out.println(step + " : ");
        System.out.print("capacity: " + buffer.capacity() + ", ");
        System.out.print("position: " + buffer.position() + ", ");
        System.out.println("limit: " + buffer.limit());
        System.out.println();
    }
}

文件内容: image.png

结果 :

image.png

选择器

NIO实现了IO的多路复用,一个线程Thread使用一个Selector轮询监听多个channel上的事件,从而让一个线程可以处理多个事件。

image.png

创建选择器

Selector selector = Selector.open();

将通道注册到选择器

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

NIO实例

package com.example.io.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

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();
    }
}
package com.example.io.nio;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

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

NIO的优点

  • NIO是非阻塞的
  • NIO是块处理,相较于I/O更具优势