Java NIO详解及源码分析

在介绍NIO之前,我们需要来接两组概念:

1.阻塞与非阻塞的概念

2.同步与非同步的概念

阻塞与非阻塞是指:例如当我们建立内存与磁盘的通道时,假设是将内存中的数据写入到磁盘中。

当建立通道的时候。表示读写就绪。这时候阻塞指的时:当通道一旦建立的时候,线程就会一直阻塞等待IO操作完成。 但是非阻塞是指:通道建立了,不需要等到IO操作是否完成、立即返回。此时线程可以去执行其他的事情。

同步和非同步的概念是指:同步操作是否需要应用程序参与执行。同步是需要的。非同步是不需要的,非同步是操作系统直接操作IO。

我们用的Java IO就是同步阻塞的IO模型 Java NIO就是同步非阻塞的IO模型。

可以看到这两种IO模型是需要应用程序参与IO操作的。

总结一下:

Java的IO操作可以分为两段等待就绪和操作

阻塞与非阻塞是指等待就绪阶段,在通道建立的时候。等待就绪IO是阻塞的、此时线程只能等待真真的IO操作完成后。才能执行后面的操作。NIO是非阻塞的。在等待就绪的时候,会立马返回结果。并不断间隔的去询问是否可以进行IO操作。

同步与异步的发生在IO操作阶段:可以看到IO和NIO都是同步的。因为在真正的IO操作时。这两种IO模型都是应用程序参与的、要等待CPU完成操作,才能去做其他事。异步则不一样,应用程序不关系IO操作的内容,交由操作系统去实现。应用程序只需要知道IO操作的结果。

接下来我们来看看NIO中几个比较重要的组件的源码:

Buffer详解

为什么说 NIO 是基于缓冲区的 IO 方式呢?因为,当一个链接建立完成后,IO 的数据未必会马上到达,为了当数据到达时能够正确完成 IO操作,在 BIO(阻塞 IO)中,等待 IO 的线程必须被阻塞,以全天候地执行 IO 操作。为了解决这种 IO方式低效的问题,引入了缓冲区的概念,当数据到达时,可以预先被写入缓冲区,再由缓冲区交给线程,因此线程无需阻塞地等待 IO。

NIO的IO模式如下图所示:

 Buffer是存储数据的缓冲区:

public abstract class Buffer {

    /**
     * The characteristics of Spliterators that traverse and split elements
     * maintained in Buffers.
     */
    static final int SPLITERATOR_CHARACTERISTICS =
        Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;

    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;

    // Used only by direct buffers
    // NOTE: hoisted here for speed in JNI GetDirectBufferAddress
    long address;

    // Creates a new buffer with the given mark, position, limit, and capacity,
    // after checking invariants.
    //
    Buffer(int mark, int pos, int lim, int cap) {       // package-private
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);
        this.capacity = cap;
        limit(lim);
        position(pos);
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("mark > position: ("
                                                   + mark + " > " + pos + ")");
            this.mark = mark;
        }
    }
}

从上面的代码我们可以看到4个比较重要的属性。buffer的构造函数也是由这个属性来创建。

  1. mark: 标记着当前position可读或可写的索引值
  2. position:指向下一次可读或者可写的索引值
  3. limit:可读可写的索引一定是小于它的,不能等于它
  4. capacity:数组容量,一旦初始化好了,便永远无法修改

Invariants: mark <= position <= limit <= capacity 4个属性之间的关系

Buffer的使用步骤:

1.写入数据到 Buffer;
2.调用 flip() 方法;
3.从 Buffer 中读取数据;
4.调用 clear() 方法或者 compact() 方法。

当向 Buffer 写入数据时,Buffer 会记录下写了多少数据。一旦要读取数据,需要通过 .flip() 方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 Buffer 的所有数据。

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear() 或 compact() 方法。clear() 方法会清空整个缓冲区。compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面
Buffer种类:

CharBuffer、DoubleBuffer、IntBuffer、LongBuffer、ByteBuffer、ShortBuffer、FloatBuffer 

不同Buffer操作不同的基本数据类型:其底层都是利用数组实现的:例如ByteBuffer底层存储数据就是用的Byte[]:

public abstract class ByteBuffer
    extends Buffer
    implements Comparable<ByteBuffer>
{

    final byte[] hb;                  // Non-null only for heap buffers
    final int offset;
    boolean isReadOnly;                 // Valid only for heap buffers
}

接下来我们看下Buffer的一个使用案例:

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

public class IO_Demo
{
    public static void main(String[] args) throws Exception
    {
        String infile = "D:\\Users\\data.txt";
        String outfile = "D:\\Users\\dataO.txt";
        // 获取源文件和目标文件的输入输出流
        FileInputStream fin = new FileInputStream(infile);
        FileOutputStream fout = new FileOutputStream(outfile);
        // 获取输入输出通道
        FileChannel fileChannelIn = fin.getChannel();
        FileChannel fileChannelOut = fout.getChannel();
        // 创建缓冲区,分配1K堆内存
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        while (true)
        {
            // clear方法重设缓冲区,使它可以接受读入的数据
            buffer.clear();
            // 从输入通道中读取数据数据并写入buffer
            int r = fileChannelIn.read(buffer);
            // read方法返回读取的字节数,可能为零,如果该通道已到达流的末尾,则返回-1
            if (r == -1)
            {
                break;
            }
            // flip方法将 buffer从写模式切换到读模式
            buffer.flip();
            // 从buffer中读取数据然后写入到输出通道中
            fileChannelOut.write(buffer);
        }
        //关闭通道
        fileChannelOut.close();
        fileChannelIn.close();
        fout.close();
        fin.close();
    }
}

首先我们看下Buffer.allocate初始化一个ByteBuffer

    public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity);
    }

    HeapByteBuffer(int cap, int lim) {            // package-private

        super(-1, 0, lim, cap, new byte[cap], 0);
    }

    ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
                 byte[] hb, int offset)
    {
        super(mark, pos, lim, cap);
        this.hb = hb;
        this.offset = offset;
    }

可以看到这里初始化了一个1024大小的字节数组,即初始化一个1024字节的byteBuffer用来存储数据。 

记下来我们看下  buffer.clear();方法:

   public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

清空buffer的过程。初始化mark为负1.position为0 limit 等于最大容量。初始化buffer后,开始写入数据。通过inputChannle写入数据的过程稍后介绍、

当写完数据后,调用buffer的flip方法切换为读数据模式

    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

当为读取模式时:limit 为写入数据的position,即写入的多少数据。那么就只有多少数据可以读:

比如在写入数据的时候,写入了1000个字节。那么在读取模式下:能读到最大数据即为1000 limit=1000 ,并初始化。第一个读取的数据源为0的位置开始读。

然后通过通道writeChannel写入数据

总结:

position 记录当前读取或者写入的位置,写模式下等于当前写入的单位数据数量,从写模式切换到读模式时,置为 0,在读的过程中等于当前读取单位数据的数量;
limit 代表最多能写入或者读取多少单位的数据,写模式下等于最大容量 capacity;从写模式切换到读模式时,等于position,然后再将 position 置为 0,所以,读模式下,limit 表示最大可读取的数据量,这个值与实际写入的数量相等。
capacity 表示 buffer 容量,创建时分配。

读写模式下:各属性值为下图所示:

接下来我们来看NIO第二个主要的组件Channel

上面我们大概知道:Buffer是存储数据的缓冲区,Channel是用于传输数据的通道.

磁盘读写数据到内存,都是通过channel读写数据到Buffer。

引用 Java NIO 中权威的说法:通道是 I/O 传输发生时通过的入口,而缓冲区是这些数据传输的来源或目标。对于离开缓冲区的传输,需要输出的数据被置于一个缓冲区,然后写入通道。对于传回缓冲区的传输,一个通道将数据写入缓冲区中。

Channel 是一个对象,可以通过它读取和写入数据。可以把它看做 IO 中的流。但是它和流相比还有一些不同:

Channel 是双向的,既可以读又可以写,而流是单向的(所谓输入/输出流);
Channel 可以进行异步的读写;

对 Channel的读写必须通过 buffer 对象。

在 Java NIO 中 Channel 主要有如下几种类型:

  1. FileChannel:从文件读取数据的
  2. DatagramChannel:读写 UDP 网络协议数据
  3. SocketChannel:读写 TCP 网络协议数据
  4. ServerSocketChannel:可以监听 TCP 连接

接着上面的buffer使用案例,我们来看下

FileChannel fileChannelIn = fin.getChannel();

FileChannel fileChannelOut = fout.getChannel();

fileChannelIn.read(buffer);

fileChannelOut.write(buffer);

源码分析:

fin.getChannel();fin是文件的输入流

    public FileChannel getChannel() {
        synchronized (this) {
            if (channel == null) {
                channel = FileChannelImpl.open(fd, path, true, false, this);
            }
            return channel;
        }
    }

FileChannelImpl是FileChannel的实现类、调用FileChannerlImpl.open主要是打开一个FileChannel的通道,用于传输buffer数据。返回一个Filechannle的通道对象、

    public static FileChannel open(FileDescriptor var0, String var1, boolean var2, boolean var3, Object var4) {
        return new FileChannelImpl(var0, var1, var2, var3, false, var4);
    }

    private FileChannelImpl(FileDescriptor var1, String var2, boolean var3, boolean var4, 
 boolean var5, Object var6) {
        this.fd = var1;
        this.readable = var3;
        this.writable = var4;
        this.append = var5;
        this.parent = var6;
        this.path = var2;
        this.nd = new FileDispatcherImpl(var5);
    }

我们看下传入的参数:FileDescriptor文件描述符对象:文件描述符的主要实际用途是创建一个 FileInputStream 或 FileOutputStream 来包含它:在FileInputStream初始化的时候就会new FileDescriptor对象。并依附在流对象上。定义writable为true 应该是输入流,所以定义为channel为写入buffer的写入通道,由次我们可以看出。Channel是支持双向的、可以读也可以写、

 public int read(ByteBuffer var1) throws IOException {
        //判断当前通道确实是open的,并且是read通道
        this.ensureOpen();
        if (!this.readable) {
            throw new NonReadableChannelException();
        } else {
            synchronized(this.positionLock) {
                int var3 = 0;
                int var4 = -1;

                byte var5;
                try {
                   //响应线程中断、
                    this.begin();
                    var4 = this.threads.add();
                    if (this.isOpen()) {
                        do {
                            var3 = IOUtil.read(this.fd, var1, -1L, this.nd);
                        } while(var3 == -3 && this.isOpen());

                        int var12 = IOStatus.normalize(var3);
                        return var12;
                    }

                    var5 = 0;
                } finally {
                    this.threads.remove(var4);
                    this.end(var3 > 0);

                    assert IOStatus.check(var3);

                }

                return var5;
            }
        }
    }


//IOUtil.read方法
    static int read(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
        if (var1.isReadOnly()) {
            throw new IllegalArgumentException("Read-only buffer");
        } else if (var1 instanceof DirectBuffer) {
            return readIntoNativeBuffer(var0, var1, var2, var4);
        } else {
             // 创建一个临时的ByteBuffer 对象
            ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining());

            int var7;
            try {
                  // 将数据读取到临时的buffer对象中去。
                int var6 = readIntoNativeBuffer(var0, var5, var2, var4);
                var5.flip();
                // 当var6不为-1说明还有数据
                if (var6 > 0) {
                     // 将真实数据写入到临时buffer中去、
                    var1.put(var5);
                }

                var7 = var6;
            } finally {
                Util.offerFirstTemporaryDirectBuffer(var5);
            }

            return var7;
        }
    }

NIO 第三个核心对象 Selector 详解

通道和缓冲区的机制,使得 Java NIO 实现了同步非阻塞 IO 模式,在此种方式下,用户进程发起一个 IO 操作以后便可返回做其它事情,而无需阻塞地等待 IO 事件的就绪,但是用户进程需要时不时的询问 IO 操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的 CPU 资源浪费。

鉴于此,需要有一个机制来监管这些 IO 事件,如果一个 Channel 不能读写(返回 0),我们可以把这件事记下来,然后切换到其它就绪的连接(channel)继续进行读写。在 Java NIO 中,这个工作由 selector 来完成,这就是所谓的同步。

Selector 是一个对象,它可以接受多个 Channel 注册,监听各个 Channel 上发生的事件,并且能够根据事件情况决定 Channel 读写。这样,通过一个线程可以管理多个 Channel,从而避免为每个 Channel 创建一个线程,节约了系统资源。如果你的应用打开了多个连接(Channel),但每个连接的流量都很低,使用 Selector 就会很方便。

要使用 Selector,就需要向 Selector 注册 Channel,然后调用它的 select() 方法。这个方法会一直阻塞到某个注册的通道有事件就绪,这就是所说的轮询。一旦这个方法返回,线程就可以处理这些事件。
下面这幅图展示了一个线程处理 3 个 Channel 的情况:
 

 接下来我们看下具体的调用例子:

/**
 * server 端
 */
public class Server {

    private ByteBuffer readBuffer = ByteBuffer.allocateDirect(1024);
    private ByteBuffer writeBuffer = ByteBuffer.allocateDirect(1024);
    private Selector selector;

    public Server() throws IOException{
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //设置非阻塞模式
        serverSocketChannel.configureBlocking(false);
        ServerSocket serverSocket = serverSocketChannel.socket();
        serverSocket.bind(new InetSocketAddress(8080));
        System.out.println("listening on port 8080");
        //打开 selector
        this.selector = Selector.open();

        //在 selector 注册感兴趣的事件
        serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
    }

    private void start() throws Exception{

        while(true){
            //调用阻塞的select,等待 selector上注册的事件发生
            this.selector.select();

            //获取就绪事件
            Iterator<SelectionKey> iterator = this.selector.selectedKeys().iterator();
            while(iterator.hasNext()){
                SelectionKey selectionKey = iterator.next();
                //先移除该事件,避免重复通知
                iterator.remove();
                // 新连接
                if(selectionKey.isAcceptable()){
                    System.out.println("isAcceptable");
                    ServerSocketChannel server = (ServerSocketChannel)selectionKey.channel();

                    // 新注册channel
                    SocketChannel socketChannel  = server.accept();
                    if(socketChannel==null){
                        continue;
                    }
                    //非阻塞模式
                    socketChannel.configureBlocking(false);

                    //注册读事件(服务端一般不注册 可写事件)
                    socketChannel.register(selector, SelectionKey.OP_READ);


                    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
                    buffer.put("hi new channel".getBytes());
                    buffer.flip();
                    int writeBytes= socketChannel.write(buffer);

                }

                // 服务端关心的可读,意味着有数据从client传来了数据
                if(selectionKey.isReadable()){
                    System.out.println("isReadable");
                    SocketChannel socketChannel = (SocketChannel)selectionKey.channel();

                    readBuffer.clear();
                    socketChannel.read(readBuffer);
                    readBuffer.flip();

                    String receiveData= Charset.forName("UTF-8").decode(readBuffer).toString();
                    System.out.println("receiveData:"+receiveData);


                    //这里将收到的数据发回给客户端
                    writeBuffer.clear();
                    writeBuffer.put(receiveData.getBytes());
                    writeBuffer.flip();
                    while(writeBuffer.hasRemaining()){
                        //防止写缓冲区满,需要检测是否完全写入
                        System.out.println("写入数据:"+socketChannel.write(writeBuffer));
                    }
                }

            }
        }
    }

    public static void main(String[] args) throws Exception{
        new Server().start();
    }

}

创建Selector

通过 Selector.open()方法, 我们可以创建一个选择器

将 Channel 注册到Selector 中

我们需要将 Channel 注册到Selector 中,这样才能通过 Selector 监控 Channel :

//非阻塞模式
channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

 注意, 如果一个 Channel 要注册到 Selector 中, 那么这个 Channel 必须是非阻塞的, 即channel.configureBlocking(false);
因为 Channel 必须要是非阻塞的, 因此 FileChannel 是不能够使用选择器的, 因为 FileChannel 都是阻塞的.

 

因为 channel 是非阻塞的,因此当没有数据的时候会理解返回,因此 实际上 Selector 是不断的在轮询其注册的 channel 是否有数据就绪。

在使用 Channel.register()方法时, 第二个参数指定了我们对 Channel 的什么类型的事件感兴趣, 这些事件有:

Connect, 连接事件(TCP 连接), 对应于SelectionKey.OP_CONNECT
Accept, 确认事件, 对应于SelectionKey.OP_ACCEPT
Read, 读事件, 对应于SelectionKey.OP_READ, 表示 buffer 可读.
Write, 写事件, 对应于SelectionKey.OP_WRITE, 表示 buffer 可写.
Selector 整体使用 流程

1、建立 ServerSocketChannel
2、通过 Selector.open() 打开一个 Selector.
3、将 Channel 注册到 Selector 中, 并设置需要监听的事件
4、循环:
1、调用 select() 方法
2、调用 selector.selectedKeys() 获取 就绪 Channel
3、迭代每个 selected key:

最后这里附上和前面对应的客户端的代码:

/**
 * client 端
 */
public class Client{

    private final ByteBuffer sendBuffer=ByteBuffer.allocate(1024);
    private final ByteBuffer receiveBuffer=ByteBuffer.allocate(1024);
    private Selector selector;
    private SocketChannel socketChannel;

    public Client()throws IOException{
        this.socketChannel = SocketChannel.open();
        this.socketChannel.connect(new InetSocketAddress(InetAddress.getLocalHost(),8080));
        this.socketChannel.configureBlocking(false);
        System.out.println("连接建立成功");
        this.selector=Selector.open();
        this.socketChannel.register(selector,SelectionKey.OP_READ);
    }

    public static void main(String[] args) throws Exception{
        final Client client=new Client();
        Thread sendMsg=new Thread(client::sendInputMsg);
        sendMsg.start();

        client.start();
    }

    private void start()throws IOException {
        while (selector.select() > 0 ){

            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()){
                SelectionKey key = it.next();
                it.remove();

                if (key.isReadable()) {
                    System.out.println("isReadable");
                    receive(key);
                }

            }

        }
    }

    /**
     * 接收服务端发送的内容
     * @param key
     * @throws IOException
     */
    private void receive(SelectionKey key)throws IOException{
        SocketChannel socketChannel=(SocketChannel)key.channel();
        socketChannel.read(receiveBuffer);
        receiveBuffer.flip();
        String receiveData=Charset.forName("UTF-8").decode(receiveBuffer).toString();

        System.out.println("receive server message:"+receiveData);
        receiveBuffer.clear();
    }

    /**
     * 发送控制台输入内容至服务器
     */
    private void sendInputMsg() {
        BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(System.in));
        try{
            String msg;
            while ((msg = bufferedReader.readLine()) != null){
                synchronized(sendBuffer){
                    sendBuffer.put((msg+"\r\n").getBytes());
                    sendBuffer.flip();
                    while(sendBuffer.hasRemaining()){
                        socketChannel.write(sendBuffer);
                    }
                    sendBuffer.compact();

                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}


 ​​​​​​​

参考资料:

https://blog.csdn.net/twt936457991/article/details/89668350

https://blog.csdn.net/u014634338/article/details/82865622

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值