Java 网络编程之 NIO

Java 网络编程系列之 NIO

1.概述

  • Java NIO(New IO 或 Non Blocking IO)是从 Java 1.4 版本开始引入的一个新的IO API,可以替代标准的 Java IO API。
  • NIO 支持面向缓冲区的、基于通道的 IO 操作。
  • NIO 将以更加高效的方式进行文件的读写操作。

阻塞IO

  • 通常在进行I/O操作时,如果读取数据,代码会阻塞知道有可以读取的数据,写入操作同样时直到数据能够写入

  • I/O通道会一直开启,知道完成I/O操作。

  • 传统的CS(客户服务器)模型会为每一个请求建立一个线程来防止阻塞其他操作无法运行,例如在建立聊天室的连接时,如果有登陆界面需要进行界面的跳转,此时如果不为跳转界面分配单独的线程就有可能被I/O操作所阻塞,由此可知这样的模式带来了大量的线程时服务器开销增大

    image-20211123205518100

  • 大多数的实现为了避免这个问题,都采用了线程池模型,并设置线程池线程的最大数量,这由带来了新的问题,如果线程池中有 100 个线程,而有100 个用户都在进行大文件下载,会导致第 101 个用户的请求无法及时处理,即便第101 个用户只想请求一个几 KB 大小的页面也无法处理。

线程池

  • 构造方法:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
     
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }
     
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }
     
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    
  • 参数:

    • corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。

    • maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞

    • keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。

    • unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。

    • workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。

    • threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。

    • handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。

    • public void UseThreadPool(){
          // 创建线程池
          ThreadPoolExecutor threadPool = new ThreadPoolExecutor(int corePoolSize,
          int maximumPoolSize,
          long keepAliveTime,
          TimeUnit unit,
          BlockingQueue<Runnable> workQueue);
      // 向线程池提交任务
          threadPool.execute(new Runnable() {
            @Override
            public void run() {
               // 线程执行的任务
            }
          });
      // 关闭线程池
          threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
          threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
        }
      
  • 线程池的工作原理

    img

    原文链接:https://blog.csdn.net/u013541140/article/details/95225769

非阻塞IO(NIO)

NIO流程
  • NIO是当发生事件时,系统再通知我们,不像阻塞IO一直开启通道
  • NIO 中实现非阻塞 I/O 的核心对象就是 Selector,Selector 就是注册各种 I/O 事件地方,而且当我们感兴趣的事件发生时,就是这个对象告诉我们所发生的事件,如下图所示:

image-20211123212804417

从图中可以看出,当有读或写等任何注册的事件发生时,可以从 Selector 中获得相应的 SelectionKey,同时从 SelectionKey 中可以找到发生的事件和该事件所发生的具体的 SelectableChannel,以获得客户端发送过来的数据

  • NIO虽然是非阻塞事件,不同于阻塞IO堵塞IO事件,但是Selector()获取读写等的IO操作时是需要阻塞的。

  • NIO 阻塞在事件获取中,没有事件就没有 IO, 从高层次看 IO 就不阻塞了.也就是说只有 IO 已经发生那么我们才评估 IO 是否阻塞,但是select()阻塞的时候 IO 还没有发生**(select阻塞的获取读写操作,此时读写还未发生IO阻塞还未开始)**,何谈 IO 的阻塞呢?

  • NIO 的本质是延迟 IO 操作到真正发生 IO 的时候,而不是以前的只要 IO 流打开了就一直等待 IO 操作。

  • IO与NIO对比:

    image-20211123213748580

NIO概述
  • **NIO组成:**Java NIO 由以下几个核心部分组成:

    - Channels

    - Buffers

    - Selectors

    虽然 Java NIO 中除此之外还有很多类和组件,但 Channel,Buffer 和 Selector 构成了核心的 API。其它组件,如 Pipe 和 FileLock,只不过是与三个核心组件共同使用的工具类

  • Channel

    首先说一下 Channel,可以翻译成“通道”。Channel 和 IO 中的 Stream(流)是差不多一个等级的。只不过 Stream 是单向的,譬如:InputStream, OutputStream.而Channel 是双向的既可以用来进行读操作,又可以用来进行写操作。NIO 中的 Channel 的主要实现有:FileChannel、DatagramChannel、SocketChannel与ServerSocketChannel,这里看名字就可以猜出个所以然来:分别可以对应文件 IO、UDP 和 TCP(Server 和 Client)。

  • Buffer
    NIO 中的关键 Buffer 实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer,
    IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double,
    float, int, long, short。

  • Selector
    Selector 运行单线程处理多个 Channel,如果你的应用打开了多个通道,但每个连接
    的流量都很低,使用 Selector 就会很方便。例如在一个聊天服务器中。

  • 要使用Selector, 得向 Selector 注册 Channel,然后调用它的 select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。

2.java NIO(Channel)

Channel 概述

  • Channel 是一个通道,可以通过它读取和写入数据,它就像水管一样,网络数据通过
    Channel 读取和写入。
  • 通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而且通道可以用于读、写或者同时用于读写。因为 Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API。
  • NIO 中通过 channel 封装了对数据源的操作,通过 channel 我们可以操作数据源,但
    又不必关心数据源的具体物理结构。这个数据源可能是多种的。比如,可以是文件,
    也可以是网络 socket。
  • 在大多数应用中,channel 与文件描述符或者 socket 是一一对应的。Channel 用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据

Channel接口源码

public interface Channel extends Closeable {

    /**
     * Tells whether or not this channel is open.
     *
     * @return <tt>true</tt> if, and only if, this channel is open
     */
    public boolean isOpen();

    /**
     * Closes this channel.
     *
     * <p> After a channel is closed, any further attempt to invoke I/O
     * operations upon it will cause a {@link ClosedChannelException} to be
     * thrown.
     *
     * <p> If this channel is already closed then invoking this method has no
     * effect.
     *
     * <p> This method may be invoked at any time.  If some other thread has
     * already invoked it, however, then another invocation will block until
     * the first invocation is complete, after which it will return without
     * effect. </p>
     *
     * @throws  IOException  If an I/O error occurs
     */
    public void close() throws IOException;

}

Channel与缓冲区

  • 与缓冲区不同,通道 API 主要由接口指定。不同的操作系统上通道实现(Channel
    Implementation)会有根本性的差异,所以通道 API 仅仅描述了可以做什么。因此很
    自然地,通道实现经常使用操作系统的本地代码。通道接口允许您以一种受控且可移
    植的方式来访问底层的 I/O 服务。

  • Channel 是一个对象,可以通过它读取和写入数据,所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

  • Java NIO 的通道类似流,但又有些不同:

    • 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
    • 通道可以异步地读写。

    • 通道中的数据总是要先读到一个 Buffer,或者总是要从一个 Buffer 中写入。
      正如上面所说,从通道读取数据到缓冲区,从缓冲区写入数据到通道。如下图所示:

      image-20211123221134628

FileChannel 介绍和示例

介绍
  • API

    image-20211124092728819

  • Buffer 通常的操作

    • 将数据写入缓冲区
    • 调用 buffer.flip() 反转读写模式
    • 从缓冲区读取数据
    • 调用 buffer.clear() 或 buffer.compact() 清除缓冲区内容
public class FileChannelDemo {

  //FileChannel读取数据到buffer中
  public static void main(String[] args) throws IOException {
    //创建FileChannel,FileChannel不能直接创建需要先导入一个文件才能读取
    RandomAccessFile RandomAccessFile = new RandomAccessFile(
        "D:\\idea项目\\network\\src\\javaNIO\\sources\\1.txt", "rw");
    FileChannel channel = RandomAccessFile.getChannel();

    //创建buffer并分配大小
    ByteBuffer buffer = ByteBuffer.allocate(1024);

    //读取数据到buffer
    int byteread = channel.read(buffer);
    //byteread为-1表示读到了最后一行结束了
    while (byteread!=-1){
      //读取大小
      System.out.println("读取了"+byteread);
      //读写转换,数据读到了buffer中需要取出
      buffer.flip();
      //hasRemaining()为true则buffer不为空
      while (buffer.hasRemaining()){
        System.out.print((char)buffer.get());
      }
      //清楚缓冲区内容
      buffer.clear();
      //开始新一轮读取,一次只能读一个字节,可能读不完
      byteread=channel.read(buffer);
    }
    RandomAccessFile.close();
    System.out.println("读取结束");
  }

}
示例
  • 打开 FileChannel
    在使用 FileChannel 之前,必须先打开它。但是,我们无法直接打开一个FileChannel,需要通过使用一个 InputStream、OutputStream 或RandomAccessFile 来获取一个 FileChannel 实例。下面是通过 RandomAccessFile
    打开 FileChannel 的示例:
RandomAccessFile RandomAccessFile = new RandomAccessFile(
        "D:\\idea项目\\network\\src\\javaNIO\\sources\\1.txt", "rw");
    FileChannel channel = RandomAccessFile.getChannel();
  • 从 FileChannel 读取数据

    调用多个 read()方法之一从 FileChannel 中读取数据。如:

        //创建buffer并分配大小
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //读取数据到buffer
        int byteread = channel.read(buffer);
        //byteread为-1表示读到了最后一行结束了
    
    • 分配一个 Buffer。从 FileChannel 中读取的数据将被读到 Buffer 中。然后,调
      用 FileChannel.read()方法。该方法将数据从 FileChannel 读取到 Buffer 中。read()
      方法返回的 int 值表示了有多少字节被读到了 Buffer 中。如果返回-1,表示到了文件末尾。
    • 注意:此处与上文说过channel不会直接读取数据,而是将数据读入缓冲区,从缓冲区读取数据并不冲突,上文代码所做的就是将数据通过channel读入缓冲区。
  • 向 FileChannel 写数据

    讲解在代码中的注释

    public class FileChannelDemo2 {
    
      //向FileChannel中写数据
      public static void main(String[] args) throws IOException {
        //打开FileChannel
        RandomAccessFile RandomAccessFile = new RandomAccessFile(
            "D:\\idea项目\\network\\src\\javaNIO\\sources\\2.txt", "rw");
        FileChannel fileChannel = RandomAccessFile.getChannel();
    
        //创建一个buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
    
        String data="writing";
        buffer.clear();
        //写入内容
        buffer.put(data.getBytes());
    
        //读写转换,要从将buffer中的内容读出,再写入文件
        buffer.flip();
    
        //FileChannel的最终实现
        //无法保证写入多少字节所以要循环
        while (buffer.hasRemaining()){
          //FileChannel是从一个路径的文件获取到的,读是从这个文件读,写自然是写入这个文件
          fileChannel.write(buffer);
        }
    
        //关闭
        fileChannel.close();
      }
    }
    
方法
  • FileChannel 的 position 方法

    • 有时可能需要在 FileChannel 的某个特定位置进行数据的读/写操作。可以通过调用position()方法获取 FileChannel 的当前位置,然后通过调用position(long pos)方法设置 FileChannel 的当前位置。

    • 例如:文件1中内容是:1234,我想在4往后3个位置进行读写操作

      //pos通常为0
      long pos = channel.position();
      channel.position(pos +7);
      
    • **示例:**文件2.txt原本内容为 fir,现从第4个位置开始写入

      public class FileChannelAPI {
        public static void main(String[] args) throws IOException {
          RandomAccessFile RandomAccessFile = new RandomAccessFile(
              "D:\\idea项目\\network\\src\\javaNIO\\sources\\2.txt","rw");
          FileChannel channel = RandomAccessFile.getChannel();
      
          long pos=channel.position();
          System.out.println(pos);
          channel.position(pos+4);
      
          String add="567";
          ByteBuffer buffer = ByteBuffer.allocate(6);
          buffer.put(add.getBytes());
      
          buffer.flip();
      
          while (buffer.hasRemaining()){
            //将缓冲区的读出,通过通道写入文件
            channel.write(buffer);
          }
      
          channel.close();
        }
      }
      

      结果:

      image-20211124103635143

      • 如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。

      • 上图文件结束符是第三个位置,但是我是从第四个开始写,所以撑大了文件,造成文件空洞

        注意:如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回-1 (文件结束标志)。

  • FileChannel 的 size 方法

FileChannel 实例的 size()方法将返回该实例所关联文件的大小。如:

long fileSize = channel.size();
  • FileChannel 的 truncate 方法

可以使用 FileChannel.truncate()方法截取一个文件。截取文件时,文件将中指定长度
后面的部分将被删除。如:

channel.truncate(1024);
//这个例子截取文件的前 1024 个字节。
  • FileChannel 的 force 方法
    FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。出于性能方
    面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到 FileChannel 里的
    数据一定会即时写到磁盘上。要保证这一点,需要调用 force()方法。
    force()方法有一个 boolean 类型的参数,指明是否同时将文件元数据(权限信息等)
    写到磁盘上

  • FileChannel 的 transferTo 和 transferFrom 方法

  • **通道之间的数据传输:**如果两个通道中有一个是 FileChannel,那你可以直接将数据从一个 channel 传输到另外一个 channel

  • transferFrom()方法:FileChannel 的 transferFrom()方法可以将数据从源通道传输到 FileChannel 中(译者注:这个方法在 JDK 文档中的解释为将字节从给定的可读取字节通道传输到此通道的文件中)

    package javaNIO.Channel;
    
    import java.io.FileNotFoundException;
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.nio.channels.FileChannel;
    
    /**
     * @Description:transferfrom实现通道之间的数据传输
     * @Author: duanyf
     * @DateTime: 2021/11/24 0024 上午 11:04
     */
    public class FileChannelDemo3 {
    
    
      public static void main(String[] args) throws IOException {
        //创建两个FileChannel
        RandomAccessFile RandomAccessFile1 = new RandomAccessFile(
            "D:\\idea项目\\network\\src\\javaNIO\\sources\\Transfer01", "rw");
        RandomAccessFile RandomAccessFile2 = new RandomAccessFile(
            "D:\\idea项目\\network\\src\\javaNIO\\sources\\Transfer02", "rw");
        FileChannel channel1 = RandomAccessFile1.getChannel();
        FileChannel channel2 = RandomAccessFile2.getChannel();
        //channel1中的数据传输到channel2中去
        long size = channel1.size();
        long pos=0;
        channel2.transferFrom(channel1,pos,size);
    
        channel1.close();
        channel2.close();
        System.out.println("over!");
    
      }
    }
    
    

    ​ 方法的输入参数 position 表示从 position 处开始向目标文件写入数据,count 表示最多传输的字节数。如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数。
    ​ 此外要注意,在SoketChannel 的实现中,SocketChannel 只会传输此刻准备好的数据(可能不足 count 字节)。因此,SocketChannel 可能不会将请求的所有数据(count 个字节)全部传输到 FileChannel 中。

  • transferTo()方法将数据从 FileChannel 传输到其他的 channel 中。

    package javaNIO.Channel;
    
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.nio.channels.FileChannel;
    
    /**
     * @Description:transferto实现通道之间的数据传输
     * @Author: duanyf
     * @DateTime: 2021/11/24 0024 上午 11:14
     */
    public class FileChannelDemo4 {
    
    
      public static void main(String[] args) throws IOException {
        //创建两个FileChannel
        RandomAccessFile RandomAccessFile1 = new RandomAccessFile(
            "D:\\idea项目\\network\\src\\javaNIO\\sources\\Transfer01", "rw");
        RandomAccessFile RandomAccessFile2 = new RandomAccessFile(
            "D:\\idea项目\\network\\src\\javaNIO\\sources\\Transfer02", "rw");
        FileChannel channel1 = RandomAccessFile1.getChannel();
        FileChannel channel2 = RandomAccessFile2.getChannel();
        //channel1中的数据传输到channel2中去
        long size = channel1.size();
        long pos=0;
    //    channel2.transferFrom(channel1,pos,size);
        channel1.transferTo(pos,size,channel2);
        channel1.close();
        channel2.close();
        System.out.println("over!");
    
      }
    }
    
    

Socket通道

  • Socket通道类是可以运行非阻塞的且也可以运行阻塞模式,这带来了极大的灵活性

  • 新的Socket通道类,不必为每个Socket都创建一个线程,借助NIO一个线程就能管理许多Socket的连接。

  • socket 通道类(DatagramChannel、SocketChannel 和 ServerSocketChannel)都继承了位于 java.nio.channels.spi 包中的 AbstractSelectableChannel。这意味着我们可以用一个Selector 对象来执行socket 通道的就绪选择(readiness selection)。

  • DatagramChannel 和 SocketChannel 实现定义读和写功能的接口而ServerSocketChannel 不实现。ServerSocketChannel 负责监听传入的连接和创建新的 SocketChannel 对象,它本身从不传输数据

  • **Socket与Socket通道的区别:**通道可重复使用。

  • 全部 socket 通道类(DatagramChannel、SocketChannel 和ServerSocketChannel)在被实例化时都会创建一个对等 socket 对象。这些是我们所熟悉的来自 java.net 的类(Socket、ServerSocket 和 DatagramSocket)。

  • socket 通道类所创建的Socket对象(来自Socket类、ServerSocket 类和 DatagramSocket类 )已被更新可以识别通道,都可以调用getChannel()方法。

  • 非阻塞IO与阻塞IO的区别就是延迟阻塞至IO操作而不是IO事件,就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标IO操作(如读或写)。

  • 设置或重新设置一个通道的阻塞模式是很简单的,只要调用 configureBlocking( )方
    法即可,传递参数值为 true 则设为阻塞模式,参数值为 false 值设为非阻塞模式。可
    以通过调用 isBlocking( )方法来判断某个 socket 通道当前处于哪种模式。

  • 非阻塞 socket的使用与锁:

    • 非阻塞 socket 通常被认为是服务端使用的,因为它们使同时管理很多 socket 通道变得更容易,有时也有益于客户端
    • 偶尔地,我们也会需要防止 socket 通道的阻塞模式被更改。API 中有一个blockingLock( )方法,该方法会返回一个非透明的对象引用。返回的对象是通道实现修改阻塞模式时内部使用的,只有拥有此对象的锁的线程才能更改通道的阻塞模式。

ServerSocketChannel

概述:

  • ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉的java.net.ServerSocket 执行相同的任务,不过增加了Channel的概念,因此能够在非阻塞模式运行

  • ServerSocketChannel 没有 bind()方法,因此有必要取出对等的 socket 并使用它来绑定到一个端口以开始监听连接

  • ServerSocketChannel 的 accept()方法会返回 SocketChannel 类型对象,SocketChannel 可以在非阻塞模式下运行。

  • 如果ServerSocketChannel 以非阻塞模式被调用,当没有传入连接在等待时,
    ServerSocketChannel.accept( )会立即返回 null

    image-20211125001624140

代码:

package javaNIO.serverSocketChannel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

/**
 * @Description:如何使用一个非阻塞的 accept()方法
 * @Author: duanyf
 * @DateTime: 2021/11/24 0024 下午 22:52
 */
public class serverSocketChannelDemo {

  public static void main(String[] args) throws IOException, InterruptedException {
    //端口号
    int port = 8888;


    //buffer
    ByteBuffer Buffer = ByteBuffer.wrap("hello java nio".getBytes());

    //ServerSocketChannel
    ServerSocketChannel ssc=ServerSocketChannel.open();

    //绑定
    ssc.socket().bind(new InetSocketAddress(port));

    //ServerSocketChannel设置阻塞模式
    ssc.configureBlocking(false);	
    //监听有链接传入
    while (true) {
      SocketChannel socketChannel = ssc.accept();
      //此行输出语句因为是非阻塞才能执行,若是阻塞必须等有连接才能执行
      System.out.println("正在等待连接........");
      if (socketChannel == null) {
        System.out.println("无连接.....");
        Thread.sleep(2000);
      }else{
        System.out.println("连接来自:"+ socketChannel.socket().getRemoteSocketAddress());
        //指向0
        Buffer.rewind();
        //将buffer写入SocketChannel传给客户端
        socketChannel.write(Buffer);
        socketChannel.close();
      }

    }
  }
}

结果:

  • 客户端采用talnet 127.0.0.1 8888

    image-20211125001806955

  • 服务端

    image-20211125001624140

SocketChannel

介绍

  • Java NIO 中的 SocketChannel 是一个连接到 TCP 网络套接字的通道。
  • SocketChannel 是一种面向流连接sockets 套接字的可选择通道。从这里可以看出:
    • SocketChannel 是用来连接 Socket 套接字
    • SocketChannel 主要用途用来处理网络 I/O 的通道
    • SocketChannel 是基于 TCP 连接传输
    • SocketChannel 实现了可选择通道,可以被多路复用

特征:

  • 对于已经存在的 socket 不能创建 SocketChannel。

  • SocketChannel 中提供的 open 接口创建的 Channel 并没有进行网络级联,需要使
    connect 接口连接到指定地址,未进行连接的 SocketChannle 执行 I/O 操作时,会抛出
    NotYetConnectedException

  • SocketChannel 支持两种 I/O 模式:阻塞式和非阻塞式

  • SocketChannel支持异步关闭,到SocketChannel在一个线程执行读操作阻塞,另一个线程调用shutdownInput,所有的读阻塞线程程将返回-1 表示没有读取任何数据;同理, 如果 SocketChannel 在一个线程上 write 阻塞,另一个线程对该SocketChannel 调用shutdownWrite,则写阻塞的线程将抛出AsynchronousCloseException

  • SocketChannel 支持设定参数

    • SO_SNDBUF 套接字发送缓冲区大小
    • SO_RCVBUF 套接字接收缓冲区大小
    • SO_KEEPALIVE 保活连接
    • O_REUSEADDR 复用地址
    • SO_LINGER 有数据传输时延缓关闭 Channel (只有在非阻塞模式下有用)
    • TCP_NODELAY 禁用 Nagle 算法
  • 设置和获取参数
    image-20211125084819829

示例代码展示:

  • SocketChannel 的使用
/**
 * @Description:SocketChannel 的使用
 * @Author: duanyf
 * @DateTime: 2021/11/25 0025 上午 8:25
 */
public class SocketChannelDemo {

  public static void main(String[] args) throws IOException {
    //创建SocketChannel方法一
    SocketChannel socketChannel = SocketChannel.open
        (new InetSocketAddress("www.baiud.com", 80));
    //创建SocketChannel方法二
//    SocketChannel socketChannel1 = SocketChannel.open();
//    socketChannel.connect(new InetSocketAddress("www.baiud.com", 80));
    //此处为true,为阻塞状态读操作不能进行;此处为false,为非阻塞状态读操作能进行
    socketChannel.configureBlocking(false);
    //读操作
    ByteBuffer byteBuffer=ByteBuffer.allocate(16);
    socketChannel.read(byteBuffer);
    socketChannel.close();
    System.out.println("结束");
  }

}
  • 结合SeverSocketChannel 做一个连接

    SeverSocketChannel代码是上面的不变

    SocketChannelDemo

    package javaNIO.SocketChannel;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SocketChannel;
    
    /**
     * @Description:SocketChannel与ServerSocketChannel连接
     * @Author: duanyf
     * @DateTime: 2021/11/25 0025 上午 8:25
     */
    public class SocketChannelDemo {
    
      public static void main(String[] args) throws IOException {
        //创建SocketChannel方法一
        SocketChannel socketChannel = SocketChannel.open
            (new InetSocketAddress("localhost", 8888));
        System.out.println("连接成功");
        //创建SocketChannel方法二
    //    SocketChannel socketChannel1 = SocketChannel.open();
    //    socketChannel.connect(new InetSocketAddress("www.baiud.com", 80));
        //此处为true,为阻塞状态读操作不能进行;此处为false,为非阻塞状态读操作能进行
        socketChannel.configureBlocking(true);
        //读操作
    
        ByteBuffer byteBuffer=ByteBuffer.allocate(16);
        socketChannel.read(byteBuffer);
        byteBuffer.flip();
        while (byteBuffer.hasRemaining()){
          System.out.print((char)byteBuffer.get());
        }
        socketChannel.close();
        System.out.println("结束");
      }
    
    }
    
    

DatagramChannel

概述:

  • 正如 SocketChannel 对应 Socket,ServerSocketChannel 对应 ServerSocket,每一个 DatagramChannel 对象也有一个关联的 DatagramSocket 对象。

  • SocketChannel,ServerSocketChannel模拟连接导向的流协议(如 TCP/IP),DatagramChannel 则模拟包导向的无连接协议(如 UDP/IP)

  • DatagramChannel是无连接的,但是可以通过send(),receive()操作发送接受数据包。

  • 连接:

    • UDP 不存在真正意义上的连接,这里的连接是向特定服务地址用 read 和 write 接收
      发送数据包。

    • read()和 write()只有在 connect()后才能使用,不然会抛NotYetConnectedException 异常。用 read()接收时,如果没有接收到包,会抛PortUnreachableException 异常。

      image-20211125101430583

代码展示:

  • 发送接受数据(未使用read write)
package javaNIO.DataGramChannel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.apache.mina.statemachine.annotation.State;
import org.junit.Test;

/**
 * @Description:DatagramChannel的使用,客户端发送,服务端接收,
 * 使用的是send() receive()
 * 使用了单元测试类地址 junit.org
 * @Author: duanyf
 * @DateTime: 2021/11/25 0025 上午 9:10
 */
public class DatagramChannelDemo {

  //发送的实现
  @Test
  public void sendDatagram() throws IOException, InterruptedException {
    //打开DatagramChannel
    DatagramChannel sendchannel = DatagramChannel.open();
    InetSocketAddress sendAddress =
        new InetSocketAddress("localhost",9999);

    //发送
    while (true) {
      ByteBuffer sendBuffer = ByteBuffer.wrap("Hell world 很大".getBytes(StandardCharsets.UTF_8));
      sendchannel.send(sendBuffer, sendAddress);
      System.out.println("发送完成");
      Thread.sleep(1000);
    }

  }

  //接受的实现
  @Test
  public void receiveDatagram() throws IOException {
    //打开DataGramChannel
    DatagramChannel receiveChannel=DatagramChannel.open();
    //打开 9999 端口接收 UDP 数据包
    InetSocketAddress receiveAddress=
        new InetSocketAddress(9999);

    //发送会填写地址端口不需要绑定,接受要绑定
    receiveChannel.bind(receiveAddress);

    //buffer
    ByteBuffer receiveBuffer=ByteBuffer.allocate(1024);

    //接受
    while (true){
      receiveBuffer.clear();
      //通过 receive()接收 UDP 包,用 toString 查看发包的 ip、端口等信息
      SocketAddress sendAddress = receiveChannel.receive(receiveBuffer);
      //读写转换
      receiveBuffer.flip();
      //得到发送方内容
      System.out.println(sendAddress.toString());
      System.out.println(Charset.forName("UTF-8").decode(receiveBuffer));

    }
  }
}

  • 连接(伪)使用read write

    只接收和发送 9999 的数据包

    package javaNIO.SocketChannel;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.DatagramChannel;
    import java.nio.charset.Charset;
    import java.nio.charset.StandardCharsets;
    import org.junit.Test;
    
    /**
     * @Description:使用read,write实现伪连接。
     * @Author: duanyf
     * @DateTime: 2021/11/25 0025 上午 10:36
     */
    public class DatagramChannelDemo2 {
    
      //连接 read和write
      @Test
      public void testconnect() throws IOException {
        //打开Datagramchannel
        DatagramChannel connectchannel=DatagramChannel.open();
        //接受方绑定,发送方在连接时填写
        connectchannel.bind(new InetSocketAddress(9999));
    
        //连接
        connectchannel.connect(new InetSocketAddress("localhost",9999));
        //发送方
        //write
        System.out.println("发送方");
        connectchannel.write(ByteBuffer.wrap("hello world".getBytes(StandardCharsets.UTF_8)));
    
        //接受方
        //buffer
        System.out.println("接收方");
        ByteBuffer readBuffer =ByteBuffer.allocate(1024);
        while (true){
          readBuffer.clear();
          //将发送过来的数据读入buffer
          connectchannel.read(readBuffer);
          //读写转换,才能输出buffer
          readBuffer.flip();
    
          System.out.println(Charset.forName("UTF-8").decode(readBuffer));
        }
      }
    }
    

Scatter/Gather

- scatter/gather 用于描述从 Channel 中读取或者写入到 Channel 的操作.

- scatter / gather 经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头
和消息体组成的消息,你可能会将消息体和消息头分散到不同的 buffer 中,这样你可
以方便的处理消息头和消息体.

  • Scatter(分散)

    • 定义:将从channel中读取的数据读入(其实就是写入)到多个buffer中去。
      image-20211125160022080

    • 代码展示:

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

      **解析:**先创建两个缓冲区,将两个缓冲区依次放入数组,将从Channel中读取的数据,按放入的顺序依次写入缓冲区header, body。

    • Scattering Reads 在移动下一个 buffer 前,必须填满当前的 buffer,这也意味着它
      不适用于动态消息(译者注:消息大小不固定)。换句话说,如果存在消息头和消息体,
      消息头必须完成填充(例如 128byte),Scattering Reads 才能正常工作。

  • Gather(聚集)

    • 定义:将多个buffer中的数据写入到一个channel中去
      image-20211125160050042

    • 代码展示:

      ByteBuffer header = ByteBuffer.allocate(128);
      ByteBuffer body = ByteBuffer.allocate(1024);
      //write data into buffers
      ByteBuffer[] bufferArray = { header, body };
      channel.write(bufferArray);
      

      **解析:**先创建两个缓冲区,将两个缓冲区依次放入数组,将缓冲区header, body中的数据按放入顺序依次写入Channel。

    • 如果一个 buffer 的容量为 128byte,但是仅仅包含 58byte 的数据,这58byte的数据将被写入channel然后到下一个buffer,不一定非要传满128byte,比Scattering Reads更具有动态性。

3.Java NIO(Buffer)

Buffer 简介

  • Java NIO 中的 Buffer 用于和 NIO 通道进行交互。数据是从通道读入缓冲区,从缓冲

    区写入到通道中的。

    image-20211125164312482

  • 面向流IO系统中,所有数据都写入或者将数据读取到Stream中,与之类似的面向缓冲区的NIO系统,在读取数据时,是先从channel读到缓冲区再从缓冲区读出;写入数据也是先写入缓冲区,channel从缓冲区中取出

  • NIO中所有缓冲区类型都继承于抽象类Buffer对于 Java 中的基本类型,基本都有一个具体 Buffer 类型与之相对应。

    image-20211125164456943

Buffer的基本用法

使用 Buffer 读写数据,一般遵循以下四个步骤:

  • 写入数据到buffer中,并记录写入多少数据
  • 一旦需要读取数据,通过flip()从写模式切换成读模式。
  • 在读模式下,读取之前写入的所有数据。
  • 一旦读完所有数据,使用clear()或 compact()方法清空缓冲区,使其可以再次被写入。

**注意:**clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

代码实例

  • buffer使用示例

    package javaNIO.buffer;
    
    import java.io.FileNotFoundException;
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    import org.junit.Test;
    
    /**
     * @Description:buffer使用示例
     * @Author: duanyf
     * @DateTime: 2021/11/25 0025 下午 16:52
     */
    public class BufferDemo1 {
    
      @Test
      public void buffer01() throws IOException {
        //创建FileChannel
        RandomAccessFile randomAccessFile =
            new RandomAccessFile("D:\\idea项目\\network\\src\\javaNIO\\sources\\1.txt", "rw");
        FileChannel channel = randomAccessFile.getChannel();
    
        //创建一个buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
    
        //将channel中的数据读入(写入)到buffer
        //表示读到了哪,如果readnum为-1表示读到了文件末尾,不为-1则表示有内容
        int readnum = channel.read(buffer);
    
        while (readnum != -1) {
          //read模式
          buffer.flip();
          //如果buffer有数据
          while (buffer.hasRemaining()) {
            System.out.print((char) buffer.get());
          }
          //清空方便下次读写
          buffer.clear();
          //读取下一个channel中的数据
          readnum = channel.read(buffer);
        }
        //关闭CHannel
        channel.close();
      }
    }
    

**注意:**bytebuffer不能读写汉字

  • intbuffer使用示例

    package javaNIO.buffer;
    
    import java.io.FileNotFoundException;
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.nio.ByteBuffer;
    import java.nio.IntBuffer;
    import java.nio.channels.FileChannel;
    import org.junit.Test;
    
    /**
     * @Description:intbuffer使用示例
     * @Author: duanyf
     * @DateTime: 2021/11/25 0025 下午 16:52
     */
    public class BufferDemo1 {
      @Test
      public void buffer2(){
        //创建buffer
        IntBuffer buffer=IntBuffer.allocate(8);
        //向buffer中循环放内容(写入)
        for (int i = 0; i < buffer.capacity(); i++) {
          buffer.put(i);
        }
    
        //读写转换方便之后读取
        buffer.flip();
        //读取buffer中的值
        while (buffer.hasRemaining()){
          int value=buffer.get();
          System.out.print(value+" ");
        }
      }
    
    }
    
    

Buffer 的 capacity、position 和 limit

为了理解 Buffer 的工作原理,需要熟悉它的三个属性:

- Capacity

- Position

- limit

position 和 limit 的含义取决于 Buffer 处在读模式还是写模式。不管 Buffer 处在什么模式,capacity 的含义总是一样的。

image-20211125195019476

  • capacity

    作为一个内存块,Buffer 有一个固定的大小值,也叫“capacity”.你只能往里写
    capacity 个 byte、long,char 等类型。一旦 Buffer 满了,需要将其清空(通过读数
    据或者清除数据)才能继续写数据往里写数据。

  • position

    • 写数据到 Buffer 中时,position 表示写入数据的当前位置,position 的初始值为
      0。当一个 byte、long 等数据写到 Buffer 后, position 会向下移动到下一个可插入
      数据的 Buffer 单元。position 最大可为 capacity – 1(因为 position 的初始值为0)
    • 读数据到 Buffer 中时,position 表示读入数据的当前位置,如 position=2 时表
      示已开始读入了 3 个 byte,或从第 3 个 byte 开始读取。通过 *ByteBuffer.flip()*切换
      到读模式时 position 会被重置为 0,当 Buffer 从 position 读入数据后,position 会
      下移到下一个可读入的数据 Buffer 单元。
  • limit

    • 写数据时,limit 表示可对 Buffer 最多写入多少个数据。写模式下,limit 等于
      Buffer 的 capacity。
    • 读数据时,limit 表示 Buffer 里有多少可读数据(not null 的数据),这个值等于写模式的position的值

Buffer 的类型

Java NIO 有以下 Buffer 类型:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

这些 Buffer 类型代表了不同的数据类型。换句话说,就是可以通过 char,short,int,long,float 或 double 类型来操作缓冲区中的字节

Buffer 分配和读写数据

  • Buffer 分配

    要想获得一个 Buffer 对象首先要进行分配。 每一个 Buffer 类都有一个 allocate
    法。

    • 分配 48 字节 capacity 的 ByteBuffer

      ByteBuffer buf = ByteBuffer.allocate(48);
      
    • 分配一个可存储 1024 个字符的 CharBuffer:

      CharBuffer buf = CharBuffer.allocate(1024);
      
  • 向 Buffer 中写数据

    写数据到 Buffer 有两种方式:
    (1)从 Channel 写到 Buffer
    (2)通过 Buffer 的 put()方法写到 Buffer 里。

    • 从 Channel 写到 Buffer

      int bytesRead = inChannel.read(buf); //read into buffer.
      

      用read方法将channel中的数据读入(写入)buf

    • 通过 Buffer 的 put()方法写到 Buffer 里

      buf.put(127);	
      

      put 方法有很多版本,允许你以不同的方式把数据写入到 Buffer 中。例如, 写到一个
      指定的位置,或者把一个字节数组写入到 Buffer。

  • flip()方法

    flip 方法将 Buffer 从写模式切换到读模式。调用 flip()方法会将 position 设回 0,并将 limit 设置成之前 position 的值。换句话说,position 现在用于标记读的位置,limit 表示之前写进了多少个 byte、char 等 (现在能读取多少个 byte、char 等)。

  • 从 Buffer 中读取数据

    从 Buffer 中读取数据有两种方式:
    (1)从 Buffer 读取数据到 Channel
    (2)使用 get()方法从 Buffer 中读取数据。

    • 从 Buffer 读取数据到 Channel

      //read from buffer into channel.
      int bytesWritten = inChannel.write(buf);
      

      通过write将buffer中的数据写入channel

    • 使用 get()方法从 Buffer 中读取数据的例子

      byte aByte = buf.get();
      

      get 方法有很多版本,允许你以不同的方式从 Buffer 中读取数据。例如,从指定position 读取,或者从 Buffer 中读取数据到字节数组。

  • Buffer 几个方法

    • rewind()方法

      Buffer.rewind()将 position 设回 0,所以你可以重读 Buffer 中的所有数据。limit 保
      持不变,仍然表示能从 Buffer 中读取多少个元素(byte、char 等)。

    • clear()与 compact()方法

      一旦读完 Buffer 中的数据,需要让 Buffer 准备好再次被写入。可以通过 clear()或
      compact()方法来完成。

      • clear()

        position 将被设回 0,limit 被设置成 capacity 的值,数据都还在,但是不知道那些读过那些没有读过。

      • compact()方法

        compact()方法将所有未读的数据拷贝到 Buffer 起始处。然后将 position 设到最后一
        个未读元素正后面。limit 属性依然像 clear()方法一样,设置成 capacity。现在
        Buffer 准备好写数据了,但是不会覆盖未读的数据。

    • mark()与 reset()方法

      通过调用 Buffer.mark()方法,可以标记 Buffer 中的一个特定 position。之后可以通
      过调用 Buffer.reset()方法恢复到这个 position

      buffer.mark();
      //call buffer.get() a couple of times, e.g. during parsing.
      buffer.reset(); //set position back to mark.
      

缓冲区操作

  • 缓冲区分片

    • 定义:
      在 NIO 中,除了可以分配或者包装一个缓冲区对象外,还可以根据现有的缓冲区对象
      来创建一个子缓冲区,即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有的
      缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当
      于是现有缓冲区的一个视图窗口。调用 *slice()*方法可以创建一个子缓冲区。

    • 代码讲解(注释重要):

      package javaNIO.buffer;
      
      import java.nio.ByteBuffer;
      import org.junit.Test;
      
      /**
       * @Description:
       * @Author: duanyf
       * @DateTime: 2021/11/25 0025 下午 21:37
       */
      public class BufferDemo2 {
        @Test
        public void buffer01(){
          ByteBuffer byteBuffer=ByteBuffer.allocate(10);
          for (int i = 0; i < byteBuffer.capacity(); i++) {
            //bytebuffer处于写模式
            byteBuffer.put((byte) i);
          }
      
          //创建子缓冲区
          //此时为写数据,postion代表写入的位置为3,limit表示最多到7个等同capacity
          byteBuffer.position(3);
          byteBuffer.limit(7);
          //此时ByteBuffer只能读取3-7的数据get(3-7)
          //将这个缓冲区设置为子缓冲区
          ByteBuffer subByteBuffer=byteBuffer.slice();
      
          //改变子缓冲区内容
          for (int i = 0; i < subByteBuffer.capacity(); i++) {
            //读子缓冲区的数据
            byte b = subByteBuffer.get(i);
            //将新的数据写入子缓冲区
            subByteBuffer.put(i, (byte) (b*10));
          }
      
          //把bytebuffer还是处于写模式,下两行代码表示从0开始写,最多可写10
          byteBuffer.position(0);
          System.out.println("byteBuffer的长度是"+byteBuffer.capacity());
          byteBuffer.limit(byteBuffer.capacity());
      
          while (byteBuffer.remaining()>0){
            //读取byteBuffer的数据
            System.out.println(byteBuffer.get());
          }
        }
      }
      
  • 只读缓冲区

    • 只读缓冲区非常简单,可以读取它们,但是不能向它们写入数据。可以通过调用缓冲
      区的 asReadOnlyBuffer()方法,将任何常规缓冲区转 换为只读缓冲区,这个方法返
      回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。
      如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化。

    • 代码详解:

      mport java.nio.ByteBuffer;
      
      /**
       * @Description:只读缓冲区
       * @Author: duanyf
       * @DateTime: 2021/11/25 0025 下午 21:57
       */
      public class BufferDemo3 {
      
        public static void main(String[] args) {
          //创建并写入数据
          ByteBuffer byteBuffer=ByteBuffer.allocate(10);
          for (int i = 0; i < byteBuffer.capacity(); i++) {
            byteBuffer.put((byte) i);
          }
      
          //创建只读缓冲区
          ByteBuffer readByteBuffer = byteBuffer.asReadOnlyBuffer();
          //改变原缓冲区内容
          for (int i = 0; i < byteBuffer.capacity(); i++) {
            byte b = byteBuffer.get(i);
            byteBuffer.put(i,(byte) (i*10));
          }
      
          //从0开始读可读10个
          readByteBuffer.position(0);
          readByteBuffer.limit(byteBuffer.capacity());
      
          //只读缓冲区内容会随原缓冲区变化
          while (readByteBuffer.remaining()>0){
            System.out.println(readByteBuffer.get());
          }
        }
      }
      
  • 直接缓冲区

    • 直接缓冲区是为加快 I/O 速度,使用一种特殊方式为其分配内存的缓冲区,JDK 文档
      中的描述为:给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机
      I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),
      尝试避免将缓冲区的内容拷贝到一个中间缓冲区中 或者从一个中间缓冲区中拷贝数
      据。

    • 普通的allocate()方法肯需要中间缓冲区,因此要分配直接缓冲区,需要调用allocateDirect()方法,而不是 allocate()方法,使用方式与普通缓冲区并无区别。

    • 代码详解:

      package javaNIO.buffer;
      
      import java.io.FileInputStream;
      import java.io.FileNotFoundException;
      import java.io.FileOutputStream;
      import java.io.IOException;
      import java.nio.ByteBuffer;
      import java.nio.channels.FileChannel;
      
      /**
       * @Description:直接缓冲区
       * @Author: duanyf
       * @DateTime: 2021/11/25 0025 下午 22:11
       */
      public class BufferDemo4 {
      
        public static void main(String[] args) throws IOException {
          String inpath="D:\\idea项目\\network\\src\\javaNIO\\sources\\dirbuffer01";
          String outpath="D:\\idea项目\\network\\src\\javaNIO\\sources\\dirbuffer02";
          FileInputStream inputStream=new FileInputStream(inpath);
          FileOutputStream outputStream=new FileOutputStream(outpath);
          FileChannel inChannel=inputStream.getChannel();
          FileChannel outChannel=outputStream.getChannel();
      
          //创建直接缓冲区
          ByteBuffer dirBuffer=ByteBuffer.allocate(1024);
          while (true){
            dirBuffer.clear();
            //将数据从inchannel中写入buffer,并且返回写入位置,-1时结束
            int read = inChannel.read(dirBuffer);
            if (read == -1) {
              break;
            }
            dirBuffer.flip();
            //将buffer的数据读入outchannel,数据会从dirbuffer01传入dirbuffer02文件中
            outChannel.write(dirBuffer);
          }
        }
      }
      
  • 内存映射文件 I/O

    • 内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通
      道的 I/O 快的多,内存映射文件I/O不是将文件读入内存,只有文件中实际读取或者写入的部分才会映射到内存中。

    • 代码详解:

      package javaNIO.buffer;
      import java.io.RandomAccessFile;
      import java.nio.MappedByteBuffer;
      import java.nio.channels.FileChannel;
      /**
       * @Description:内存映射文件IO
       * @Author: duanyf
       * @DateTime: 2021/11/25 0025 下午 22:33
       */
      public class BufferDEmo5 {
      
        static private final int start = 0;
        static private final int size = 1024;
        static public void main(String args[]) throws Exception {
          //创建channel
          RandomAccessFile raf = new RandomAccessFile("d:\\atguigu\\01.txt",
              "rw");
          FileChannel fc = raf.getChannel();
          //内存映射缓冲
          MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE,
              start, size);
          mbb.put(0, (byte) 97);
          mbb.put(1023, (byte) 122);
          raf.close();
        }
      }
      

4.Java NIO(Selector)

Selector 简介

  • Selector 和 Channel 关系:

    • Selector一般被称为选择器(多路复用器),用于检查一个或多个 NIO Channel(通道)的状态是否处于可读、可写

    • 可以实现单线程管理多个 channels,也就是可以管理多个网络链接。

      image-20211126212341656

  • 可选择通道(SelectableChannel)

    • 只有继承了SelectableChanne的Channel才能被Selector复用。
    • SelectableChannel 类提供了实现通道的可选择性所需要的公共方法
    • SelectableChannel是所有支持就绪检查的通道类的父类。
    • 所有 socket 通道,都继承了 SelectableChannel 类都是可选择的,包括从管道(Pipe)对象的中获得的通道。而 FileChannel 类,没有继承 SelectableChannel,因此是不是可选通道
    • 通道和选择器之间的关系,使用注册的方式完成,一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次,在注册时需要指定通道的操作(读,写…)。
      image-20211126212913054
  • Channel 注册到 Selector

    • 使用 Channel.register(Selector sel,int ops)方法,将一个通道注册到一个
      选择器时。第一个参数,指定通道要注册的选择器。第二个参数指定选择器需要查询
      的通道操作(Selector感兴趣的操作)。

    • 可以供选择器查询的通道操作,从类型来分,包括以下四种

      - 可读 : SelectionKey.OP_READ

      - 可写 : SelectionKey.OP_WRITE

      - 连接 : SelectionKey.OP_CONNECT

      - 接收 : SelectionKey.OP_ACCEPT

    • 如果 Selector 对通道的多操作类型感兴趣,可以用“位或”操作符来实现:

      int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE
      
    • 选择器查询的不是通道的操作,而是通道的某个操作的一种就绪状态,

    • 一旦通道具备完成某个操作的条件,表示该通道的某个操作已经就绪,就可以被 Selector 查询到,程序可以对通道进行对应的操作。比方说,某个SocketChannel 通道可以连接到一个服务器,则处于“连接就绪”(OP_CONNECT),可以进行还没进行只是准备好了

  • 选择键(SelectionKey)

    • Channel 注册到后,并且一旦通道处于某种就绪的状态,就可以被选择器Selector 的 select()方法查询到
    • select 方法的作用:寻找感兴趣的通道操作,进行就绪状态的查询
    • Selector 可以不断的查询 Channel 中发生的操作的就绪状态,并且挑选感兴趣的操作就绪状态,一旦通道有操作的就绪状态达成,并且是 Selector 感兴趣的操作, 就会被 Selector 选中,放入选择键集合中。
    • 一个选择键包含了注册在 Selector 的通道操作的类型(SelectionKey.OP_READ),也包含了特定的通道与特定的选择器之间的注册关系
    • 选择键的概念,和事件的概念比较相似。一个选择键类似监听器模式里边的一个 事件。由于 Selector 不是事件触发的模式,而是主动去查询的模式,所以不叫事件 Event,而是叫 SelectionKey 选择键。

Selector 的使用方法

  • Selector 的创建
    通过调用 Selector.open()方法创建一个 Selector 对象

    // 1、获取 Selector 选择器
    Selector selector = Selector.open();
    
  • 注册 Channel 到 Selector:
    要实现 Selector 管理 Channel,需要将 channel 注册到相应的 Selector 上.

    • 与 Selector 一起使用时,Channel 必须处于非阻塞模式下,FileChannel 不能与 Selector 一起使用,因 为 FileChannel 不能切换到非阻塞模式,而套接字相关的所有的通道都可以。
    • 一个通道,并没有一定要支持所有的四种操作。比如服务器通道 ServerSocketChannel 支持 Accept 接受操作,而 SocketChannel 客户端通道则不支 持。可以通过通道上的 *validOps()*方法,来获取特定通道下所有支持的操作集合
    // 1、获取 Selector 选择器
    Selector selector = Selector.open();
    // 2、获取通道
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    // 3.设置为非阻塞
    serverSocketChannel.configureBlocking(false);
    // 4、绑定连接
    serverSocketChannel.bind(new InetSocketAddress(9999));
    // 5、将通道注册到选择器上,并制定监听事件为:“接收”事件
    serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
    
  • 轮询查询就绪操作

    • 通过 Selector 的 select()方法,可以查询出是否有已经就绪的通道操作返回通道数量,select()方法返回1代表有一个通道就绪但是不知道是哪个,
    • 当select()>0时,我们可以调用Selector 中有一个 selectedKeys()方法,会返回一个已选择键集合,对其进行遍历。
    //查询已就绪通道的操作,放入集合
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        //遍历集合
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        while (iterator.hasNext()){
          System.out.println("开始遍历");
          //得到每个存储就绪状态的key
          SelectionKey key = iterator.next();
          //判断key就绪状态
          if (key.isAcceptable()) {
            System.out.println("接受操作");
    
          } else if (key.isConnectable()) {
    
          } else if (key.isReadable()) {
    
          }else if (key.isWritable()){
          }
          iterator.remove();
        }
    
  • 停止选择的方法
    选择器执行选择的过程,系统底层会依次询问每个通道是否已经就绪.此时需要调用select()方法。但是select一次只能返回一个值,因此在多个通道同时询问时可能会造成阻塞。

    • wakeup()方法 :通过调用 Selector 对象的 wakeup()方法让处在阻塞状态的 select()方法立刻返回,该方法使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中的选择操作,那么下一次对 select()方法的一次调用将立即返回。
    • close()方法 :通过 close()方法关闭 Selector,该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似 wakeup()),同时使得注册到该 Selector 的所有 Channel 被注销,所有的键将被取消,但是 Channel本身并不会关闭。

示例代码

package javaNIO.selector;

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.Date;
import java.util.Iterator;
import java.util.Set;
import org.junit.Test;

/**
 * @Description:
 * @Author: duanyf
 * @DateTime: 2021/11/26 0026 22:44
 */
public class SelectorDemo3 {
  //客户端代码
  @Test
  public void clientDemo() throws IOException {
    //获取端口号
    SocketChannel socketChannel = SocketChannel.open(
        new InetSocketAddress("localhost", 9999));
    //切换成非阻塞模式
    socketChannel.configureBlocking(false);
    //创建buffer
    ByteBuffer clientBuffer=ByteBuffer.allocate(1024);
    //写入buffer数据
    clientBuffer.put((new Date().toString()+" hello world").getBytes());
    //读写转换
    clientBuffer.flip();
    //buffer中的数据写入channel
    socketChannel.write(clientBuffer);
    //清空buffer
    clientBuffer.clear();
  }

  @Test
  public void serverDemo() throws IOException {
    //获取服务端通道
    ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
    //切换成非阻塞模式
    serverSocketChannel.configureBlocking(false);
    //绑定端口号
    serverSocketChannel.socket().bind(new InetSocketAddress(9999));
    //获取Selector选择器
    Selector selector = Selector.open();
    //通道注册到选择器,进行监听
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    //选择器进行轮询,进行后续操作
    //selector.select()为1就是就绪状态,但是不知道是哪个通道就绪
    while (selector.select()>0){
      //已就绪通道的操作集合
      Set<SelectionKey> selectionKeys = selector.selectedKeys();
      //遍历(需要借用Iterator才能遍历)
      Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
      while (selectionKeyIterator.hasNext()){
        //获取就绪操作
        SelectionKey next = selectionKeyIterator.next();
        //判断是什么操作
        if(next.isAcceptable()){
          //服务端是注册的是接受操作
          //获取连接
          SocketChannel accept = serverSocketChannel.accept();
          //切换到非阻塞
          accept.configureBlocking(false);
          //注册为读操作,接下来的接受会转到读操作
          accept.register(selector,SelectionKey.OP_READ);


        }else if (next.isConnectable()) {

        } else if (next.isReadable()) {
          //此时next是读操作,由他获取channel
          SocketChannel channel = (SocketChannel) next.channel();
          //创建缓冲区用于读数据
          ByteBuffer byteBuffer=ByteBuffer.allocate(1024);

          //读取数据
          //将数据从通道写入缓冲区,且返回读取位置
          int length=1;
//          int length=channel.read(byteBuffer);
          while ((length=channel.read(byteBuffer))>0){
            byteBuffer.flip();
            System.out.println(new String(byteBuffer.array(),0,length) );
            byteBuffer.clear();
          }
        }else if (next.isWritable()){
        }
      }
      selectionKeyIterator.remove();
    }
  }
}

5.Java NIO(Pipe 和 FileLock)

Pipe

  • 定义:Java NIO 管道是 2 个线程之间的单向数据连接。Pipe 有一个 source 通道和一个 sink 通道。数据会被写到 sink 通道,从 source 通道读取。

    image-20211127204406902

  • 管道读写过程

    • 创建管道
      通过 Pipe.open()方法打开管道。

      Pipe pipe = Pipe.open();
      
    • 写入管道
      要向管道写数据,需要访问 sink 通道。

      Pipe.SinkChannel sinkChannel = pipe.sink();
      

      通过调用 SinkChannel 的 write()方法,将数据写入 SinkChannel:

      String newData = "New String to write to file..." + System.currentTimeMillis();
      ByteBuffer buf = ByteBuffer.allocate(48);
      buf.clear();
      buf.put(newData.getBytes());
      buf.flip();
      while(buf.hasRemaining()) {
       sinkChannel.write(buf);
      }
      
    • 从管道读取数据
      从读取管道的数据,需要访问 source 通道

      Pipe.SourceChannel sourceChannel = pipe.source();
      

      调用 source 通道的 read()方法来读取数据

      ByteBuffer buf = ByteBuffer.allocate(48);
      //ByteBuffer buf = ByteBuffer.allocate(48);
      int bytesRead = sourceChannel.read(buf);
      
  • 代码展示

    package javaNIO.pipe;
    
    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.nio.channels.Pipe;
    
    /**
     * @Description:管道的读写
     * @Author: duanyf
     * @DateTime: 2021/11/27 0027 20:26
     */
    public class PipeDemo {
    
      public static void main(String[] args) throws IOException {
        //写
        //获取管道
        Pipe pipe=Pipe.open();
        //获取sink通道
        Pipe.SinkChannel sinkChannel = pipe.sink();
        //创建缓冲区,
        ByteBuffer sinkBuffer=ByteBuffer.allocate(1024);
        sinkBuffer.put("hellow world".getBytes());
        sinkBuffer.flip();
        //向sink中写入数据
        sinkChannel.write(sinkBuffer);
    
        //读
        //获取source通道
        Pipe.SourceChannel sourceChannel=pipe.source();
        //创建缓冲区,从source中读取数据
        ByteBuffer sourceBuffer=ByteBuffer.allocate(1024);
        int length=sourceChannel.read(sourceBuffer);
        sourceBuffer.flip();
        System.out.println(new String(sourceBuffer.array(),0,length));
        //关闭通道
        sinkChannel.close();
        sourceChannel.close();
    
      }
    }
    

FileLock

介绍与方法
  • FileLock 简介

    • 文件锁在 OS 中很常见,如果多个程序同时访问、修改同一个文件,很容易因为文件数据不同步而出现问题。给文件加一个锁,同一时间,只能有一个程序修改此文件, 或者程序都只能读此文件,这就解决了同步问题。
    • 文件锁是进程级别的,不是线程级别的。文件锁可以解决多个进程并发访问、修改同 一个文件的问题,但不能解决多线程并发访问、修改同一文件的问题。使用文件锁时,同一进程内的多个线程,可以同时访问、修改此文件。
    • 文件锁是当前程序所属的 JVM 实例持有的,一旦获取到文件锁(对文件加锁),要调用 release(),或者关闭对应的 FileChannel 对象,或者当前 JVM 退出,才会释放这 个锁。
    • 一旦某个进程(比如说 JVM 实例)对某个文件加锁,则在释放这个锁之前,此进程不能再对此文件加锁,就是说 JVM 实例在同一文件上的文件锁是不重叠的(进程级别不 能重复在同一文件上获取锁)。
  • 文件锁分类

    • 排它锁:又叫独占锁。对文件加排它锁后,该进程可以对此文件进行读写,该进程独占此文件,其他进程不能读写此文件,直到该进程释放文件锁。
    • 共享锁:某个进程对文件加共享锁,其他进程也可以访问此文件,但这些进程都只能读此文件,不能写。线程是安全的。只要还有一个进程持有共享锁,此文件就只能 读,不能写。
  • 获取文件锁方法

    • lock():对整个文件加锁,默认为排它锁。

    • lock(long position, long size, booean shared):自定义加锁方式。前 2 个参数 指定要加锁的部分(可以只对此文件的部分内容加锁),第三个参数值指定是否是共 享锁。

    • tryLock() :对整个文件加锁,默认为排它锁。

    • tryLock(long position, long size, booean shared) 自定义加锁方式。

      如果指定为共享锁,则其它进程可读此文件,所有进程均不能写此文件,如果某进程试图对此文件进行写操作,会抛出异常。

  • lock 与 tryLock 的区别

    • lock 是阻塞式的,如果未获取到文件锁,会一直阻塞当前线程,直到获取
    • 文件锁 tryLock 和 lock 的作用相同,只不过 tryLock 是非阻塞式的,tryLock 是尝试获取文 件锁,获取成功就返回锁对象,否则返回 null,不会阻塞当前线程。
  • FileLock 两个方法

    • boolean isShared() 此文件锁是否是共享锁
    • boolean isValid() 此文件锁是否还有效
      在某些 OS 上,对某个文件加锁后,不能对此文件使用通道映射。
代码实例
  • 测试lock功能代码

    package javaNIO.FileLock;
    
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    import java.nio.channels.FileLock;
    import org.junit.Test;
    
    /**
     * @Description:
     * @Author: duanyf
     * @DateTime: 2021/11/27 0027 21:01
     */
    public class FileLockDemo {
      @Test
      public void haslock01() throws IOException, InterruptedException {
        RandomAccessFile randomAccessFile=new RandomAccessFile("D:\\idea"
            + "项目\\network\\src\\javaNIO\\sources\\FileLock01","rw");
        FileChannel fileChannel=randomAccessFile.getChannel();
    
        //对文件加锁,此处换为共享锁会直接无法锁住,因为此单元测试类中是写操作
        FileLock fileLock = fileChannel.lock();
    //    FileLock fileLock = fileChannel.lock(0,Long.MAX_VALUE, true);
    
        while (true) {
          //写入
          ByteBuffer writeBuffer=ByteBuffer.allocate(1024);
          writeBuffer.put("write".getBytes());
          writeBuffer.flip();
          int a=fileChannel.write(writeBuffer);
          System.out.println("写入"+new String(writeBuffer.array(),0,a));
          Thread.sleep(2000);
        }
      }
    
      @Test
      public void writelock02() throws IOException {
        RandomAccessFile randomAccessFile=new RandomAccessFile("D:\\idea"
            + "项目\\network\\src\\javaNIO\\sources\\FileLock01","rw");
        FileChannel fileChannel=randomAccessFile.getChannel();
    
        //对文件加锁
    //    FileLock fileLock = fileChannel.lock();
    
        //写入
        ByteBuffer writeBuffer=ByteBuffer.allocate(1024);
        writeBuffer.put("write".getBytes());
        writeBuffer.flip();
        fileChannel.write(writeBuffer);
      }
    
      @Test
      public void readlock03() throws IOException {
        RandomAccessFile randomAccessFile=new RandomAccessFile("D:\\idea"
            + "项目\\network\\src\\javaNIO\\sources\\FileLock01","rw");
        FileChannel fileChannel=randomAccessFile.getChannel();
    
        //对文件加锁
    //    FileLock fileLock = fileChannel.lock();
    
        //写入
        ByteBuffer readBuffer=ByteBuffer.allocate(1024);
        int length=fileChannel.read(readBuffer);
        System.out.println(new String(readBuffer.array(),0,length));
    
      }
    
    }
    

    经测试haslock01()使用时,其余两个读写操作都无法进行
    image-20211127212259071

  • 共享锁与独占锁

    package javaNIO.FileLock;
    
    import java.io.BufferedReader;
    import java.io.FileReader;
    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    import java.nio.channels.FileLock;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.nio.file.StandardOpenOption;
    
    /**
     * @Description:
     * @Author: duanyf
     * @DateTime: 2021/11/27 0027 21:33
     */
    public class FileLockDemo2 {
    
      public static void main(String[] args) throws IOException {
        String input="hello world";
        System.out.println("input "+input);
        ByteBuffer inputBuffer=ByteBuffer.wrap(input.getBytes());
        String filepath="D:\\idea项目\\network\\src\\javaNIO\\sources\\FileLock02";
        Path path= Paths.get(filepath);
        //设置通道可以写和追加
        FileChannel fileChannel=FileChannel.open(path, StandardOpenOption.WRITE,
            StandardOpenOption.APPEND);
        //为了方便追加设置
        System.out.println("文件长度:"+fileChannel.size());
        fileChannel.position(fileChannel.size()-1);
        //加锁
        //独占锁 获得锁 可读可写FileLock lock = fileChannel.lock();
        //共享锁,都只能读不能写
        FileLock lock = fileChannel.lock(0,Long.MAX_VALUE, true);
        //判断是否时共享锁
        System.out.println("是否时共享锁?"+lock.isShared());
    
        //写
        fileChannel.write(inputBuffer);
        fileChannel.close();
    
    
        //读文件
        readfile(filepath);
      }
    
      private static void readfile(String filepath) throws IOException {
        FileReader fileReader=new FileReader(filepath);
        //包装
        BufferedReader bufferedReader=new BufferedReader(fileReader);
        String s=bufferedReader.readLine();
        System.out.println("读取内容");
        while (s!=null){
          System.out.println(" "+s);
          s=bufferedReader.readLine();
        }
        fileReader.close();
        bufferedReader.close();
      }
    
    }
    
    

6.Java NIO(其他)

path

  • Path 简介

    • Java Path 接口是 Java NIO 更新的一部分,同 Java NIO 一起已经包括在 Java6 和 Java7 中
    • Java Path 接口是在 Java7 中添加到 Java NIO 的。Path 接口位于 java.nio.file 包中,所以 Path 接口的完全限定名称为 java.nio.file.Path。
    • Java Path 实例表示文件系统中的路径。一个路径可以指向一个文件或一个目录。路径可以是绝对路径,也可以是相对路径。绝对路径包含从文件系统的根目录到它指向的文件或目录的完整路径。相对路径包含相对于其他路径的文件或目录的路径
    • java.nio.file.Path 接口类似于 java.io.File 类,但是有一些差别。不过, 在许多情况下,可以使用 Path 接口来替换 File 类的使用。
  • 创建 Path 实例

    使用 java.nio.file.Path 实例必须创建一个 Path 实例。可以使用 Paths 类 (java.nio.file.Paths)中的静态方法 Paths.get()来创建路径实例

    public class PathDemo {
    
      public static void main(String[] args) {
        Path path= Paths.get("D:\\idea项目\\network\\src\\javaNIO\\sources\\1.txt");
        System.out.println("创建成功");
      }
    }
    
    • 创建绝对路径

      Path path= Paths.get("D:\\idea项目\\network\\src\\javaNIO\\sources\\1.txt");
      
    • 创建相对路径

          Path path= Paths.get("D:\\idea项目\\network\\src\\javaNIO","sources");
          Path path1=Paths.get("D:\\idea项目\\network\\src\\javaNIO","path\\1.txt")
      
    • 路径标准化(Path.normalize())
      Path 接口的 normalize()方法可以使路径标准化。标准化意味着它将移除所有在路径 字符串的中间的.和…代码,并解析路径字符串所引用的路径。
      代码:

      public class PathDemo {
      
        public static void main(String[] args) {
          Path path= Paths.get("D:\\idea项目\\network\\src\\javaNIO\\sources\\..\\01");
          System.out.println("标准化前:"+path);
          Path path1=path.normalize();
          System.out.println("标准化后:"+path1);
      
        }
      }
      

      结果:

      image-20211128230948001

Files

  • Java NIO Files 类(java.nio.file.Files)提供了几种操作文件系统中的文件的方法
  • java.nio.file.Files 类与 java.nio.file.Path 实例一起工作

方法

  • Files.createDirectory()

    • iles.createDirectory()方法,用于根据 Path 实例创建一个新目录
    • 第一行创建表示要创建的目录的 Path 实例

    代码:

    public class PathDemo2 {
    
      public static void main(String[] args) {
        //要创建文件的目录
        Path path= Paths.get("D:\\idea项目\\network\\src\\javaNIO\\sources\\copypath");
        try {
          Path copypath= Files.createDirectory(path);
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
    
  • Files.copy()

    • Files.copy()方法从一个路径拷贝一个文件到另外一个目录

          Path path= Paths.get("D:\\idea项目\\network\\src\\javaNIO\\sources\\path"
              + "\\01");
          Path path1=Paths.get("D:\\idea项目\\network\\src\\javaNIO\\sources"
              + "\\copypath\\001.txt");
          try {
            Files.copy(path,path1);
          } catch (IOException e) {
            e.printStackTrace();
          }
      
    • 覆盖已存在的文件

      Files.copy(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING);

  • Files.move()

    Files.move()用于将文件从一个路径移动到另一个路径,且可以重命名。

        Path sourcePath = Paths.get("D:\\idea项目\\network\\src\\javaNIO\\sources"
            + "\\path\\01");
        Path destinationPath = Paths.get("D:\\idea项目\\network\\src\\javaNIO"
            + "\\sources\\copypath\\01test");
        try {
          Files.move(sourcePath, destinationPath,
              StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
          //移动文件失败
          e.printStackTrace();
        }
    
  • Files.delete()

    Files.delete()方法可以删除一个文件或者目录。

    Path path = Paths.get("d:\\hello\\001.txt");
    try {
     Files.delete(path);
    } catch (IOException e) {
     // 删除文件失败
     e.printStackTrace();
    }
    
  • Files.walkFileTree()

    • Files.walkFileTree()方法包含递归遍历目录树功能,将 Path 实例和 FileVisitor 作为参数。Path 实例指向要遍历的目录,FileVisitor 在遍历期间被调用
    • FileVisitor 是一个接口,必须自己实现 FileVisitor 接口,并将实现的实例传递给walkFileTree()方法。在目录遍历过程中,您的 FileVisitor 实现的每个方法都将被调用。如果不需要实现所有这些方法,那么可以扩展 SimpleFileVisitor 类,它包含 FileVisitor 接口中所有方法的默认实现
    • FileVisitor 接口的方法中,每个都返回一个 FileVisitResult 枚举实例。
      • CONTINUE 继续
      • TERMINATE 终止
      • SKIP_SIBLING 跳过同级
      • SKIP_SUBTREE 跳过子级

    代码:

        Path rootPath = Paths.get("D:\\idea项目\\network\\src\\javaNIO\\sources"
            + "\\copypath");
        //File.separator就是分隔符"\"
        String fileToFind = File.separator + "001.txt";
    //    System.out.println("separator: "+File.separator);
        try {
          //递推查找rootPath路径下有没有001.txt
          Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws
                IOException {
              //完整路径fileString
              String fileString = file.toAbsolutePath().toString();
              //System.out.println("pathString = " + fileString);
              //若完整路径fileString包括001.txt则找到001.txt
              if(fileString.endsWith(fileToFind)){
                //输出完整路径
                System.out.println("file found at path: " + file.toAbsolutePath());
                //终止
                return FileVisitResult.TERMINATE;
              }
              //继续,继续进行递推操作
              return FileVisitResult.CONTINUE;
            }
          });
        } catch(IOException e){
          e.printStackTrace();
        }
    

AsynchronousFileChannel

在 Java 7 中,Java NIO 中添加了 AsynchronousFileChannel,也就是是异步地将数 据写入文件。

  • 创建 AsynchronousFileChannel

    ath path = Paths.get("d:\\atguigu\\01.txt");
    try {
     AsynchronousFileChannel fileChannel =
     AsynchronousFileChannel.open(path, StandardOpenOption.READ);
    } catch (IOException e) {
     e.printStackTrace();
    }
    
    • 第一个参数:指向与 AsynchronousFileChannel 相关联文件的 Path 实 例
    • 第二个参数:AsynchronousFileChannel 在文件上执 行什么操作,可有多个。
  • 通过 Future 读取数据

    用返回 Future 的 read()方法

    /**
     * @Description:通过 Future 读取数据
     * @Author: duanyf
     * @DateTime: 2021/11/29 0029 8:14
     */
    public class AsynchronousFileChannelDemo {
    
      public static void main(String[] args) throws IOException {
        //创建AsynchronousFileChannel
        Path path = Paths.get("D:\\idea项目\\network\\src\\javaNIO\\sources\\2.txt");
        AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path,
            StandardOpenOption.READ);
    
        //创建buffer
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
    
        //调用filechannel中的read方法得到future,异步读取
        Future<Integer> read = fileChannel.read(byteBuffer, 0);
    
        //判断当前方法是否完成,完成返回true
        System.out.println("方法是否完成?"+read.isDone());//此时还没有完成
        while (!read.isDone());
    //    System.out.println("判断结束");
        //从bytebuffer里面读取数据
        System.out.println("方法是否完成?"+read.isDone());
        byteBuffer.flip();
    
        while (byteBuffer.hasRemaining()){
          System.out.print((char)byteBuffer.get());
        }
        System.out.println();
        System.out.println("方法是否完成?"+read.isDone());
        byteBuffer.clear();
    
      }
    
    }
    

    以上代码的步骤

    image-20211129093325648

  • 通过 CompletionHandler 读取数据

    是调用 read()方法,该方法将一个 CompletionHandler 作为参数
    代码:

    public class AsynchronousFileChannelDemo2 {
    
      public static void main(String[] args) throws IOException {
        Path path= Paths.get("D:\\idea项目\\network\\src\\javaNIO\\sources\\2.txt");
        AsynchronousFileChannel channel=AsynchronousFileChannel.open(path,
            StandardOpenOption.READ);
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
        channel.read(byteBuffer, 0, byteBuffer,
            new CompletionHandler<Integer, ByteBuffer>() {
              @Override
              public void completed(Integer result, ByteBuffer attachment) {
                //读取成功
                //输出文件长度
                System.out.println("result="+result);
                //这里attachment是buffer
                attachment.flip();
                System.out.println("读写转换完毕");
                byte[] data=new byte[attachment.limit()];
                //此方法将字节从从此缓冲区传递到目标byte数组
                System.out.println("读入byte数组");
                attachment.get(data);
                System.out.println(new String(data));
                attachment.clear();
              }
    
              @Override
              public void failed(Throwable exc, ByteBuffer attachment) {
                //读取失败
              }
            });
      }
    }
    

    以上代码步骤:

    image-20211129095551221

  • 通过 Future 写数据

    • 代码:

      public class AsynchronousFileChannelDemo3 {
      
        public static void main(String[] args) throws IOException {
          Path path = Paths.get("D:\\idea项目\\network\\src\\javaNIO\\sources\\1.txt");
          AsynchronousFileChannel channel = AsynchronousFileChannel.open(path,
              StandardOpenOption.WRITE);
          ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
          byteBuffer.put("hello world".getBytes());
          byteBuffer.flip();
          Future<Integer> write = channel.write(byteBuffer, 0);
          while (!write.isDone());
          System.out.println("写入完成");
        }
      }
      
    • 以上代码步骤:

      image-20211129100651831

  • 通过 CompletionHandler 写数据

    • 代码

      public class AsynchronousFileChannelDemo4 {
      
          public static void main(String[] args) throws IOException {
              Path path= Paths.get("D:\\idea项目\\network\\src\\javaNIO\\sources\\1.txt");
              AsynchronousFileChannel channel=AsynchronousFileChannel.open(path,
                                                                           StandardOpenOption.WRITE);
              ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
      
              byteBuffer.put("helllo world".getBytes());
              byteBuffer.flip();
              channel.write(byteBuffer, 0, byteBuffer,
                            new CompletionHandler<Integer, ByteBuffer>() {
                                @Override
                                public void completed(Integer result, ByteBuffer attachment) {
                                    System.out.println("result="+result);
                                    System.out.println("写入完成");
                                }
      
                                @Override
                                public void failed(Throwable exc, ByteBuffer attachment) {
      
                                }
                            });
          }
      }
      

charset

java 中使用 Charset 来表示字符集编码对象

  • Charset 常用静态方法

    • 通过编码类型获得Charset 对象

      public static Charset forName(String charsetName)
      
    • 获得系统支持的所有编码方式

      public static SortedMap<String,Charset> availableCharsets
      
    • 获得虚拟机默认的编码方式

      public static Charset defaultCharset()
      
    • 判断是否支持该编码类型

      public static boolean isSupported(String charsetName)
      
  • Charset 常用普通方法

    • 获得 Charset 对象的编码类型(String)

      public final String name()
      
    • 获得编码器对象

      public abstract CharsetEncoder newEncoder()
      
    • 获得解码器对象

      public abstract CharsetDecoder newDecoder()
      
  • 代码实例

    package javaNIO.charset;
    
    import java.nio.ByteBuffer;
    import java.nio.CharBuffer;
    import java.nio.charset.CharacterCodingException;
    import java.nio.charset.Charset;
    import java.nio.charset.CharsetDecoder;
    import java.nio.charset.CharsetEncoder;
    import java.util.Map;
    import java.util.Set;
    
    /**
     * @Description:
     * @Author: duanyf
     * @DateTime: 2021/11/29 0029 10:23
     */
    public class chatsetDemo {
    
      public static void main(String[] args) throws CharacterCodingException {
        //获取charset对象
        Charset charset=Charset.forName("UTF-8");
    
        //通过charset对象获取编码器对象
        CharsetEncoder charsetEncoder=charset.newEncoder();
    
        //创建缓冲区
        CharBuffer charBuffer=CharBuffer.allocate(1024);
        charBuffer.put("world很大");
        charBuffer.flip();
    
        //对charbuffer进行编码
        ByteBuffer byteBuffer = charsetEncoder.encode(charBuffer);
        //遍历查看每个内容编码后是什么
        System.out.println("编码结果:");
        for (int i = 0; i < byteBuffer.limit(); i++) {
          System.out.println(byteBuffer.get());
        }
    
        //tongguo通过charset对象获取解码器对象
        byteBuffer.flip();
        CharsetDecoder charsetDecoder=charset.newDecoder();
    
        //通过bytebuffer进行解码
        CharBuffer charBuffer1 = charsetDecoder.decode(byteBuffer);
        System.out.println("解码之后的结果:");
        System.out.println(charBuffer1.toString());
    
        //使用GBK进行解码 方式一
        Charset charset1=charset.forName("GBK");
        byteBuffer.flip();
        CharsetDecoder charsetDecoder1=charset1.newDecoder();
        CharBuffer charBuffer2=charsetDecoder1.decode(byteBuffer);
        System.out.println("解码之后的结果:");
        System.out.println(charBuffer2.toString());
    
        //使用GBK进行解码 方式二
        Charset charset2=charset.forName("GBK");
        byteBuffer.flip();
        CharBuffer charBuffer3=charset2.decode(byteBuffer);
        System.out.println("解码之后的结果:");
        System.out.println(charBuffer3.toString());
    
        //获取 Charset 所支持的字符编码
        Map<String,Charset> map=Charset.availableCharsets();
        //遍历输出所有支持度额编码方式
        Set<Map.Entry<String,Charset>> set=map.entrySet();
        for (Map.Entry<String,Charset> entry:set) {
          System.out.println(entry.getKey()+"="+entry.getValue().toString());
        }
      }
    }
    
    

    image-20211129135053433

7.java NIO多人聊天室

ChatServer

package niopro.server;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

/**
 * @Description:聊天室服务器
 * @Author: duanyf
 * @DateTime: 2021/11/29 0029 22:12
 */
public class ChatServer {


  //服务器端启动方法
  public void startserver() throws IOException {
    //创建Selector选择器
    Selector selector = Selector.open();
    //创建ServerSocketChannel通道
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    //为通道绑定端口
    serverSocketChannel.socket().bind(new InetSocketAddress(9999));
    //设置非阻塞模式
    serverSocketChannel.configureBlocking(false);
    //把channel注册到选择器上
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    System.out.println("服务器启动成功");
    //循环,等待有新的链接接入
    for (; ; ) {
      //获取通道数量
      int channelNum = selector.select();
      if (channelNum == 0) {
        System.out.println("没有获取到通道");
        continue;
      }
      //获取到通道,加到集合中
      Set<SelectionKey> selectionKeys = selector.selectedKeys();
      //遍历集合
      Iterator<SelectionKey> iterator = selectionKeys.iterator();
      while (iterator.hasNext()) {
        //取出集合中的通道
        SelectionKey next = iterator.next();
        //取出后需要移除,移除集合当前SelectionKey也就是next
        iterator.remove();
        //根据就绪状态,调用对应方法实现具体业务操作
        //如果是accept ServerSocketChannel
        if (next.isAcceptable()) {
          acceptMethod(serverSocketChannel, selector);
        }
        //如果是可读状态 SocketChannel
        else if (next.isReadable()) {
          //此时获得的next应该是第二个也就是acceptMethod中注册的那个
          // 因为第一个用于接受的已经被remove
          readMethod(selector, next);

        }
      }

    }


  }

  //处理可读状态
  private void readMethod(Selector selector, SelectionKey next)
      throws IOException {
    //从selectionKeys获取已经就绪的通道
    //因为是可读的不是做接受所以要使用SocketChannel
    SocketChannel socketChannel = (SocketChannel) next.channel();
    //创建buffer
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    //循环读取客户端消息
    //将客户端SocketChannel中的数据读入bytebuffer
    int readlength = socketChannel.read(byteBuffer);
    String msg = "";
    if (readlength > 0) {
      //切换成读模式
      byteBuffer.flip();
      //读取内容解码
      msg += Charset.forName("UTF-8").decode(byteBuffer);

    }

    //将channel再次注册到选择器,监听可读状态
    //猜测是为了可被其他客户端读取
    socketChannel.register(selector, SelectionKey.OP_READ);
    //把客户端发送消息,广播到其他客户端
    if (msg.length() > 0) {
      System.out.println(msg);
      castOtherClient(msg, selector, socketChannel);
    }
  }

  // 广播到其他客户端
  private void castOtherClient(String message, Selector selector,
      SocketChannel socketChannel) throws IOException {
    //获取所有已经接入客户端channel
    Set<SelectionKey> selectionKeySet = selector.keys();

    //循环向所有channel中广播消息
    for (SelectionKey selectionKey : selectionKeySet) {
      //获取每个channel
      Channel tarChannel = selectionKey.channel();
      //不需要给自己发送
      if (tarChannel instanceof SocketChannel && tarChannel != socketChannel) {
//        System.out.println("广播");
        ((SocketChannel) tarChannel).write(Charset.forName("utf-8").encode(
            message));
      }

    }
  }


  //处理接入状态操作
  private void acceptMethod(ServerSocketChannel serverSocketChannel,
      Selector selector)
      throws IOException {
    //创建SocketChannel
    SocketChannel socketChannel = serverSocketChannel.accept();
    //SocketChannel设置成非阻塞
    socketChannel.configureBlocking(false);
    //把channel注册到Selector选择器上,监听可读
    //服务端处理完accept之后,在本次循环最后会删除该事件,那么如果发送了数据需要处理,
    // 就需要服务端在接受client后手动为该client添加一个读事件,等下次循环时,
    // 才能在该client可读时被判断出, 不然在accept之后就没有关联这个client的事件了
    socketChannel.register(selector, SelectionKey.OP_READ);
    //回复消息,将消息写入客户端通道,发送给客户端
    socketChannel.write(Charset.forName("UTF-8").encode("欢迎进入聊天室1"));
  }

  public static void main(String[] args) throws IOException {
    new ChatServer().startserver();
  }

}

ChatClient

package niopro.client;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Scanner;

/**
 * @Description:聊天室服务器端
 * @Author: duanyf
 * @DateTime: 2021/11/29 0029 22:11
 */
public class ChatClient {

  //启动客户端方法
  public void startClient(String name) throws IOException {
    //连接服务端
    SocketChannel socketChannel = SocketChannel.open(
        new InetSocketAddress("localhost", 9999));

    //接受服务端响应数据
    Selector selector=Selector.open();
    socketChannel.configureBlocking(false);
    socketChannel.register(selector, SelectionKey.OP_READ);
    //创建线程
    ClientThread clientThread=new ClientThread(selector);
    new Thread(clientThread).start();

    //向服务端发送消息
    Scanner scanner=new Scanner(System.in);
    while (scanner.hasNextLine()){
      String msg = scanner.nextLine();
      if(msg.length()>0){
        socketChannel.write(Charset.forName("UTF-8").encode(name+":"+msg));
      }
    }



  }

  public static void main(String[] args) throws IOException {
    new ChatClient().startClient("sf");
  }

}

ClientThread

package niopro.client;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

/**
 * @Description:
 * @Author: duanyf
 * @DateTime: 2021/11/29 0029 23:38
 */
public class ClientThread implements Runnable {

  private Selector selector;

  public ClientThread(Selector selector) {
    this.selector = selector;
  }

  @Override
  public void run() {
    try {
      for (; ; ) {
        //获取通道数量
        int channelNum = selector.select();
        if (channelNum == 0) {
          System.out.println("没有获取到通道");
          continue;
        }
        //获取到通道,加到集合中
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        //遍历集合
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        while (iterator.hasNext()) {
          //取出集合中的通道
          SelectionKey next = iterator.next();
          //取出后需要移除,移除集合当前SelectionKey也就是next
          iterator.remove();

//          //根据就绪状态,调用对应方法实现具体业务操作
//          //如果是accept ServerSocketChannel
//          if (next.isAcceptable()) {
//            acceptMethod(serverSocketChannel, selector);
//          }
          //如果是可读状态 SocketChannel
          if (next.isReadable()) {
            //此时获得的next应该是第二个也就是acceptMethod中注册的那个
            // 因为第一个用于接受的已经被remove
            readMethod(selector, next);

          }
        }

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

  //处理可读状态
  private void readMethod(Selector selector, SelectionKey next)
      throws IOException {
    //从selectionKeys获取已经就绪的通道
    //因为是可读的不是做接受所以要使用SocketChannel
    SocketChannel socketChannel = (SocketChannel) next.channel();
    //创建buffer
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    //循环读取客户端消息
    //将客户端SocketChannel中的数据读入bytebuffer
    int readlength = socketChannel.read(byteBuffer);
    String msg = "";
    if (readlength > 0) {
      //切换成读模式
      byteBuffer.flip();
      //读取内容解码
      msg += Charset.forName("UTF-8").decode(byteBuffer);

    }

    //将channel再次注册到选择器,监听可读状态
    //猜测是为了可被其他客户端读取
    socketChannel.register(selector, SelectionKey.OP_READ);
    //把客户端发送消息,广播到其他客户端
    if (msg.length() > 0) {
      System.out.println(msg);
//      castOtherClient(msg, selector, socketChannel);
    }
  }
}

Aclient

public class Aclient {

  public static void main(String[] args) throws IOException {
    new ChatClient().startClient("郭小小");
  }

}

Bclient

public class Bclient {
  public static void main(String[] args) throws IOException {
    new ChatClient().startClient("郭大大");
  }

}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值