NIO

作者: YaleWei | 来源:发表于2018-12-04 15:58 被阅读0次

    http://tutorials.jenkov.com/java-nio/index.html 原文地址
    Java NIO (New IO) is an alternative IO API for Java (from Java 1.4), meaning alternative to the standard Java IO and Java Networking API's. Java NIO offers a different way of working with IO than the standard IO API's

    • Java NIO: Channels and Buffers
      标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。

    • Java NIO: Non-blocking IO ( 非阻塞IO )
      Java NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。

    • Java NIO: Selectors ( 选择器 )
      Java NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。

    Channel

    jdk中的注释 :

    A nexus for I/O operations.
    A channel represents an open connection to an entity such as a hardware device, a file, a network socket, or a program component that is capable of performing one or more distinct I/O operations, for example reading or writing.

    Java NIO Channels 和Stream类似,但又有些不同:
    Channel既可以从通道中读取数据,又可以写数据到通道。但流通常是只读或只写的。
    Channel可以异步地读写。
    Channel总是读数据到一个Buffer,或者从一个Buffer中写数据到Channel。如下图所示:


    ReadAndWrite.png

    Channel的实现:

    ChannelImple.png

    FileChannel : 从文件中读写数据
    DatagramChannel : 通过UDP读写网络中的数据
    SocketChannel : 能通过TCP读写网络中的数据
    ServerSocketChannel : 可以监听指定端口TCP连接,对每一个新进来的连接都创建一个SocketChannel。

    Buffer

    使用Buffer读写数据一般遵循四个步骤 : 1. Channel.Read数据到Buffer中,或者直接Buffer.put(数据)。 2. 调用 Buffer.flip()切换Buffer到读模式。 3. Buffer.get()读取字节或者Channel.write(buffer)将Buffer中数据写入Channel。4. 调用Buffer.clear()或者Buffer.compact()。clear()方法清空整个缓冲区。compact()方法只会清除已读过的数据,未读数据移到Buffer的起始处。 下面是一个负值文件的example :

            String infile = "nio/src/main/resources/CopyFile.java";
            String outfile = "nio/src/main/resources/CopyFile.java.copy";
            File file = new File(infile);
    
            FileInputStream fin = new FileInputStream(file);
            FileOutputStream fout = new FileOutputStream(outfile);
    
            FileChannel finChannel = fin.getChannel();
            FileChannel foutChannel = fout.getChannel();
    
            ByteBuffer buffer = ByteBuffer.allocate(1024);
    
            while (true) {
                buffer.clear();
    
                int read = finChannel.read(buffer);
                if (read == -1) break;
    
                buffer.flip(); 
                foutChannel.write(buffer);
            }
    
    • Buffer 的 capacity position limit

    capacity : Buffer内存块的上限值,一旦Buffer满了需要清空数据才能继续往Buffer写数据。
    position : 写模式时,标识当前写的位置,初始值为0,最大值小于capacity。读模式时,标识当前读位置。写模式切换到读模式,position会被重置为0。
    limit : 在写模式下,limit==capacity,读模式下,limit表示能读到的数据最高位置。因此,切换Buffer到读模式时,limit会被设置成写模式下position值。

    • Buffer的类型

      BufferImpl.png
    • Buffer api

          //Buffer内存分配
          ByteBuffer buffer = ByteBuffer.allocate(1024);    //分配一个1024capacity的ByteBuffer
          
          //向Buffer中写入数据
          finChannel.read(buffer);  //read data from channel into buffer
          buffer.put((byte) 2); //put data into buffer
    
          //flip()
          buffer.flip(); //将Buffer从写模式切换到读模式
    
          //从Buffer中读取数据
          buffer.get(); //直接从Buffer中get数据
          foutChannel.write(buffer); //将Buffer中数据写入到Channel
    
          //rewind() 将position设置回0,可以用此重复读Buffer。
          buffer.rewind();
    
          //clear() 与compact()
          buffer.clear() //position设置回0,limit等于capacity,进入写模式。
          buffer.compact() //将未读数据拷贝到起始处,position设置到没有元素的位置,新写入数据不会覆盖未读数据。
    
          //mark() reset()
          buffer.mark(); //标记当前position
          buffer.get();  //get()一次 position后移
          buffer.reset(); //reset()position 复位到mark的位置
    
          //equals() 与 compareTo()
          buffer1.equals(buffer2); //比较Buffer重的剩余未读元素(position到limit之间的元素)相等
          buffer1.compareTo(buffer2);  //比较元素 第一个不相等的元素比较大小 或者 元素全部相等 元素个数多少比较
    

    Scatter/Gather

    A scattering read from a channel is a read operation that reads data into more than one buffer.
    分散读是指从Channel中读取数据写入到多个Buffer中,下面是示例图和Code :


    ScatteringReads.png
            ByteBuffer header = ByteBuffer.allocate(128);
            ByteBuffer body = ByteBuffer.allocate(1024);
    
            ByteBuffer[] buffers = {header, body};
            channel.read(buffers);
    

    A gathering write to a channel is a write operation that writes data from multiple buffers into a single channel.
    聚集写是指从多个Buffer中向一个Channel里写数据,下面是示例图和Code :


    GatheringWrites.png
    ByteBuffer header = ByteBuffer.allocate(128);
    ByteBuffer body   = ByteBuffer.allocate(1024);
    
    ByteBuffer[] bufferArray = { header, body };
    
    channel.write(bufferArray);
    

    Channel to Channel Transfers

    • transFrom()

    In Java NIO you can transfer data directly from one channel to another, if one of the channels is a FileChannel. The FileChannel class has a transferTo() and a transferFrom() method which does this for you.

            FileChannel finChannel = fin.getChannel();
            FileChannel foutChannel = fout.getChannel();
    
            long position = 0;
            long size = finChannel.size();
    
            foutChannel.transferFrom(finChannel,position,size);
    
    • transTo

    The transferTo() method transfer from a FileChannel into some other channel. Here is a simple example:

            FileChannel finChannel = fin.getChannel();
            FileChannel foutChannel = fout.getChannel();
    
            long position = 0;
            long size = finChannel.size();
    
            finChannel.transferTo(1,size,foutChannel);
    

    Selector

    A Selector is a Java NIO component which can examine one or more NIO Channel's, and determine which channels are ready for e.g. reading or writing. This way a single thread can manage multiple channels, and thus multiple network connections.

    • 为什么使用Selector

    仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。但是,需要记住,现代的操作系统和CPU在多任务方面表现的越来越好,所以多线程的开销随着时间的推移,变得越来越小了。实际上,如果一个CPU有多个内核,不使用多任务可能是在浪费CPU能力。

    • Selector api
        SocketChannel channel = SocketChannel.open();
            //创建Selector
            Selector selector = Selector.open();
    
            //向Selector注册通道
            //Channel必须处于非阻塞模式下才能与Selector使用,这意味着FileChannel不能使用Selector
            channel.configureBlocking(false);
    
            //将Channel注册到Selector上 并表示是可读的 返回key
            SelectionKey key = channel.register(selector, (SelectionKey.OP_READ | SelectionKey.OP_WRITE));
    
    //        通道四种interest状态 如果不止一种状态可以用 | 传多个
    //        SelectionKey.OP_READ;
    //        SelectionKey.OP_ACCEPT;
    //        SelectionKey.OP_CONNECT;
    //        SelectionKey.OP_WRITE;
    
            //SelectionKey interestSet
            int interestOps = key.interestOps(); //interest集合 返回了之前注册的 OP_READ | OP_WRITE == 5
            int readyOps = key.readyOps(); //ready集合 是已经准备就绪的操作集合 一次Selection后会首先访问这个ready set
    
            //可通过key获取 channel 和 selector
            SelectableChannel selectableChannel = key.channel();
            Selector selector1 = key.selector();
    
            //可以给key添加附加对象 标识区分key
            Object attach = key.attach(new Buffer[]{});
            Object attachment = key.attachment();
    
            //通过Selector选择通道
            //向Selector注册了通道后,可以调用select()方法,返回的int值表示多少通道已经就绪,这是个同步阻塞方法,若无就绪通道会等待直到有就绪通道.
            //selector.wakeup(); 可以唤醒阻塞select()方法 立马返回
            int ready = selector.select();
            //在确认ready channel > 0 后 获取就绪通道注册的keys
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
    
            //当像Selector注册Channel时,Channel.register()方法会返回一个SelectionKey 对象。
            //这个对象代表了注册到该Selector的通道。可以通过SelectionKey的selectedKeySet()方法访问这些对象。
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while (keyIterator.hasNext()){
                SelectionKey next = keyIterator.next();
                if (next.isAcceptable()){
                    SelectableChannel readyAcceptChannel = next.channel(); //获取通道
                }
                keyIterator.remove();
            }
    
            selector.close();//关闭selector 注册的keys全部失效 channel不会失效
    

    FileChannel

    A Java NIO FileChannel is a channel that is connected to a file. Using a file channel you can read data from a file, and write data to a file.

    SocketChannel

    A Java NIO SocketChannel is a channel that is connected to a TCP network socket.

            //Open a SocketChannel
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("localhost",8080));
    
            //Reading data from a SocketChannel into Buffer
            ByteBuffer buffer = ByteBuffer.allocate(48);
            socketChannel.read(buffer);
    
            //Writing data from Buffer into SocketChannel
            buffer.flip();
            while (buffer.hasRemaining()){
                socketChannel.write(buffer);
            }
    
            //Configure the SocketChannel be non-blocking mode,the method may return before a connection is established.
            //Then call the finishConnect() method to determine whether connection is established.
            socketChannel.configureBlocking(false);
            socketChannel.connect(new InetSocketAddress("localhost",8080));
            if (socketChannel.finishConnect()) {
                //TODO
            }
    
            //Closing a SocketChannel
            socketChannel.close();
    

    ServerSocketChannel

    A Java NIO ServerSocketChannel is a channel that can listen for incoming TCP connections.

            //Opening a ServerSocketChannel
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //Binds the {@code ServerSocket} to a specific address (IP address and port number).
            serverSocketChannel.socket().bind(new InetSocketAddress(8080));
    
            while (true){
                //Listening for Incoming Connections
                //The accept() method blocks until an incoming connection arrives,and returns a SocketChannel.
                SocketChannel socketChannel = serverSocketChannel.accept();
                //TODO ...
    
                //In non-blocking mode the accept() method returns immediately,
                // if no incoming connection had arrived,you will have to check if the returned channel is null.
                serverSocketChannel.configureBlocking(false);
                if (socketChannel!=null){
                    //TODO ...
                }
    
                //Closing a ServerSocketChannel
                serverSocketChannel.close();
            }
    

    DatagramChannel

    A Java NIO DatagramChannel is a channel that can send and receive UDP packets.

    Pipe

    A Java NIO Pipe is a one-way data connection between two threads. A Pipe has a source channel and a sink channel. You write data to the sink channel. This data can then be read from the source channel.Here is an illustration of the Pipe principle :


    Pipe Internals.png
            //Creating a Pipe.
            Pipe pipe = Pipe.open();
    
            //Writing to a Pipe.
            Pipe.SinkChannel sinkChannel = pipe.sink();//To write to a pipe you need to access the sink channel.
    
            ByteBuffer buffer = ByteBuffer.allocate(48);
            buffer.put(new byte[]{1,2,3,4,5});
            buffer.flip();
    
            while (buffer.hasRemaining()){
                sinkChannel.write(buffer); //Wiriting data from buffer into pipe.sinkChannel
            }
    
            //Reading from a Pipe
            Pipe.SourceChannel sourceChannel = pipe.source(); //To read from a pipe you need to access the channel.
            ByteBuffer buffer2 = ByteBuffer.allocate(48);
            int read = sourceChannel.read(buffer2);//Reading data from pipe.sourceChannel into buffer.
            buffer.flip();
            buffer2.flip();
            System.out.println(buffer.equals(buffer2)); //true
    

    NIO和IO比较

    Differences.png
    • Stream VS Buffer

    Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

    • Blocking vs. Non-blocking IO

    Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

    • Selectors

    ava NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

    相关文章

      网友评论

          本文标题:NIO

          本文链接:https://www.haomeiwen.com/subject/ygqycqtx.html