NIO的简单理解

3 篇文章 0 订阅

在之前的IO内存模型梳理的时候,也提到过NIO:

I/O(input/output)内存模型(2)_PigeonEssence的博客-CSDN博客我们了解了I/O的基本概念,之后需要讨论的就是比如Java支持三大I/O模型,BIO,NIO和AIO1.BIOBIO就是Blocking I/O, 同步阻塞型IO,数据的读取写入必须阻塞在一个线程内等待其完成。我们很熟悉的java输入和输出流都是基于BIO去实现的,如InputStream,OutputStream等。 (这种Block是不会影响同时运行的其他程序(进程)的,因为现代操作系统都是多任务的,任务之间的切换是抢占式的。这里Block只是指Block当前的...https://blog.csdn.net/m0_56289903/article/details/121012698?spm=1001.2014.3001.5502        我们对于NIO有了一个基本的了解之后,接下来就要更多的去看看这种I/O的具体使用了:

        NIO是同步非阻塞的IO模型,实现模式是一个线程处理多个请求,采用轮询机制,不断的尝试有没有数据到达。为了实现这种结构,NIO的设计里出现了三大核心:Buffer(缓冲区),Channel (通道),和Selectors(选择器/多路复用器)。

 下面参考了文章:   三.Netty入门到超神系列-Java NIO 三大核心(selector,channel,buffer)_墨家@俏如来-CSDN博客

感谢刚哥让我省了很多事

Channel

在NIO中,用的比较频繁的就是FileChannel,所以我们用这个来举例说明channel的作用:

我们很容易就可以在Java中找到他的源码,源码的注释说明也很详细:

        A channel for reading, writing, mapping, and manipulating a file.
        A file channel is a SeekableByteChannel that is connected to a file. It has a current position within its file which can be both queried and modified. The file itself contains a variable-length sequence of bytes that can be read and written and whose current size can be queried. The size of the file increases when bytes are written beyond its current size; the size of the file decreases when it is truncated. The file may also have some associated metadata such as access permissions, content type, and last-modification time; this class does not define methods for metadata access.
        In addition to the familiar read, write, and close operations of byte channels, this class defines the following file-specific operations:

  • Bytes may be read or written at an absolute position in a file in a way that does not affect the channel's current position.
  • A region of a file may be mapped directly into memory; for large files this is often much more efficient than invoking the usual read or write methods.
  • Updates made to a file may be forced out to the underlying storage device, ensuring that data are not lost in the event of a system crash.
  • Bytes can be transferred from a file to some other channel, and vice versa, in a way that can be optimized by many operating systems into a very fast transfer directly to or from the filesystem cache.
  • A region of a file may be locked against access by other programs.

那么我觉得比较重点的几个点就是:

        1.Channel是用于读取、写入、映射和操作文件的通道。

        2.字节可以从一个文件传输到另一个通道,反之亦然,这种传输方式可以由许多操作系统优化为直接到文件系统缓存或从文件系统缓存快速传输。

这就说明了Channel再NIO的一个重要作用:双向读写。

        在nio的channel中,有如下几个常用的channel:

  • FileChannel:主要用于文件的IO操作
  • DatagramChannel:主要用于 UDP 的数据读写
  • ServerSocketChannel 和 SocketChannel:用于TCP数据读取

所以我对于channel的理解就是一个输入输出的通道。

Buffer

buffer主要是和channel通道做数据交互,可以看做是有一个数组来存储元素。

在nio的buffer类中注释是这样的:

        A container for data of a specific primitive type.
        A buffer is a linear, finite sequence of elements of a specific primitive type. Aside from its content, the essential properties of a buffer are its capacity, limit, and position:
        A buffer's capacity is the number of elements it contains. The capacity of a buffer is never negative and never changes.
        A buffer's limit is the index of the first element that should not be read or written. A buffer's limit is never negative and is never greater than its capacity.
        A buffer's position is the index of the next element to be read or written. A buffer's position is never negative and is never greater than its limit.
        There is one subclass of this class for each non-boolean primitive type.

那么我觉得比较重点的几个点就是:

        buffer是用于存储特定元素类型数据的容器。

        缓冲区的容量是它包含的元素数。缓冲区的容量永远不会为负,也永远不会改变。

通过这两条性质我们就可以对缓冲区做出很多操作了。

        我们来看看你Buffer源码中四个很重要的属性:


    /**
     * Returns this buffer's capacity.
     * Buffer所能够存放的最大容量,最多只能向 Buffer 写入 capacity 大小的字节
     */
    public final int capacity() {
        return capacity;
    }

    /**
     * Returns this buffer's position.
     * 下一个被读或写的位置,随着不停的写入数据,position会向后移动。
     * 初始值是0,最大值是capacity - 1。
     */
    public final int position() {
        return position;
    }
    
    /**
    * Sets this buffer's position. 
    * If the mark is defined and larger than the 
    * new position then it is discarded  
    *   设置此缓冲区的位置。
    *   如果标记已定义且大于新位置,则该标记将被丢弃
    */
    
    /**
     * Returns this buffer's limit.
     * 对position的限制,在写模式下限制你能将多少数据写入Buffer中,limit等同于Buffer的容量        (capacity)
     */
    public final int limit() {
        return limit;
    }

    /**
    * Sets this buffer's limit. 
    * If the position is larger than the new limit then it is set to the new limit. 
    * If the mark is defined and larger than the new limit then it is discarded.
    * 设置此缓冲区的限制。
    * 如果位置大于新限制,则设置为新限制。
    * 如果标记已定义且大于新限值,则该标记将被丢弃。
    */

    /**
     * Sets this buffer's mark at its position.
     * 标记位置,用于记录某次读写的位置
     */
    public final Buffer mark() {
        mark = position;
        return this;
    }
    /**
    * Resets this buffer's position to the previously-marked position.
    * Invoking this method neither changes nor discards the mark's value.
    * 将此缓冲区的位置重置为先前标记的位置。
    * 调用此方法既不会更改也不会丢弃标记的值。
    */

所以对于Buffer和Channel,我们用一个简单的例子来实战一下可以更好的说明I/O的实现:

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

public class IOTest {
    public static void main(String[] args) throws IOException {
        //生成100以内随机数
        int i = (int) (Math.random() * 100);
        //为了防止重复采用随机数地址
        String location = "d:/demo"+i+".txt";
        //调用写入测试
        nioW1iteTest(location);
        //读取测试
        nioReadTest(location);
        String location2 = "d:/out"+i+".txt";
        nioFileCopyTest(location,location2);
    }

    /*
    *
    *nio写入测试
    * */
    public static void nioW1iteTest(String location) throws IOException {
        //文件输出流
        FileOutputStream fileOutputStream = new FileOutputStream(location);
        //获取通道
        FileChannel channel = fileOutputStream.getChannel();
        //构建缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //给buffer写入数据
        System.out.println(byteBuffer.getClass().getName());

        byteBuffer.put("NIO".getBytes(StandardCharsets.UTF_8));
        //转换
        byteBuffer.flip();
        //写入磁盘
        channel.write(byteBuffer);
        //关闭通道
        channel.close();
        //关闭输出流
        fileOutputStream.close();
    }
    /*
     *
     *nio读取测试
     * */
    public static void nioReadTest(String location) throws IOException {
        //创建文件对象
        File file =new File(location);
        //创建文件输入流
        FileInputStream fileInputStream = new FileInputStream(file);
        //获取通道
        FileChannel channel = fileInputStream.getChannel();
        //根据文件长度创建buffer
        ByteBuffer byteBuffer =ByteBuffer.allocate((int)file.length());
        //读取文件
        channel.read(byteBuffer);
        System.out.println(new String(byteBuffer.array()));
        //关闭通道
        channel.close();
        //关闭流
        fileInputStream.close();
    }

    /*
    * NIO文件拷贝:
    *创建一个FileInputStream,目的是读取磁盘的某个文件
     通过FileInputStream得到FileChannel
     创建一个ByteBuffer用来接收数据
     调用 channel.read 把数据写入bytebuffer
     创建FileOutputStream,目的是把数据写到另外一个文件
     通过FileOutputStream得到FileChannel通道
     调用channel.write,把ByteBuffer中的数据写入FileChannel,从而写到磁盘文件
    * */
    public static void nioFileCopyTest(String location,String location2) throws IOException {
        //创建文件对象
        File file = new File(location);
        //创建输入流
        FileInputStream fileInputStream = new FileInputStream(file);
        //创建输出流
        FileOutputStream fileOutputStream = new FileOutputStream(location2);

        //获取输入通道
        FileChannel inChannel = fileInputStream.getChannel();
        //获取输出通道
        FileChannel outChannel = fileOutputStream.getChannel();
        //创建Buffer接收数据
        ByteBuffer buffer = ByteBuffer.allocate((int)file.length());

        //把数据写入bytebuffer
        inChannel.read(buffer);
        //转换
        buffer.flip();
        //把bytebuffer的数据写入输出流
        outChannel.write(buffer);

        //关闭输入通道
        inChannel.close();
        //关闭输入流
        fileInputStream.close();
        //关闭输出通道
        outChannel.close();
        //关闭输出流
        fileOutputStream.close();
    }
}

        NIO强大的点除了以上的双向输入输出以外,Selectors是一个很强大的设计他在NIO实现多路复用切换的时候起到了重大的作用。NIO有一个特点就是当有信息传输的时候,才会进行通道的切换,这样节省消耗提高性能,那么selectors是如何作用的呢:

Selector

我们来看看这个图和代码来分析:

import org.junit.Test;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
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 SelectorTest {
    //通道
    @Test
    public void serverSocketChannelTest() throws IOException {

        //创建服务端通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //socket监听地址和端口
        SocketAddress socketAddress = new InetSocketAddress("127.0.0.1",5000);

        //和某个SocketAddress绑定
        serverSocketChannel.bind(socketAddress);

        //NIO默认采用阻塞,为了兼容BIO
        serverSocketChannel.configureBlocking(false);

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

        //通道注册到选择器,事件类型为:OP_ACCEPT “接受”
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //==选择器轮询=======================================================================

        while(true){
            //select,选择有事件的通道,返回有事件发生通道的key的个数  ,超时时间 1s
            if(selector.select(1000) == 0){
                System.out.println("无连接...轮询等待...");
                continue;
            }
            //有事件发生,得到有事件的通道的key的集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            //遍历key的集合
            while (iterator.hasNext()){
                //拿到每个通道的key
                SelectionKey key = iterator.next();
                //如果当前通道事件是: OP_ACCEPT ,就注册通道
                if(key.isAcceptable()){
                    //接收一个socketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("客户端链接成功...");
                    socketChannel.configureBlocking(false);
                    //把socketChannel注册到选择器 ,并给通道绑定一个buffer
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }

                //如果通道事件是: OP_READ,说明通道有数据
                if(key.isReadable()){
                    //通过key得到SocketChannel
                    SocketChannel channel = (SocketChannel)key.channel();
                    //得到channel绑定的buffer
                    ByteBuffer byteBuffer = (ByteBuffer)key.attachment();
                    //从通道把数据读取到buffer
                    channel.read(byteBuffer);

                    System.out.println(new String(byteBuffer.array()));
                }
                //删除当前key
                iterator.remove();
            }
        }
    }

    //通道
    @Test
    public void socketChannelTest() throws IOException {
        //创建一个SocketChannel
        SocketChannel socketChannel = SocketChannel.open();

        //使用非阻塞模式
        socketChannel.configureBlocking(false);

        //链接的地址和端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 5000);
        //尝试链接,如果使用的异步,那么需要使用 socketChannel.finishConnect() 来确保连接成功。
        if(!socketChannel.connect(inetSocketAddress)){
            //如果没链接成功,会通过while循环,直到 finishConnect 链接成功,跳出while
            while(!socketChannel.finishConnect()){
                System.out.println("还未完成链接...等待中...");
            }
        }

        //链接成功,把数据写出去
        socketChannel.write(ByteBuffer.wrap("你好".getBytes()));
        System.out.println("向服务端发送数据...");
        //防止客户端结束,所以使用read()阻塞
        System.in.read();
    }

}

        我们可以看到,通过客户端与服务端的收发,根据是否有数据,我们的选择器可以判断并且执行相应的操作,具体的我们点进去看看源码。

selector

        首先我们看看selecter的注释说明:

A multiplexor of SelectableChannel objects.
A selector may be created by invoking the open method of this class, which will use the system's default selector provider to create a new selector. A selector may also be created by invoking the openSelector method of a custom selector provider. A selector remains open until it is closed via its close method.
A selectable channel's registration with a selector is represented by a SelectionKey object. A selector maintains three sets of selection keys:

  • The key set contains the keys representing the current channel registrations of this selector. This set is returned by the keys method.
  • The selected-key set is the set of keys such that each key's channel was detected to be ready for at least one of the operations identified in the key's interest set during a prior selection operation. This set is returned by the selectedKeys method. The selected-key set is always a subset of the key set.
  • The cancelled-key set is the set of keys that have been cancelled but whose channels have not yet been deregistered. This set is not directly accessible. The cancelled-key set is always a subset of the key set.

All three sets are empty in a newly-created selector.

        简单说明一下:

        选择器的创建方法有两种,第一种是我们可以通过调用此类的open方法来创建选择器;第二种是我们还可以通过调用自定义选择器提供程序的openSelector方法来创建选择器。上面的代码用的是.open的形式。在创建了选择器之后,他会一直保持打开的状态,直到你主动关闭它。

        可选通道与选择器的注册由SelectionKey对象表示:

SelectionKey对象我们会涉及到几个方法:

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

密钥集包含注册在selector中等待IO操作(及有事件发生)channel的selectionKey。

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

 密钥集包含表示此选择器当前频道注册的密钥。此集合由keys方法返回。 

selectionKey是keys的子集。

还有一个概念就是cancelled-key:

cancelled-key是已取消但其频道尚未取消注册的密钥集。不能直接被访问,也始终是key的子集。

ServerSocketChannel

server-socket channel的注释如下:

A selectable channel for stream-oriented listening sockets.
A server-socket channel is created by invoking the open method of this class. It is not possible to create a channel for an arbitrary, pre-existing ServerSocket. A newly-created server-socket channel is open but not yet bound. An attempt to invoke the accept method of an unbound server-socket channel will cause a NotYetBoundException to be thrown. A server-socket channel can be bound by invoking one of the bind methods defined by this class.        

他就是一个面向流的侦听套接字的可选通道。我们了解过channel就不能理解他的用处。

他是服务端用来用来监听客户端Socket链接,通过accept方法可以获取客户端SocketChannel,从而将 SocketChannel 注册到Selector。

SocketChannel

socket channel的注释如下:

A selectable channel for stream-oriented connecting sockets.

A socket channel is created by invoking one of the open methods of this class. It is not possible to create a channel for an arbitrary, pre-existing socket. A newly-created socket channel is open but not yet connected. An attempt to invoke an I/O operation upon an unconnected channel will cause a NotYetConnectedException to be thrown. A socket channel can be connected by invoking its connect method; once connected, a socket channel remains connected until it is closed. Whether or not a socket channel is connected may be determined by invoking its isConnected method.

Socket channels support non-blocking connection: A socket channel may be created and the process of establishing the link to the remote socket may be initiated via the connect method for later completion by the finishConnect method. Whether or not a connection operation is in progress may be determined by invoking the isConnectionPending method.

Socket channels support asynchronous shutdown, which is similar to the asynchronous close operation specified in the Channel class. If the input side of a socket is shut down by one thread while another thread is blocked in a read operation on the socket's channel, then the read operation in the blocked thread will complete without reading any bytes and will return -1. If the output side of a socket is shut down by one thread while another thread is blocked in a write operation on the socket's channel, then the blocked thread will receive an AsynchronousCloseException.

socket channel就是一个面向流的连接插座的可选通道。也就是客户端一般用于接受的通道。

 一个客户端链接服务端就会产生通道:ServerChannel ,需要注册到Selector,被Selector监听通道的读写事件。ServerChannel 负责具体的读写,把缓冲区的数据写入通道,或者把通道中的数据写入缓冲区。

        其中的方法在上面的java程序中也有应用。总而言之,NIO通过selector,channel和buffer实现了同步非阻塞IO模型,达到了一个线程处理多个请求的目的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

PigeonEssence

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值