NIO

NIO

是什么

  • New IO,提供与标准IO不同的IO工作方式,标准IO的工作方式是基于字节流和字符流进行操作的,而NIO是基于通道和缓存区进行操作的
  • Non-blocking IO,可以非阻塞的使用IO,当线程从通道读取数据到数缓冲区时,线程还是可以进行其他的事情,当数据写入到缓冲区时,线程可以继续处理
  • NIO还引入了选择器的概念,选择器用于监听多个通道的事件,如连接打开和数据到达,因此单个线程可以监听多个数据通道

与IO比较

比较对象IONIO
面向对象面向流面向缓冲
IO方式阻塞IO非阻塞IO
管理者选择器
  • 面向流与面向缓冲
    • IO是面向流的,NIO是面向缓冲区的,IO面向流意味着每次从流中读一个或多个字节,直至读取所有的字节,他们没有被缓存在任何地方,此外它不能前后移动流中的数据,如果需要,得先将它们存到一个缓冲区。NIO的缓冲可以在缓冲区直接进行前后移动
  • 阻塞与非阻塞
    • IO的各种流是阻塞的,当调用read或write时,该线程被阻塞,这时的线程不能再做其他事情了。NIO的非阻塞式模式,使一个线程从某通道发送请求数据读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他事情,非阻塞写也是如此,所以一个单独的线程现在可以管理多个输入和输出通道
  • 选择器
    • NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或选择已准备写入的通道,这种选择机制,使得一个单独的线程很容易来管理多个通道

核心组件

  • Channels
  • Buffers
  • Selectors
  • :Channel、Butter和Selector构成了核心的API,虽然Java NIO中除此之外还有很多类和组件,例如Piple和FileLock,它们是与三个核心组件共同使用的工具类

Channel

  • 通道,类似流,但有些不同
    • 既可以从通道中读数据,又可以写数据到通道,但流的读写通常是单向的
    • 通道可以异步的读写
    • 通道中的数据总是先要读到一个Buffer,或者总要从一个Buffer中写入
  • Channel的主要类型
    • FileChannel【文件IO】
    • DatagramChannel【UDP IO】
    • SocketChannel【TCP IO】
    • ServerSocketChannel【TCP IO】
  • Channel例子

    //文件对象
    RandomAccessFile raf = new RandomAccessFile("test.txt", "rw");
    //获取通道
    FileChannel fc = raf.getChannel();
    //缓冲区
    ByteBuffer buf = ByteBuffer.allocate(48);
    
    int len = fc.read(buf);
    while (len != -1) {
        //转为读模式【limit=position;position=0】
        buf.flip();
        //hasRemaining()判断position是否小于limit
        while(buf.hasRemaining()){
            System.out.print((char) buf.get());
        }
        //清空缓存区
        buf.clear();
        //继续读取通道内容
        len = fc.read(buf);
    }
    raf.close();
    
  • Channel之间的传输

    • 如果两个Channel中有一个是FileChannel,那么可以直接从一个Channel传输到另外一个通道
    • transferFrom方法可以将数据从源通道传输到FileChannel中

    • transferTo方法,调用对象与transferFrom相对

      RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
      FileChannel      fromChannel = fromFile.getChannel();
      
      RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
      FileChannel      toChannel = toFile.getChannel();
      
      long position = 0;
      long count = fromChannel.size();
      
      fromChannel.transferTo(position, count, toChannel);
      
  • FileChannel

    • 是一个连接到文件的通道,可以通过文件通道读写文件,FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式
    • 打开FileChannel
      • 在使用FileChannel之前,必须先打开它,但是无法直接打开一个FileChannel,需要通过一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例
    • 向File Channel写入数据

      ByteBuffer buf = ByteBuffer.allocate(48);
      buf.clear();
      buf.put("hello world".getBytes());
      
      buf.flip();
      
      while(buf.hasRemaining()) {
          channel.write(buf);
      }
      
    • position()方法
      • 获取当前的位置
    • positio(long pos)
      • 设置当前的位置
    • size()方法
      • 返回所关联文件的大小
    • truncate(长度)
      • 截取前“长度”个字节
    • force()方法
      • 将通道里尚未写入磁盘的数据强制写到磁盘上
      • 参数 boolean:是否同时将文件元数据(权限信息等)写到磁盘
  • SocketChannel

    • SocketChannel是一个连接到TCP网络套接字的通道,可以通过以下2中方式创建SocketChannel
      1. 打开SocketChannel,并连接到服务器
      2. 一个新的连接到达ServerSocketChannel时,会创建一个SocketChannel
    • SocketChannel相关操作

      SocketChannel socketChannel = SocketChannel.open();
      socketChannel.connect(new InetSocketAddress("localhost", 8000));
      
      ByteBuffer buf = ByteBuffer.allocate(48);
      buf.clear();
      buf.put("hello world!".getBytes());
      
      buf.flip();
      
      while(buf.hasRemaining()) {
          channel.write(buf);
      }
      

       

      SocketChannel socketChannel = SocketChannel.open();
      socketChannel.configureBlocking(false);//设置为非阻塞式
      socketChannel.connect(new InetSocketAddress("localhost", 8000));
      
      while(! socketChannel.finishConnect() ){
      
      }
      
  • ServerSocketChannel
    • 可以监听新进来的TCP连接的通道,就像标准IO的ServerSocket一样
      ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
      serverSocketChannel.socket().bind(new InetSocketAddress(8000));
      serverSocketChannel.configureBlocking(false);//设置为非阻塞式
      while(true){
          //监听新进来的连接
          SocketChannel socketChannel =
                  serverSocketChannel.accept();
          //todo
      }
      
  • DataGramChannel

    • 是一个能收发UDP包的通道,因为UDP是无连接的网络协议,所以不能像其他通道那样读取和写入,它发送和接收的是数据包
    • DatagramChannel channel = DatagramChannel.open();
      channel.socket().bind(new InetSocketAddress(8001));
      
      //接收数据
      ByteBuffer buf = ByteBuffer.allocate(48);
      buf.clear();
      channel.receive(buf);//将数据包的内容复制到指定的Buffer,容不下的数据将丢弃
      

       

      //发送数据
      ByteBuffer buf = ByteBuffer.allocate(48);
      buf.clear();
      buf.put("hello world!".getBytes());
      buf.flip();
      //下面将数据发送到本地的8000端口,因为服务端没有监控这个端口,
      //所以可能什么也不会发生,也不会通知你发出的数据包是否已经收到,因为UDP在数据传送方面没有任何保证
      //因为UDP是无连接的,所以不会像tcp通道那样创建一条真正的连接,而是锁住DataGramChannel,让其从特定的位置收发数据
      int bytesSent = channel.send(buf, new InetSocketAddress("localhost", 8001));
      
    • 也可以使用read和write函数,只是在数据传送方面没有任何保证

Buffer

  • NIO中的Buffer用于和通道Channel进行交互,数据可以从Channel读到Buffer中,也可以从Buffer写到Channel中
  • 缓冲区本质上是一块可以进入数据,然后可以从中读取数据的内存,这块内存被包装成NIO Buffer对象,并提供方法方便使用内存
  • Bufer的主要类型
    • ByteBuffer
    • CharBuffer
    • DoubleBuffer
    • FloatBuffer
    • IntBuffer
    • LongBuffer
    • ShortBuffer
    • MappedByteBuffer【内存映射文件】
  • Buffer的基本用法
    1. 写入数据到Buffer
    2. 调用flip方法
    3. 从Buffer中读取数据
    4. 调用clear或compact方法
  • 当向Buffer写入数据时,Buffer会记录下写了多少数据,当读取数据时,需要flip方法将Buffer从写模式换到读模式,在读模式下,可以读取之前写入到Buffer的数据,一旦读完所有的数据,就需要清空缓冲区,让它可以再次被写入,有两种方式能清空缓冲区,调用clear或compact方法,clear会清空整个缓冲区,而compact方法只会清除已经读过的数据,任何未读的数据都被移到缓冲区的起始处,新的数据将放到缓冲区未读数据的后面
  • 例子

    //文件对象
    RandomAccessFile raf = new RandomAccessFile("test.txt", "rw");
    //获取通道
    FileChannel fc = raf.getChannel();
    //缓冲区
    ByteBuffer buf = ByteBuffer.allocate(48);
    
    int len = fc.read(buf);
    while (len != -1) {
        //转为读模式【limit=position;position=0】
        buf.flip();
        //hasRemaining()判断position是否小于limit
        while(buf.hasRemaining()){
            System.out.print((char) buf.get());
        }
        //清空缓存区
        buf.clear();
        //继续读取通道内容
        len = fc.read(buf);
    }
    raf.close();
    
  • Buffer的capacity,position和limit
    • capacity的含义是不变的,即缓冲区的容量,position和limit的含义取决于Buffer的读写模式
    • 写模式:position表示当前的位置,初始的position为0,当写入一个数据后,position会向前移动到下一个可插入数据的Buffer单元,position的最大值为capacity-1;limit表示最多往里面写多少数据,limit=capacity
    • 读模式:position通过flip函数将position重置为0,从Buffer开始位置进行读取数据;limit由写模式转为读模式时,会将limit的值重置为position写模式时的最后的位置
  • Buffer的分配
    • 要想获得一个Buffer对象,首先要进行分配,allocate方法
      CharBuffer buf = CharBuffer.allocate(1024);
      
  • 向Buffer写
    • int len = inChannel.read(buf);//通道->Buffer
    • buf.put(666);
  • 向Buffer读
    • fc.write(buf);//Buffer->通道
    • buf.get();
  • 方法
    • flip方法
      • 将Buffer的写模式切换为读模式,调用flip函数,会将limit置为position,position置为0
    • rewind方法
      • 将position置为0,limit不变
    • mark方法
      • 标记Buffer的一个特定的position
    • reset方法
      • 恢复到标记的特定的position
    • equals方法
      • 元素类型+元素个数+元素相同
    • compareTo方法
      • 第一个不相等的元素相减
      • 第一个元素耗尽

Selector

  • Selector允许单线程处理多个Channel,如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便,例如聊天室。
  • 要使用Selector得向Selector注册Channel,然后调用它的select()方法, 这个方法会一直阻塞到某个注册的通道事件就绪,一旦这个方法返回,线程就可以处理这些事件【新连接、数据接收等】
  • 选择器能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件,这样,一个单独的线程可以管理多个Channel,从而管理多个网络连接,对于操作系统来说,线程之间的上下文切换的开销很大,而且每个线程都要占用系统的资源,因此,线程越少越好
  • //创建Selector
    Selector selector = Selector.open();
    //将Channel设置为非阻塞
    channel.configureBlocking(false);
    //注册(选择器,感兴趣事件【Connect,Accept,Read,Write】)
    SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
    
  • SelectionKey常量
    • SelectionKey.OP_CONNECT
    • SelectionKey.OP_ACCEPT
    • SelectionKey.OP_READ
    • SelectionKey.OP_WRITE
  • 多个事件组合
    • int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
  • SelectionKey对象
    • interest集合
    • ready集合
    • Channel
    • Selector
    • 附加对象
  • interest集合

    • 所选择的感兴趣的事件集合
    • int interestSet = selectionKey.interestOps();
      
      boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
      boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
      boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
      boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;
      
  • ready集合
    • 已经准备好的操作的集合
    • int readySet = selectionKey.readyOps();
      
    • 也可以使用下面的方法
      selectionKey.isAcceptable();
      selectionKey.isConnectable();
      selectionKey.isReadable();
      selectionKey.isWritable();
      
  • 访问Channel和Selector
    Channel  channel  = selectionKey.channel();
    Selector selector = selectionKey.selector();
    
  • 附加对象
    //selectionKey对象添加
    selectionKey.attach(theObject);
    Object attachedObj = selectionKey.attachment();
    

     

    //注册时进行添加
    SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
    
  • 通过Selector选择通道

    • select();阻塞到至少有一个通道在注册的事件上就绪
      • select()方法返回的int值表示有多少通道已经就绪,即自上次调用select方法后有多少通道变成就绪状态
      • Set selectedKeys = selector.selectedKeys();//已选择的键集
      • Set selectedKeys = selector.selectedKeys();
        Iterator keyIterator = selectedKeys.iterator();
        while(keyIterator.hasNext()) {
            SelectionKey key = keyIterator.next();
            if(key.isAcceptable()) {
                // a connection was accepted by a ServerSocketChannel.
            } else if (key.isConnectable()) {
                // a connection was established with a remote server.
            } else if (key.isReadable()) {
                // a channel is ready for reading
            } else if (key.isWritable()) {
                // a channel is ready for writing
            }
            keyIterator.remove();
        }
        
      • wakeUp方法
        • 某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。阻塞在select()方法上的线程会立马返回。如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来(wake up)”。
      • close方法
        • 用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。
    • select(long timeout);最长会阻塞timeout毫秒,剩下的和select一样
    • selectNow();不会阻塞,不管什么通道就绪就立刻返回
    • 例-服务端

      import java.io.IOException;
      import java.net.InetSocketAddress;
      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 SelectorServer {
          public static void main(String[] args) {
              try {
                  ServerSocketChannel ssc = ServerSocketChannel.open();
                  ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000));
                  ssc.configureBlocking(false);
      
                  Selector selector = Selector.open();
                  // 注册 channel,并且指定感兴趣的事件是 Accept
                  ssc.register(selector, SelectionKey.OP_ACCEPT);
      
                  ByteBuffer readBuff = ByteBuffer.allocate(1024);
                  ByteBuffer writeBuff = ByteBuffer.allocate(128);
                  writeBuff.put("received".getBytes());
                  writeBuff.flip();
      
                  while (true) {
                      int nReady = selector.select();
                      Set<SelectionKey> keys = selector.selectedKeys();
                      Iterator<SelectionKey> it = keys.iterator();
      
                      while (it.hasNext()) {
                          SelectionKey key = it.next();
                          it.remove();
      
                          if (key.isAcceptable()) {
                              // 创建新的连接,并且把连接注册到selector上,而且,
                              // 声明这个channel只对读操作感兴趣。
                              SocketChannel socketChannel = ssc.accept();
                              socketChannel.configureBlocking(false);
                              socketChannel.register(selector, SelectionKey.OP_READ);
                          } else if (key.isReadable()) {
                              SocketChannel socketChannel = (SocketChannel) key.channel();
                              readBuff.clear();
                              socketChannel.read(readBuff);
      
                              readBuff.flip();
                              System.out.println("客户端发来:" + new String(readBuff.array()));
                              key.interestOps(SelectionKey.OP_WRITE);
                          } else if (key.isWritable()) {
                              writeBuff.rewind();
                              SocketChannel socketChannel = (SocketChannel) key.channel();
                              socketChannel.write(writeBuff);
                              key.interestOps(SelectionKey.OP_READ);
                          }
                      }
                  }
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
      }
      
    • 例-客户端

      import java.io.IOException;
      import java.net.InetSocketAddress;
      import java.nio.ByteBuffer;
      import java.nio.channels.SocketChannel;
      
      public class SelectorClient {
          public static void main(String[] args) throws IOException {
              try {
                  SocketChannel socketChannel = SocketChannel.open();
                  socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));
      
                  ByteBuffer writeBuffer = ByteBuffer.allocate(32);
                  ByteBuffer readBuffer = ByteBuffer.allocate(32);
      
                  writeBuffer.put("hello".getBytes());
                  writeBuffer.flip();
      
                  while (true) {
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      writeBuffer.rewind();
                      socketChannel.write(writeBuffer);
                      readBuffer.clear();
                      socketChannel.read(readBuffer);
                      System.out.println("服务端发来:" + new String(readBuffer.array()));
                  }
              } catch (IOException e) {
              }
          }
      }
      

Scatter【分散】和Gather【聚集】

  • Scatter,从Channel中读取写到多个Buffer中

    • 例如

      ByteBuffer header = ByteBuffer.allocate(128);
      ByteBuffer body   = ByteBuffer.allocate(1024);
      
      ByteBuffer[] bufferArray = { header, body };
      
      channel.read(bufferArray);
      
  • Gather,将多个Buffer写入同一个Channel

    • 例如

      ByteBuffer header = ByteBuffer.allocate(128);
      ByteBuffer body   = ByteBuffer.allocate(1024);
      
      ByteBuffer[] bufferArray = { header, body };
      
      channel.write(bufferArray);
      
  • 使用场景
    • 将传输数据分开处理的场合【例如消息头和消息体】

Pipe

  • Channel是两个线程之间的单向数据连接,Pipe有一个source通道和一个sink通道,数据会被写到sink通道,从source通道读取
  • Pipe pipe = Pipe.open();
    
    //写入数据
    Pipe.SinkChannel sinkChannel = pipe.sink();
    
    ByteBuffer buf = ByteBuffer.allocate(48);
    buf.clear();
    buf.put("hello world!".getBytes());
    
    buf.flip();
    
    while(buf.hasRemaining()) {
        sinkChannel.write(buf);
    }
    
    //读取数据
    Pipe.SourceChannel sourceChannel = pipe.source();
    ByteBuffer buf = ByteBuffer.allocate(48);
    int bytesRead = sourceChannel.read(buf);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

乘风御浪云帆之上

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

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

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

打赏作者

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

抵扣说明:

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

余额充值