Java NIO 简介

Java NIO简介

JDK1.4版本之前,Java IO类库是阻塞IO;从1.4版本开始,引进了新的异步IO库,被称为Java New IO类库,简称 Java NIO,之前阻塞的称为Old IO,简称OIO。新的异步IO库的目标是让Java支持非阻塞IO,弥补OIO同步阻塞的不足,为标准的Java代码提供了高速的,面向缓冲区的IO。

Java NIO 三个核心组件

  • Channel (通道)
  • Buffer(缓冲区)
  • Selector(选择器)

Java NIO是属于IO多路复用模型,NIO组件提供了统一的API,为开发人员屏蔽了底层不同操作系统的差异。

NIO和OIO对比
  • OIO是面向流(Stream Oriented)的,NIO是面向缓冲区(Buffer Oriented)的。

    • 面向流

      指面向字节流或字符流,一般OIO操作中。以流式的方式顺序地从一个流(Stream)中读取一个或多个字节。因为顺序读取,所以不能随意改变读取指针的位置。

    • 面向缓冲区

      NIO中引入了Channel(通道)和Buffer(缓冲区)的概念。读取和写入只需要从通道中读取数据到缓冲区,或将数据从缓冲区中写入到通道中。并不是顺序操作,所以可以随意读取Buffer中任意位置的数据。

  • OIO操作是阻塞的,NIO操作是非阻塞的。

    • OIO 阻塞

      调用一个read方法读取一个文件的内容时候,调用read的线程会被阻塞住,直到read操作完成。

    • NIO 非阻塞

      调用read方法时,如果此时有数据,则read读取数据并返回。如果此时没有数据,则read直接返回,而不会阻塞当前线程。依靠的就是使用了通道和通道的多路复用技术。

  • OIO没有选择器(Selector)的概念,而NIO有选择器的概念。

    NIO实现是基于底层的选择器的系统调用。

    NIO的选择器是需要底层操作系统提供支持,而OIO不需要用到选择器。

缓冲区(Buffer)

缓冲区是面向流的OIO没有的,是NIO非阻塞的前提和基础。
通道的读取就是将数据从通道读取到缓冲区;通道的写入就是将数据从缓冲区写入到通道中。

NIO的Buffer(缓冲区)本质上是一个内存块,既可以写入数据,也可以从中读取数据。
NIO的Buffer类位于java.nio.Buffer是一个抽象类。
注意:是一个非线程安全的类

Buffers are not safe for use by multiple concurrent threads. If a buffer is to be used by more than one thread then access to the buffer should be controlled by appropriate synchronization.
Buffer类的主要子类

image-20220429094622353

除了MappedByteBuffer的其他7种覆盖了能在IO种传输的所有Java的基本数据类型,MappedByteBuffer是专门用于内存映射的ByteBuffer类型。

Buffer类的主要属性

  • mark: 备忘位置,mark在被标记前是未定义的

    可以将当前的positon读取位置临时存入mark中,需要时候再从mark标记恢复到position位置

    调用mark()方法来设置mark=position,再调用reset()可以让position恢复到mark标记的位置

    即position=mark

  • positon: 当前位置,缓冲区中下一个要被读或写的元素索引

    positon属性与缓冲区的读写模式有关。

    • 写入模式
      1. 刚进入写模式,position值为0,代表写入位置从头开始
      2. 每当一个数据写入缓冲区之后,position会向后移动到下一个可写位置
      3. 初始的position值为0,最大值为limit-1。
    • 读取模式 (模式切换需要使用flip翻转方法)
      1. 缓冲区刚开始进入到读模式,position值会被重置为0
      2. 从缓存区读取时,也是从position位置开始读,读取后向后移动到下一个可读位置
      3. position最大的值为最大可读上限limit
  • limit: 读写限制, 缓冲区中第一个不能被读或写的元素索引

    limit属性在读写模式下也有不同的含义

    • 写入模式

      limit属性值代表可以写入的数据最大上限

      在刚进入写模式时,limit的值会被设置为capacity最大容量值。

    • 读取模式

      切换成读模式时候,会将之前写入当前位置positon设置为读模式下limit值。

      意思就切换时候把limit值设置为刚刚写到哪儿了。

  • capacity: 缓冲区的最大容量。在缓冲区创建时设定,一经设定永远不能改变

Buffer类的重要方法
  • allocate() 创建缓冲区

    在使用Buffer(缓冲区)之前,首先需要获取Buffer子类的实例对象,并分配内存空间。

    通过调用子类的allocate()方法。

  • put() 写入到缓冲区

​ put了一个值进去,position元素位置从0变成1,limit以及capacity没变

  • flip() 翻转

    向缓存区写入数据后,并不能直接从里面读取数据。此时缓冲区还处于写模式,想要读取数据需要将缓冲区转换成读模式。flip()的作用就是将写入模式翻转成读取模式。

    沿着刚刚的put操作后,flip()翻转后对比上图可以发现

    • 首先设置limit的值为刚刚position最后写入的值
    • 然后把position值设为0,表示从头开始
    • 最后清除之前就的mark标记,mark标记之前是写模式下的临时绘制。
  • clear()清空缓冲区,变成写入模式

    • position值设为0,设置写入起始位置为0
    • limit值设置为capacity最大容量值,设置写入上限为最大容量
  • compact()压缩缓冲区,变成写入模式

  • rewind() 倒带

    image-20220506160339022

    • position值设为0,表示从头开始
    • 清除之前就的mark标记
    • limit 值不变
  • mark()和reset()

    mark方法作用是把当前position值保存到mark属性中

    reset方法是将mark值恢复到position中,正常配套使用。

Buffer使用的基本步骤
  • 创建Buffer类的实例对象,通过创建子类实例对象的allocate()方法

  • 调用put方法,将数据写入到缓冲区

  • 写入完成后,在读取数据之前,调用flip()方法,将缓冲区转换成读模式

  • 调用get方法,从缓冲区读取数据

  • 读取完成后,调用clear()或者compact()方法,将缓冲区转换成写入模式

通道(Channel)

在OIO中,同一个网络连接会关联到两个流:一个输入流(Input Stream),另外一个输出流(Ouput Stream)。通过这两个流,不断地进行输入和输出的操作。

在NIO中,同一个网络连接使用一个通道表示。所以NIO的IO操作都是从通道开始。

一个通道类似于OIO中两个流的结合体,既可以从通道读取,也可以向通道写入。

FileChannel文件通道
  • 常见方法

    image-20220506171251683

    • read(ByteBuffer dst)

      从通道读取数据并放入缓冲区

    • write(ByteBuffer src)

      把缓冲区数据写到通道中

    • transferFrom(ReadableByteChannel src,long position, long count)

      从目标通道中复制数据到当前通道

    • transferTo(long position, long count, WritableByteChannel target)

      把数据从当前通道复制到目标通道

  • 实际使用(图片可读性最好,就直接截图了)

    • 写数据到本地文件

      image-20220506172516725

    • 从本地文件读取数据

      image-20220506173204584

    • 基于read&write实现文件拷贝

      image-20220506174837462

    • 基于transferFrom()拷贝文件

      image-20220506175519118

SocketChannel套接字通道
ServerSocketChannel服务器嵌套字通道(服务器监听通道)

SocketChannel负责连接传输,对应OIO中的Socket类

ServerSocketChannel负责连接监听,对应OIO中断ServerSocket

demo需要配合Selector,放在后面

DatagramChannel数据报通道

区别于Socket套接字的TCP传输协议,UDP协议不是面向连接的协议。

Selector选择器

是一个IO事件的查询器,通过选择器,一个线程可以查询多个通道的IO事件的就绪状态。

从开发层面即,首页把通道注册到选择器中,然后通过选择器的内部的机制。查询(select)这些注册的通道是否有已经就绪的IO事件(例如可读,可写,网络连接完成等)

一个选择器只需一个线程监控,即使用一个线程就可以通过选择器去管理多个通道,

与OIO相比,使用Selector选择器的最大优势:系统开销小,系统不必为每一个网络连接(文件描述符)创建进程/线程,从而减少系统开销。

通道IO事件类型
  • 可读就绪: SelectionKey.OP_READ

    有数据可读的SocketChannel通道

  • 可写就绪: SelectionKey.OP_WRITE (不明确应用场景,搞不懂

    有等待写入数据的通道

  • 连接就绪: SelectionKey.OP_CONNECT

    当SocketChannel通道完成了和对端的握手连接

  • 接收就绪: SelectionKey.OP_ACCEPT

    当ServerSocketChannel服务器通道监听到一个新连接到来

同时监控多个事件类型,通过"按位或"运算符实现

int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE
选择器的使用流程
  • 获取选择器实例

    // 调用静态工程方法open()来获取Selector实例
    Selector selector = Selector.open();
    
  • 将通道注册到选择器中

    // 获取通道
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    // 设置为非阻塞
    serverSocketChannel.configureBlocking(false);
    // 绑定连接
    serverSocketChannel.bind(new InetSocketAddress(80));
    // 将通道注册到选择器上,并制定监听事件
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    
  • 轮询感兴趣 的IO就绪事件(选择键集合)

    //轮询注册的IO事件
    while (selector.select() > 0) {
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        if (iterator.hasNext()) {
            SelectionKey key = iterator.next();
            if (key.isAcceptable()) {
                // 监听通道有新连接   
            }
            // 处理完成,移除选择键
            iterator.remove();
        }
    }
    
基于NIO 实现 demo
  • 读取客户端通道的输入数据

    • 服务端

      package com.wjl.nio;
      
      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 NioDiscardServer {
      
          public static void startServer() throws IOException {
              // 创建选择器
              Selector selector = Selector.open();
      
              // 创建通道
              ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
              // 设置非阻塞
              serverSocketChannel.configureBlocking(false);
              // 绑定端口
              serverSocketChannel.bind(new InetSocketAddress(88));
      
              // 注册接受就绪事件到选择器
              serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
      
              // 处理注册的事件
              while (selector.select() > 0) {
                  // 获取到所有的选择键
                  Set<SelectionKey> selectionKeys = selector.selectedKeys();
                  Iterator<SelectionKey> iterator = selectionKeys.iterator();
                  while (iterator.hasNext()) {
                      SelectionKey key = iterator.next();
                      // 如果选择键是"连接就绪"事件,就获取客户端连接
                      if (key.isAcceptable()) {
                          SocketChannel socketChannel = serverSocketChannel.accept();
                          // 设置非阻塞模式
                          socketChannel.configureBlocking(false);
                          // 将客户端连接的通道可读事件,注册到选择器上面.同时关联一个Buffer
                          socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                      }
                      // 如果选择键是"可读就绪"事件,读取数据
                      if (key.isReadable()) {
                          // 通过选择键反向获取到对应channel
                          SocketChannel socketChannel = (SocketChannel) key.channel();
                          // 获取到该channel关联的buffer
                          ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
                          socketChannel.read(byteBuffer);
                          System.out.println(new String(byteBuffer.array()));
                          // 关闭通道
                          socketChannel.close();
                      }
                      // 防止重复操作
                      iterator.remove();
                  }
              }
          }
      
          public static void main(String[] args) throws IOException {
              startServer();
          }
      }
      
    • 客户端

      import java.io.IOException;
      import java.net.InetSocketAddress;
      import java.nio.ByteBuffer;
      import java.nio.channels.SocketChannel;
      import java.nio.charset.StandardCharsets;
      
      public class NioDiscardClient {
      
          public static void main(String[] args) throws IOException {
              // 获取通道
              SocketChannel socketChannel = SocketChannel.open();
              // 设置非阻塞
              socketChannel.configureBlocking(false);
              // 提供服务器地址及端口
              InetSocketAddress socketAddress = new InetSocketAddress(88);
      
              // 连接服务器
              if (!socketChannel.connect(socketAddress)) {
                  while (!socketChannel.finishConnect()) {
                      System.out.println("------服务器连接中------");
                  }
              }
      
              String str = "hello, world";
              ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes(StandardCharsets.UTF_8));
              socketChannel.write(byteBuffer);
          }
      }
      
  • 实现NIO群聊系统

    • 服务器端

      检测用户上线,离线,并实现消息转发

      package com.wjl.chat;
      
      import java.io.IOException;
      import java.net.InetSocketAddress;
      import java.nio.ByteBuffer;
      import java.nio.channels.*;
      import java.util.Iterator;
      
      /**
       *  1.创建服务端ServerSocketChannel管道对象绑定端口
       *  2.注册接受就绪事件到选择器中(接受就绪: 当ServerSocketChannel服务器通道监听到一个新连接到来)
       *  3.监听选择器中是否有新的SocketChannel管道连接,当触发新连接时,注册该SocketChannel管道可读事件到选择器中
       *  4.当SocketChannel管道内有数据可读时候,触发可读事件转发到其他SocketChannel管道
       */
      public class GroupChatServer {
      
          private Selector selector;
          private ServerSocketChannel listenChannel;
      
          // 初始化服务器
          public GroupChatServer() {
              try {
                  // 获取选择器
                  this.selector = Selector.open();
                  // ServerSocketChannel
                  this.listenChannel =  ServerSocketChannel.open();
                  // 提供服务器地址及端口
                  InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 88);
                  // 绑定端口
                  this.listenChannel.socket().bind(socketAddress);
                  // 设置非阻塞模式
                  this.listenChannel.configureBlocking(false);
                  // 将该listenChannel 注册到selector
                  this.listenChannel.register(selector, SelectionKey.OP_ACCEPT);
              }catch (IOException e) {
                  e.printStackTrace();
              }
          }
      
          // 监听
          public void listen() {
              try {
                  // 循环处理
                  while (true) {
                      if(selector.select() > 0) {//有事件处理
      
                          //遍历得到selectionKey 集合
                          Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                          while (iterator.hasNext()) {
                              //取出selectionKey选择键
                              SelectionKey key = iterator.next();
      
                              // 监听到accept
                              if(key.isAcceptable()) {
                                  SocketChannel socketChannel = listenChannel.accept();
                                  socketChannel.configureBlocking(false);
                                  //将该 socketChannel 注册到 seletor
                                  socketChannel.register(selector, SelectionKey.OP_READ);
                                  System.out.println(socketChannel.getRemoteAddress() + " 上线 ");
                              }
                              // 通道发送read事件,即通道是可读的状态
                              if(key.isReadable()) {
                                  readData(key);
                              }
                              // 当前的key 删除,防止重复处理
                              iterator.remove();
                          }
      
                      } else {
                          System.out.println("等待....");
                      }
                  }
      
              }catch (Exception e) {
                  e.printStackTrace();
      
              }finally {
                  //发生异常处理....
      
              }
          }
      
          // 读取客户端消息
          private void readData(SelectionKey key) {
      
              // 取到关联的channle
              SocketChannel channel = null;
      
              try {
                 //得到channel
                  channel = (SocketChannel) key.channel();
                  //创建buffer
                  ByteBuffer buffer = ByteBuffer.allocate(1024);
      
                  int count = channel.read(buffer);
                  //根据count的值做处理
                  if(count > 0) {
                      //把缓存区的数据转成字符串
                      String msg = new String(buffer.array());
                      //输出该消息
                      System.out.println("form 客户端: " + msg);
      
                      //向其它的客户端转发消息(去掉自己), 专门写一个方法来处理
                      sendInfoToOtherClients(msg, channel);
                  }
      
              }catch (IOException e) {
                  try {
                      System.out.println(channel.getRemoteAddress() + " 离线了..");
                      //取消注册
                      key.cancel();
                      //关闭通道
                      channel.close();
                  }catch (IOException e2) {
                      e2.printStackTrace();;
                  }
              }
          }
      
          //转发消息给其它客户(通道)
          private void sendInfoToOtherClients(String msg, SocketChannel self ) throws  IOException{
      
              System.out.println("服务器转发消息中...");
              System.out.println("服务器转发数据给客户端: " + Thread.currentThread().getName());
              //遍历 所有注册到selector 上的 SocketChannel,并排除 self
              for(SelectionKey key: selector.keys()) {
                  //通过 key  取出对应的 SocketChannel
                  Channel targetChannel = key.channel();
                  //排除自己
                  if(targetChannel instanceof  SocketChannel && targetChannel != self) {
                      //转型
                      SocketChannel dest = (SocketChannel)targetChannel;
                      // 将msg 存储到buffer
                      ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                      //将buffer 的数据写入 通道
                      dest.write(buffer);
                  }
              }
      
          }
      
          public static void main(String[] args) {
              // 创建服务器对象
              GroupChatServer groupChatServer = new GroupChatServer();
              groupChatServer.listen();
          }
      }
      
    • 客户端

      通过channel实现无阻塞发送消息给其他所有用户,并接受其他用户发送消息

      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.SocketChannel;
      import java.util.Iterator;
      import java.util.Scanner;
      
      /**
       *  1.创建SocketChannel管道对象连接服务端ServerSocketChannel绑定的ip及端口
       *  2.将当前SocketChannel管道注册可读事件到选择器中
       *  3.定时获取选择器中其他管道关联的Buffer数据
       *  4.获取键盘输入写入SocketChannel管道中的buffer
       */
      public class GroupChatClient {
      
          private final Selector selector;
          private final SocketChannel socketChannel;
          private final String clientName;
      
          // 初始化客户端
          public GroupChatClient(String clientName) throws IOException {
              this.clientName = clientName;
              this.selector = Selector.open();
              // 提供服务器地址及端口
              InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 88);
              this.socketChannel = SocketChannel.open(socketAddress);
              //设置非阻塞
              this.socketChannel.configureBlocking(false);
              //将channel 注册到selector
              socketChannel.register(selector, SelectionKey.OP_READ);
          }
      
          // 向服务器发送消息
          public void sendInfo(String info) {
              info = clientName + " 说:" + info;
              try {
                  socketChannel.write(ByteBuffer.wrap(info.getBytes()));
              }catch (IOException e) {
                  e.printStackTrace();
              }
          }
      
          // 读取从服务器端回复的消息
          public void readInfo() {
              try {
                  int readChannels = selector.select();
                  // 有可以用的通道
                  if(readChannels > 0) {
                      Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                      while (iterator.hasNext()) {
      
                          SelectionKey key = iterator.next();
                          // 如果选择键是"可读就绪"事件,读取数据
                          if(key.isReadable()) {
                              // 通过选择键反向获取到对应channel
                             SocketChannel socketChannel = (SocketChannel) key.channel();
                             // 获取到该channel关联的buffer
                              ByteBuffer buffer = ByteBuffer.allocate(1024);
                              //读取
                              socketChannel.read(buffer);
                              System.out.println(new String(buffer.array()).trim());
                          }
                      }
                      // 删除当前的selectionKey, 防止重复操作
                      iterator.remove();
                  } else {
                      System.out.println("没有可以用的通道...");
                  }
              }catch (Exception e) {
                  e.printStackTrace();
              }
          }
      
          public static void main(String[] args) throws Exception {
              // 启动客户端
              final GroupChatClient chatClient = new GroupChatClient("张三");
      
              // 另启动一个线程, 每间隔3秒,读取从服务器发送数据
              new Thread(() -> {
                  while (true) {
                      chatClient.readInfo();
                      try {
                          Thread.sleep(3000);
                      }catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              }).start();
      
              //发送数据给服务器端
              Scanner scanner = new Scanner(System.in);
              while (scanner.hasNextLine()) {
                  String str = scanner.nextLine();
                  chatClient.sendInfo(str);
              }
          }
      }
      
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值