Java NIO(下)

67 阅读25分钟

四、 Java NIO(Selector)

1. Selector简介

1)Selector和Channel的关系

Selector一般称为选择器,也可以翻译为多路复用器。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写,如此可以实现单线程管理多个Channels,也就是可以`管理多个网络链接

image-20231225142755388

使用Selector的好处在于:

使用更少的线程来就可以来处理通道了,相比使用多个线程,避免了线程上下文切换带来的开销

2)可选择通道(SelectableChannel)

  • 不是所有的Channel都可以被Selector复用的:比方说,FileChannel就不能被选择器复用判断一个Channel能被Selector复用,有一个前提:判断他是否继承了一个抽象类SelectableChannel。如果继承了SelectableChannel,则可以被复用,否则不能
  • SelectableChannel类提供了实现通道的可选择性所需要的公共方法:它是所有支持就绪检查的通道类的父类。所有Socket通道,都继承了SelectableChannel类都是可选择的,包括从管道(Pipe)对象的中获得的通道。而FileChannel类,没有继承SelectableChannel,因此是不是可选通道
  • 一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。通道和选择器之间的关系,使用注册的方式完成。SelectableChannel可以被注册到Selector对象上,在注册的时候,需要指定通道的哪些操作,是Selector感兴趣的

image-20231225143649150

3)Channel注册到Selector

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

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

    • 可读: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)。再比方说,一个ServerSocketChannel服务器通道准备好接收新进入的连接,则处于"接收就绪(OP_ACCEPT)状态。还比方说,一个有数据可读的通道,可以说是"读就绪"(OP_READ)。一个等待写数据的通道可以说是"写就绪"(OP_WRITE)

4)选择键(SelectionKey)

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

2. Selector的使用方法

1)Selector的创建

通过调用Selector.open()方法创建一个Selector对象,如下:

//获取 Selector 选择器
Selector selector = Selector.open();

2)注册 Channel到Selector

要实现Selector管理Channel,需要将Channel注册到相应的Selector上

// 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);

上面通过调用通道的 register()方法会将它注册到一个选择器上

首先需要注意的是:

  • 与Selector一起使用时,Channel必须处于非阻塞模式下,否则将抛出异常IllegalBlockingModeException。这意味着,FileChannel不能与Selector一起使用,因为FileChannel不能切换到非阻塞模式,而套接字相关的所有的通道都可以
  • 一个通道,并没有一定要支持所有的四种操作,比如:SelectionKey.OP_ACCEPT。比如服务器通道ServerSocketChannel支持Accept接受操作,而SocketChannel客户端通道则不支持。可以通过通道上的validOps()方法,来获取特定通道下所有支持的操作集合

3)轮询查询就绪操作

  • 通过Selector的select()方法,可以查询出已经就绪的通道操作,这些就绪的状态集合,包存在一个元素是SelectionKey对象的Set集合中
  • 下面是Selector几个重载的查询select()方法:
    • select():阻塞到至少有一个通道在你注册的事件上就绪了
    • select(long timeout):和select()一样,但最长阻塞事件为timeout毫秒
    • selectNow():非阻塞,只要有通道就绪就立刻返回

select()方法返回的int值,表示有多少通道已经就绪,更准确的说,是自前一次select方法以来到这一次select方法之间的时间段上,有多少通道变成就绪状态

4)例子

首次调用select()方法,如果有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回 1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了

一旦调用select()方法,并且返回值不为0时,在Selector中有一个selectedKeys()方法,用来访问已选择键集合,迭代集合的每一个选择键元素,根据就绪操作的类型,完成对应的操作:

//获取已经就绪的通道操作
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    //判断key就绪状态是什么
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    //清除掉事件
    keyIterator.remove();
}

5)停止选择的方法

选择器执行选择的过程,系统底层会依次询问每个通道是否已经就绪,这个过程可能会造成调用线程进入阻塞状态,那么我们有以下三种方式可以唤醒在 select()方法中阻塞的线程

  • wakeup()方法:

通过调用Selector对象的 wakeup()方法让处在阻塞状态的select()方法立刻返回(唤醒操作)

该方法使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中的选择操作,那么下一次对select()方法的一次调用将立即返回

  • close()方法:

通过close()方法关闭Selector,该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),

同时使得注册到该Selector的所有Channel被注销,所有的键将被取消,但是Channel本身并不会关闭

3. 示例代码

1)服务器代码

@Test
public void ServerDemo() {
    try {
        //获取服务端通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //绑定服务端ip端口号
        ssc.socket().bind(new InetSocketAddress("127.0.0.1",8000));
        //切换非阻塞模式
        ssc.configureBlocking(false);
        
        //获取selector选择器
        Selector selector = Selector.open();
        // 将服务端channel注册到选择器上,并且指定感兴趣的事件是 Accept
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        
        //创建读/写缓冲区
        ByteBuffer readBuff = ByteBuffer.allocate (1024);
        ByteBuffer writeBuff = ByteBuffer.allocate (128);
        
        //写入数据
        writeBuff.put("received".getBytes());
        //切换读写模式
        writeBuff.flip();
        
        while (true) {
            int nReady = selector.select();
            //获取就绪状态集合
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> it = keys.iterator();
            while (it.hasNext()) {
                //获取到就绪状态
                SelectionKey key = it.next();
                it.remove();
                
                //判断是什么状态,对对应操作进行对应处理
                if (key.isAcceptable()) {
                    // 创建新的连接,并且把连接注册到 selector 上,而且,声明这个 channel 只对读操作感兴趣。
                    SocketChannel socketChannel = ssc.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }
                else if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    readBuff.clear();
                    socketChannel.read(readBuff);
                    readBuff.flip();
                    System.out.println("received : " + new String(readBuff.array()));
                    key.interestOps(SelectionKey.OP_WRITE);
                }
                else if (key.isWritable()) {
                    writeBuff.rewind();
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    socketChannel.write(writeBuff);
                    key.interestOps(SelectionKey. OP_READ );
                }
                
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

2)客户端代码

@Test
public void ClientDemo() {
    try {
        //获取通道
        SocketChannel socketChannel = SocketChannel.open();
        //绑定主机的ip端口号
        socketChannel.connect(new InetSocketAddress("127.0.0.1",8000));
        //设置非阻塞模式
        socketChannel.configureBlocking(false);
        //创建buffer
        ByteBuffer writeBuffer = ByteBuffer.allocate(32);
        ByteBuffer readBuffer = ByteBuffer.allocate(32);
        //给buffer写入数据
        writeBuffer.put("hello".getBytes());
        //模式切换
        writeBuffer.flip();
        while (true) {
            writeBuffer.rewind();
            //写入通道数据
            socketChannel.write(writeBuffer);
            //关闭
            readBuffer.clear();
            socketChannel.read(readBuffer);
        }
    } catch (IOException e) {

    }
}

3)NIO 编程步骤总结

  1. 创建Selector选择器
  2. 创建ServerSocketChannel通道,并绑定监听端口
  3. 设置Channel通道是非阻塞模式
  4. 把Channel注册到Selector选择器上,监听连接事件
  5. 调用Selector的select方法(循环调用),监测通道的就绪状况
  6. 调用selectKeys方法获取就绪Channel集合
  7. 遍历就绪Channel集合,判断就绪事件类型,实现具体的业务操作
  8. 根据业务,决定是否需要再次注册监听事件,重复执行第三步操作

五、Pipe和FileLock

1. Pipe

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

image-20231225153551213

  • 创建管道

通过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);
int bytesRead = sourceChannel.read(buf);

read()方法返回的int值会告诉我们多少字节被读进了缓冲区

示例:

@Test
public void testPipe() throws IOException {
    // 1、获取通道
    Pipe pipe = Pipe.open();
    // 2、获取 sink 管道,用来传送数据,写管道
    Pipe.SinkChannel sinkChannel = pipe.sink();
    // 3、申请一定大小的缓冲区
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    byteBuffer.put("aaa".getBytes());
    byteBuffer.flip();

    // 4、sink 发送数据,写入操作
    sinkChannel.write(byteBuffer);

    // 5、创建接收 pipe 数据的 source 管道,读管道
    Pipe.SourceChannel sourceChannel = pipe.source();

    // 6、接收数据,并保存到缓冲区中
    ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024);
    int length = sourceChannel.read(byteBuffer2);
    System.out.println(new String(byteBuffer2.array(), 0, length));

    // 7、关闭
    sourceChannel.close();
    sinkChannel.close();
}

2. FileLock文件锁

1)FileLock简介

文件锁在OS中很常见,如果多个程序同时访问、修改同一个文件,很容易因为文件数据不同步而出现问题。给文件加一个锁,同一时间,只能有一个程序修改此文件,或者程序都只能读此文件,这就解决了同步问题

文件锁是进程级别的,不是线程级别的。文件锁可以解决多个进程并发访问、修改同一个文件的问题,但不能解决多线程并发访问、修改同一文件的问题。使用文件锁时,同一进程内的多个线程,可以同时访问、修改此文件

文件锁是当前程序所属的JVM实例持有的,一旦获取到文件锁(对文件加锁),要调用release(),或者关闭对应的FileChannel 对象,或者当前JVM退出,才会释放这个锁

一旦某个进程(比如说JVM实例)对某个文件加锁,则在释放这个锁之前,此进程不能再对此文件加锁,就是说JVM实例在同一文件上的文件锁是不重叠的(进程级别不能重复在同一文件上获取锁)

2)文件锁分类

  • 排它锁:又叫独占锁。对文件加排它锁后,该进程可以对此文件进行读写,该进程独占此文件,其他进程不能读写此文件,直到该进程释放文件锁
  • 共享锁:某个进程对文件加共享锁,其他进程也可以访问此文件,但这些进程都只能读此文件,不能写。线程是安全的。只要还有一个进程持有共享锁,此文件就只能读,不能写
//创建 FileChannel 对象,文件锁只能通过 FileChannel 对象来使用
FileChannel fileChannel=new FileOutputStream("./1.txt").getChannel();
//对文件加锁
FileLock lock=fileChannel.lock();
//对此文件进行一些读写操作

//释放锁
lock.release();

文件锁要通过 FileChannel 对象使用

3)获取文件锁的方法

一共4种:

  • lock():
//对整个文件加锁,默认为排它锁。
//前2个参数指定要加锁的部分(可以只对此文件的部分内容加锁),第三个参数值指定是否是共享锁
lock(long position, long size, booean shared)
//自定义加锁方式
  • tryLock():
//对整个文件加锁,默认为排它锁
//自定义加锁方式
//如果指定为共享锁,则其它进程可读此文件,所有进程均不能写此文件,如果某进程试图对此文件进行写操作,会抛出异常
tryLock(long position, long size, booean shared)

lock与tryLock的区别

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

FileLock 两个方法:

boolean isShared() //此文件锁是否是共享锁
boolean isValid() //此文件锁是否还有效

在某些OS上,对某个文件加锁后,不能对此文件使用通道映射

例子:

public class Demo1 {
    public static void main(String[] args) throws IOException {
        String input = "aaa";
        System.out.println("输入 :" + input);
        
        ByteBuffer buf = ByteBuffer.wrap(input.getBytes());
        String filePath = "D:\\achang\\01.txt";
        Path pt = Paths.get(filePath);
        FileChannel channel = FileChannel.open(pt,
                                                 StandardOpenOption.WRITE,
                                                 StandardOpenOption.APPEND);
        
        channel.position(channel.size() - 1); // position of a cursor at the end offile
        
        //文件获取锁
        // 获得锁方法一:lock(),阻塞方法,当文件锁不可用时,当前进程会被挂起
        //lock = channel.lock();// 无参 lock()为独占锁
        // lock = channel.lock(0L, Long.MAX_VALUE, true);有参 lock()为共享锁,有写操作会报异常
        // 获得锁方法二:trylock(),非阻塞的方法,当文件锁不可用时,tryLock()会得到 null 值
        FileLock lock = channel.tryLock(0,Long. MAX_VALUE ,false);
        System.out.println("共享锁 shared: " + lock.isShared());
        channel.write(buf);
        
        channel.close(); // Releases the Lock
        System.out.println("写操作完成.");
        
        //读取数据
        readPrint (fp);
    }
}
//读取数据
public static void readPrint(String path) throws IOException {
    //通过字符流读取
    FileReader filereader = new FileReader(path);
    //包装缓存流
    BufferedReader bufferedreader = new BufferedReader(filereader);
    
    String tr = bufferedreader.readLine();
    System.out.println("读取内容: ");
    while (tr != null) {
        System.out.println(" " + tr);
        tr = bufferedreader.readLine();
    }
    
    //关闭
    filereader.close();
    bufferedreader.close();
}

六、Path&Files&AsynchronousFileChannel异步通道

1. Path

1)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类的使用

2)创建Path实例

使用java.nio.file.Path实例必须创建一个Path实例

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

import java.nio.file.Path;
import java.nio.file.Paths;

public class PathDemo {
    public static void main(String[] args) {
        Path path = Paths.get("d:\\aaa\\001.txt");
    }
}

上述代码,可以理解为,Paths.get()方法是Path实例的工厂方法

3)创建绝对路径

①创建绝对路径,通过调用Paths.get()方法,给定绝对路径文件作为参数来完成

Path path = Paths.get("d:\\aaa\\001.txt");

上述代码中,绝对路径是d:\aaa\001.txt。在Java字符串中, \是一个转义字符,需要编写\,告诉Java编译器在字符串中写入一个\字符

②如果在Linux、MacOS等操作字体上,上面的绝对路径可能如下:

Path path = Paths.get("/home/aaa/myfile.txt");

绝对路径现在为/home/aaa/myfile.txt.

③如果在Windows机器上使用了从/开始的路径,那么路径将被解释为相对于当前驱动器

4)创建相对路径

Java NIO Path类也可以用于处理相对路径。您可以使用Paths.get(basePath,relativePath)方法创建一个相对路径

示例代码:

//d:\achang\projects
Path projects = Paths.get("d:\\achang", "projects");
//d:\achang\projects\002.txt
Path file = Paths.get("d:\\achang", "projects\\002.txt");

代码1创建了一个Java Path的实例,指向路径(目录):d:\achang\projects 代码2创建了一个Path的实例,指向路径(文件):d:\achang\projects\002.txt

5)Path.normalize()

Path接口的normalize()方法可以使路径标准化。标准化意味着它将移除所有在路径字符串的中间的.和…代码,并解析路径字符串所引用的路径

Path.normalize()示例:

String originalPath ="d:\\achang\\projects\\..\\yygh-project";

Path path1 = Paths. get (originalPath);
System. out .println("path1 = " + path1);

Path path2 = path1.normalize();//标准化处理
System. out .println("path2 = " + path2);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0e90PvVW-1631446717703)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210912180839500.png)]

2. Files

Java NIO Files类(java.nio.file.Files)提供了几种操作文件系统中的文件的方法。以下内容介绍Java NIO Files最常用的一些方法java.nio.file.Files类与java.nio.file.Path实例一起工作,因此在学习Files类之前,需要先了解Path类

1)Files.createDirectory()

Files.createDirectory()方法,用于根据Path实例创建一个新目录

实例:

Path path = Paths.get("d:\\aaa");
try {
    //创建一个新目录
    Path newDir = Files.createDirectory(path);
} catch(FileAlreadyExistsException e){
    // 目录已经存在
} catch (IOException e) {
    // 其他发生的异常
    e.printStackTrace();
}

第一行创建表示要创建的目录的Path实例。在try-catch块中,用路径作为参数调用Files.createDirectory()方法。如果创建目录成功,将返回一个Path实例,该实例指向新创建的路径

如果该目录已经存在,则是抛出一个java.nio.file.FileAlreadyExistsException

如果出现其他错误,可能会抛出IOException,例如,如果想要的新目录的父目录不存在,则可能会抛出IOException

2)Files.copy()

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

示例:

Path sourcePath = Paths.get("d:\\achang\\01.txt");
Path destinationPath = Paths.get("d:\\achang\\002.txt");

try {
    //文件复制操作
    Files.copy(sourcePath, destinationPath);
} catch(FileAlreadyExistsException e) {
    // 目录已经存在
} catch (IOException e) {
    // 其他发生的异常
    e.printStackTrace();
}

首先,该示例创建两个Path实例。然后,这个例子调用Files.copy(),将两个Path实例作为参数传递。这可以让源路径引用的文件被复制到目标路径引用的文件中

如果目标文件已经存在,则抛出一个java.nio.file.FileAlreadyExistsException异常

如果有其他错误,则会抛出一个IOException;例如,如果将该文件复制到不存在的目录,则会抛出IOException

  • 覆盖已存在的文件

Files.copy()方法的第三个参数。如果目标文件已经存在,这个参数指示 copy()方法覆盖现有的文件

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

3)Files.move()

Files.move()用于将文件从一个路径移动到另一个路径

移动文件与重命名相同,但是移动文件既可以移动到不同的目录,也可以在相同的操作中更改它的名称

示例:

Path sourcePath = Paths.get("d:\\achang\\01.txt");
Path destinationPath = Paths.get("d:\\achang\\001.txt");

try {
    Files. move (sourcePath, destinationPath,StandardCopyOption. REPLACE_EXISTING );
} catch (IOException e) {
    //移动文件失败
    e.printStackTrace();
}

Files.move()的第三个参数。这个参数告诉Files.move()方法来覆盖目标路径上的任何现有文件

4. Files.delete()

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

示例:

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

创建指向要删除的文件的Path。然后调用Files.delete()方法。

如果Files.delete()不能删除文件(例如,文件或目录不存在),会抛出一个IOException

5. Files.walkFileTree()

  1. Files.walkFileTree()方法包含递归遍历目录树功能,将Path实例和FileVisitor作为参数。Path实例指向要遍历的目录,FileVisitor在遍历期间被调用
  2. FileVisitor是一个接口,必须自己实现FileVisitor接口,并将实现的实例传递给walkFileTree()方法。在目录遍历过程中,您的FileVisitor实现的每个方法都将被调用。如果不需要实现所有这些方法,那么可以扩展SimpleFileVisitor类,它包含FileVisitor接口中所有方法的默认实现
  3. FileVisitor接口的方法中,每个都返回一个FileVisitResult枚举实例
  4. 查找一个名为 001.txt 的文件示例:
  5. java.nio.file.Files类包含许多其他的函数,有关这些方法的更多信息,请查看java.nio.file.Files类的JavaDoc

FileVisitResult 枚举包含以下四个选项:

  • CONTINUE 继续
  • TERMINATE 终止
  • SKIP_SIBLING 跳过同级
  • SKIP_SUBTREE 跳过子级
Path rootPath = Paths.get("d:\\achang");
String fileToFind = File.separator + "001.txt";

try {
    Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
            String fileString = file.toAbsolutePath().toString();
            //System.out.println("pathString = " + fileString);
            //查这个文件是否存在
            if(fileString.endsWith(fileToFind)){
                System.out.println("file found at path: " + file.toAbsolutePath());
                //终止操作
                return FileVisitResult.TERMINATE ;
            }
            //继续操作
            return FileVisitResult.CONTINUE ;
        }
    });
} catch(IOException e){
    e.printStackTrace();
}

3. AsynchronousFileChannel异步通道

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

1)创建AsynchronousFileChannel

通过静态方法open()创建

示例:

Path path = Paths. get ("d:\\achang\\01.txt");

try {
    //获取异步通道,指定读操作StandardOpenOption.READ
    AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path,StandardOpenOption.READ);
} catch (IOException e) {
    e.printStackTrace();
}

open()方法的第一个参数指向与AsynchronousFileChannel相关联文件的Path实例

第二个参数是一个或多个打开选项,它告诉AsynchronousFileChannel在文件上执行什么操作

在本例中,我们使用了StandardOpenOption.READ选项,表示该文件将被打开阅读

2)通过Future读取数据

可以通过两种方式从AsynchronousFileChannel读取数据

第一种方式是调用返回Future的read()方法

示例:

Path path = Paths.get("d:\\achang\\001.txt");

AsynchronousFileChannel fileChannel = null;
try {
    //创建了一个 AsynchronousFileChannel异步通道,指定读操作StandardOpenOption.READ
    fileChannel = AsynchronousFileChannel.open(path,StandardOpenOption.READ);
} catch (IOException e) {
    e.printStackTrace();
}
//创建一个 ByteBuffer,它被传递给 read()方法作为参数,以及一个 0 的位置
ByteBuffer buffer = ByteBuffer.allocate(1024);

//在调用 read()之后,循环
long position = 0;
Future<Integer> operation = fileChannel.read(buffer, position);

//直到返回的 isDone()方法返回 true
while(!operation.isDone());

//读取操作完成后,数据读取到 ByteBuffer 中,然后打印到 System.out 中
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println(new String(data));//以字符串的形式输出
buffer.clear();

上述代码:

  1. 创建了一个AsynchronousFileChannel
  2. 创建一个ByteBuffer,它被传递给read()方法作为参数,以及一个0的位置
  3. 在调用read()之后,循环,直到返回的isDone()方法返回true
  4. 读取操作完成后,数据读取到ByteBuffer中,然后打印到System.out中

3)通过CompletionHandler读取数据

比上面的方式要简单些

第二种方法是调用read()方法,该方法将一个CompletionHandler作为参数

示例:

Path path = Paths.get("d:\\achang\\001.txt");
AsynchronousFileChannel fileChannel = null;
try {
    //获取异步通道,指定读操作StandardOpenOption.READ
    fileChannel = AsynchronousFileChannel.open(path,StandardOpenOption.READ);
} catch (IOException e) {
    e.printStackTrace();
}
ByteBuffer buffer = ByteBuffer.allocate(1024);

long position = 0;
fileChannel.read(buffer, position, buffer, new CompletionHandler<Integer,
                 ByteBuffer>() {
                     //读取完成之后会调用
                     @Override
                     public void completed(Integer result,ByteBuffer attachment) {
                         System.out.println("result = " + result);

                         attachment.flip();
                         byte[] data = new byte[attachment.limit()];
                         attachment.get(data);
                         System.out.println(new String(data));
                         attachment.clear();
                     }

                     //读取失败后调用
                     @Override
                     public void failed(Throwable exc, ByteBuffer attachment) {
                     }
                 });

步骤:

  1. 读取操作完成,将调用CompletionHandler的completed()方法
  2. 对于completed()方法的参数传递一个整数,它告诉我们读取了多少字节,以及传递给read()方法的""附件"。"附件"是read()方法的第三个参数。在本代码中,它是ByteBuffer,数据也被读取
  3. 如果读取操作失败,则将调用CompletionHandler的failed()方法

4)通过Future写数据

和读取一样,可以通过两种方式将数据写入一个AsynchronousFileChannel

示例:

Path path = Paths. get ("d:\\achang\\001.txt");
AsynchronousFileChannel fileChannel = null;
try {
    //获取异步通道,指定写操作StandardOpenOption.WRITE
    fileChannel = AsynchronousFileChannel.open(path,StandardOpenOption.WRITE);
} catch (IOException e) {
    e.printStackTrace();
}
//创建buffer,并写入数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
buffer.put("achang data".getBytes());
buffer.flip();//切换写模式

//写操作
Future<Integer> operation = fileChannel.write(buffer, position);
buffer.clear();

//判断是否写入完成
while(!operation.isDone());
System.out println("Write over");

首先,AsynchronousFileChannel以写模式打开。然后创建一个ByteBuffer,并将一些数据写入其中。然后,ByteBuffer中的数据被写入到文件中。最后,示例检查返回的Future,以查看写操作完成时的情况

注意,文件必须已经存在。如果该文件不存在,那么write()方法将抛出一个java.nio.file.NoSuchFileException

5)通过CompletionHandler写数据

示例:

Path path = Paths.get("d:\\achang\\001.txt");

//文件不存在就创建
if(!Files.exists(path)){
    try {
        Files.createFile(path);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

AsynchronousFileChannel fileChannel = null;
try {
    //获取异步通道,指定写操作StandardOpenOption.WRITE
    fileChannel = AsynchronousFileChannel.open(path,StandardOpenOption.WRITE);
} catch (IOException e) {
    e.printStackTrace();
}

//创建buffer,并写入数据
ByteBuffer buffer = ByteBuffer.allocate 1024);
long position = 0;
buffer.put("achang data".getBytes());
buffer.flip();//切换为写操作

//写操作
fileChannel.write(buffer, position, buffer, new CompletionHandler<Integer,
                  ByteBuffer>() {
                      //写入完成后会调用
                      @Override
                      public void completed(Integer result, ByteBuffer attachment) {
                          System.out.println("bytes written: " + result);
                      }

                      //写入失败后调用
                      @Override
                      public void failed(Throwable exc, ByteBuffer attachment) {
                          System.out.println("Write failed");
                          exc.printStackTrace();
                      }
                  });

当写操作完成时,将会调用CompletionHandler的completed()方法

如果写失败,则会调用failed()方法

6)字符集(Charset)

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

  • Charset常用静态方法
public static Charset forName(String charsetName)//通过编码类型获得 Charset 对象
public static SortedMap<String,Charset> availableCharsets()//获得系统支持的所有编码方式
public static Charset defaultCharset()//获得虚拟机默认的编码方式
public static boolean isSupported(String charsetName)//判断是否支持该编码类型
  • Charset常用普通方法
public final String name()//获得 Charset 对象的编码类型(String)
public abstract CharsetEncoder newEncoder()//获得编码器对象
public abstract CharsetDecoder newDecoder()//获得解码器对象

代码示例:

@Test
public void charSetEncoderAndDecoder() throws
    CharacterCodingException {
    //0.获取Charset对象
    Charset charset=Charset.forName("UTF-8");
    
    //1.获取编码器对象
    CharsetEncoder charsetEncoder=charset.newEncoder();
    
    //2.获取解码器对象
    CharsetDecoder charsetDecoder=charset.newDecoder();
    
    //3.创建buffer缓冲区对象,并写入数据
    CharBuffer charBuffer=CharBuffer.allocate(1024);
    charBuffer.put("achang数据");
    charBuffer.flip();//转换读写模式
    
    //4.通过编码器对象,进行编码
    ByteBuffer byteBuffer=charsetEncoder.encode(charBuffer);
    System.out.println("编码后:");
    for (int i=0;i<byteBuffer.limit();i++) {
        System.out.println(byteBuffer.get());
    }
    
    //5.解码
    byteBuffer.flip();//读写切换
    //通过解码器对象,进行解码
    CharBuffer charBuffer1=charsetDecoder.decode(byteBuffer);
    System.out.println("解码后:");
    System.out.println(charBuffer1.toString());
    
    System.out.println("指定其他格式解码:");
    Charset charset1=Charset.forName("GBK");
    byteBuffer.flip//读写切换
    CharBuffer charBuffer2 =charset1.decode(byteBuffer);
    System.out.println(charBuffer2.toString());
    
    //6.获取 Charset 所支持的字符编码
    Map<String ,Charset> map= Charset.availableCharsets();
    Set<Map.Entry<String,Charset>> set=map.entrySet();
    for (ap.Entry<String,Charset> entry: set) {
        System. out .println(entry.getKey()+"="+entry.getValue().toString());
    }
}

七、使用Java NIO实现简易在线多人聊天室

使用Java NIO实现简易在线多人聊天室

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A6EJoXvw-1631541201809)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210913214731942.png)]

1. 服务器端代码

package chatroom.server;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

public class ChatServer {

    //服务端启动的方法
    public void startServer() throws IOException {
        //1、创建Selector选择器
        Selector selector = Selector.open();

        //2、创建ServerSocketChannel通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //3、为channel通道绑定端口号
        serverSocketChannel.bind(new InetSocketAddress(9090));
        serverSocketChannel.configureBlocking(false);//设置非阻塞模式

        //4、把serverSocketChannel绑定到selector上
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务器启动.......");

        //5、循环监听是否有连接连入
        while (true) {
            int select = selector.select();

            //如果为0,则为没连接,没有获取到,就跳出循环
            if (select == 0) {
                continue;
            }

            //获取可用channel
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            //遍历
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();

                //移除 set 集合当前 selectionKey
                iterator.remove();

                //6、根据就绪状态,调用对应方法实现具体业务操作
                if (selectionKey.isAcceptable()) {
                    //6.1 如果 accept 状态
                    acceptOperator(serverSocketChannel, selector);
                }
                if (selectionKey.isReadable()) {
                    //6.2 如果可读状态
                    readOperator(selector, selectionKey);
                }
            }
        }
    }

    //处理可读状态操作
    private void readOperator(Selector selector, SelectionKey selectionKey) throws IOException {
        //1 从 SelectionKey 获取到已经就绪的通道
        SocketChannel channel = (SocketChannel) selectionKey.channel();

        //2 创建 buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //3 循环读取客户端消息
        int readLength = channel.read(byteBuffer);
        String message = "";//用于接收解码后的信息

        //表示里面有数据
        if (readLength > 0) {
            //切换读模式
            byteBuffer.flip();
            //读取内容
            message += Charset.forName("UTF-8").decode(byteBuffer);
        }

        //4 将 channel 再次注册到选择器上,监听可读状态
        channel.register(selector, SelectionKey.OP_READ);

        //5 把客户端发送消息,广播到其他客户端
        if (message.length() > 0) {
            //广播给其他客户端
            System.out.println(message);
            castOtherClient(message, selector, channel);
        }
    }

    //广播到其他客户端
    private void castOtherClient(String message, Selector selector, SocketChannel channel) throws IOException {
        //1 获取所有已经接入 channel
        Set<SelectionKey> selectionKeySet = selector.keys();
        //2 循环想所有 channel 广播消息
        for (SelectionKey selectionKey : selectionKeySet) {
            //获取每个 channel
            Channel tarChannel = selectionKey.channel();
            //不需要给自己发送
            if (tarChannel instanceof SocketChannel && tarChannel != channel) {//不向自己广播
                ((SocketChannel) tarChannel).write(Charset.forName("UTF-8").encode(message));
            }
        }
    }


    //处理接入状态操作
    private void acceptOperator(ServerSocketChannel serverSocketChannel, Selector selector) throws IOException {
        //1 接入状态,创建 socketChannel
        SocketChannel accept = serverSocketChannel.accept();
        //2 把 socketChannel 设置非阻塞模式
        accept.configureBlocking(false);
        //3 把 channel 注册到 selector 选择器上,监听可读状态
        accept.register(selector, SelectionKey.OP_READ);
        //4 客户端回复信息
        accept.write(Charset.forName("UTF-8").encode("欢迎进入聊天室,请注意隐私安全"));
    }

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

}

2. 客户端代码

package chatroom.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;

public class ChatClient {

    //启动方法
    public void startClient(String name) throws IOException {
        //连接服务端
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9090));
        //接收服务端响应数据
        Selector selector = Selector.open();
        socketChannel.configureBlocking(false);//设置非阻塞连接
        socketChannel.register(selector, SelectionKey.OP_READ);//将通道注册到selector上
        //创建线程,来接收服务端的响应信息
        new Thread(new ClientThread(selector)).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));
            }
        }
    }

}

3. 客户端异步监听服务端响应Runnable类

package chatroom.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;

public class ClientThread implements Runnable {

    private Selector selector;

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

    @Override
    public void run() {
        try {
            while (true) {
                //获取 channel 数量
                int readChannels = selector.select();
                if (readChannels == 0) {
                    continue;
                }
                //获取可用的 channel
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                //遍历集合
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    //移除 set 集合当前 selectionKey
                    iterator.remove();
                    //如果可读状态
                    if (selectionKey.isReadable()) {
                        //处理可读状态操作
                        readOperator(selector, selectionKey);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    //处理可读状态操作
    private void readOperator(Selector selector, SelectionKey selectionKey) throws IOException {
        //1 从 SelectionKey 获取到已经就绪的通道
        SocketChannel socketChannel =(SocketChannel) selectionKey.channel();
        //2 创建 buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //3 循环读取客户端消息
        int readLength = socketChannel.read(byteBuffer);
        String message = "";//用于存储解码后的消息
        if (readLength > 0) {
            //切换读模式
            byteBuffer.flip();
            //读取内容
            message += Charset.forName("UTF-8").decode(byteBuffer);
        }
        //4 将 channel 再次注册到选择器上,监听可读状态
        socketChannel.register(selector, SelectionKey.OP_READ);
        //5 把客户端发送消息,广播到其他客户端
        if (message.length() > 0) {
            //广播给其他客户端
            System.out.println(message);
        }
    }

}

4. A/B客户端

1)AClient

public class AClient {
    public static void main(String[] args) {
        try {
            new ChatClient().startClient("oliver一号");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2)BClient

public class BClient {
    public static void main(String[] args) {
        try {
            new ChatClient().startClient("oliver二号");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

演示:

服务端启动后,AClient和BClient都发送一条消息后的效果

  • 服务端控制台

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2rs8udg7-1631541201812)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210913215152188.png)]

  • AClient客户端控制台

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ZQMygfn-1631541201814)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210913215212188.png)]

  • BClient客户端控制台

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cLXuJqbZ-1631541201818)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20210913215224384.png)]