Java NIO 中的 Path 、Files 和 AsychronousFileChannel (附多人聊天室内代码)

439 阅读10分钟

「这是我参与2022首次更文挑战的第17天,活动详情查看:2022首次更文挑战」。

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.Path 接口类食欲 java.io.File 类。但是有一些差别。不过在许多情况下,可以使用 Path 接口来替换 File 类的使用

2、创建 Path 实例

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

示例代码

Path path = Paths.get("/xxx/01.txt");

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

3、创建绝对路径

(1)创建绝对够路径,通过调用 Paths.get() 方法,给绝对路径作为参数来完成:

示例代码:

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

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

(2)如果在 Linux 、MacOS 等操作系统上,上面的绝对路径可能如下:

Path path = Paths.get("/home/zsh/filename.txt")

绝对路径地址为 /home/zsh/filename.txt

(3) 如果在 Windows 及其上使用了从 /开始的路径,那么将被解释为相对当前驱动器。

4、创建相对路径

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

示例代码:

// 代码1
Path projects = Paths.get("c:\SoruceCode", "aproject");


// 代码2
Path projects = Paths.get("c:\SoruceCode", "aproject\a.txt");

代码 1 创建了一个 Java Path 实例, 指向路径(目录)“c:\SoruceCode\aproject”

代码 2 创建了一个 Path 的实例, 指向路径(文件)“c:\SoruceCode\aproject\a.txt”

5、 Path.normalize()

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

Path.normailze() 例子:

String originalPath = "c:\SoruceCode\..\ss-demo";

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

Path path2 = path1.normalize();
System.out.println("path2 = " + path2);

数据结果:标准化的路径不包含 SoruceCode\.. 部分

Files

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

1、Files.createDrectory()

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

实例:

Path path = Paths.get("/xx/newdir");

try {
    Path newDir = Files.createDirectory(path);
} catch (FileAlreadyExistsException ex) {
    // 目录已经存在
    ex.printStackTrace();
} catch (IOException e) {
    // 其他异常
    e.printStackTrace();
}

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

如果该目录已经存在则抛出一个 java.nio.file.FileAlreadyExistsException 。 如果出现其他的错误可能抛出 IOException。 例如,如果想要创建的新目录的父级目录不窜在,则可能抛出 IOException。

2、Files.copy()

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

示例:

Path sourcePath = Paths.get("/xxx/newdir/01.txt");

Path destinationPath = Paths.get("/xxx/newdir/01.txt");

try {
    Files.copy(sourcePath, destinationPath);
} catch (FileAlreadyExistsException ex) {
    // 目录已经存在
    ex.printStackTrace();
} catch (IOException e) {
    // 其他异常
    e.printStackTrace();
}

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

如果目标文件以及经存在。则抛出 java.nio.file.FileAlreadyExistsException 异常。如果有其他错误则抛出一个 IOException 。例如,如果将该文件复制到不存在的目录,则会抛出 IOException。

(2)覆盖已经存在的文件

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

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

3、Files.move()

Files.move() 用于文件从一个路径移动到另外一个路径。移动文件与重命名相同。移动文件既可以移动到不同的目录,也可以在相同的操作中更爱它的名称。

示例;

Path sourcePath = Paths.get("/xxx/newdir/01.txt");

Path destinationPath = Paths.get("/xxx/newdir/01.txt");

try {
    //Files.copy(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING);
    Files.move(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING);
} catch (FileAlreadyExistsException ex) {
    // 目录已经存在
    ex.printStackTrace();
} catch (IOException e) {
    // 其他异常
    e.printStackTrace();
}

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

4、Files.delete()

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

示例:

Path path = Paths.get("/xxx/newdir/01.txt");

try {
   
    Files.delete(path);
} catch (FileAlreadyExistsException ex) {
    // 目录已经存在
    ex.printStackTrace();
} catch (IOException e) {
    // 其他异常
    e.printStackTrace();
}

创建指向需要删除的文件的 Path 。 然后调用 Files.delete() 方法,如果 Files.delete() 不能删除文件(例如,文件或者目录不存在),会抛出一个 IOException。

5、Files.walkFileTree()

(1)Files.walkFileTree() 方法包含递归遍历目录树功能,将 Path 实例和 FileVisitor 作为参数。Path 实例指向要遍历的目录,FileVisitor 在遍历期间被调用。

(2)FileVisitor 是一个接口,必须自己实现 FilleVisitor 接口,并将实现的实例 床给 walkFileTree() 方法,在目录遍历过程中,您的 FileVisitor 实现的每个方法都将被调用。如果不需要实现所有这些方法,那么可以拓展 SimpleFileVisitor 类,它包含 FileVisitor 接口中所有方法的默认实现。

(3)Filevisitor 接口的一个方法中,每个都返回一个 FileVisitResult 枚举实例。 FileVisitResult 枚举包含以下四个选项:

  • CONTINUE 继续
  • TERMINATE 终止
  • SKIP_SIBING 跳过同级
  • SKIP_SUBTREE 跳过子级

(4) 查找一个名为 001.txt 文件例子

Path rootPath = Paths.get("c:\SourceCode");
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 = String.valueOf(file.toAbsolutePath());

            if (fileString.endsWith(filetoFind)) {
                System.out.println("file found at path" + file.toAbsolutePath());
                return FileVisitResult.TERMINATE;
            }

            return FileVisitResult.CONTINUE;
        }
    });
} catch (FileAlreadyExistsException ex) {
    // 目录已经存在
    ex.printStackTrace();
} catch (IOException e) {
    // 其他异常
    e.printStackTrace();
}

AsychronousFileChannel

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

1、创建 AynchronousFileChannel

通过静态方法 open 创建

Path path = Paths.get("/xxx/01.txt");
try {
    AsynchronousFileChannel fileChannel =
        AsynchronousFileChannel.open(path, StandardOpenOption.READ);
} catch (IOException e) {
    e.printStackTrace();
}

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

第二个参数是一个或者多个打开选项,它告诉 AsynchronousFileChannel 在我呢叫爱你上执行什么操作。在本例子中,我们使用了 StandardOpenOption.READ 选项,表示该文件将被打开阅读。

2、通过 Future 读取数据

可以通过两种方式从 AsynchronousFileChannel 读取数据。第一种方式是调用返回 Futrue 的 read() 方法。

示例:

Path path = Paths.get("/xxx/01.txt");
AsynchronousFileChannel fileChannel = null;
try {

    fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
} catch (IOException e) {
    e.printStackTrace();
}

ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
Future<Integer> future = fileChannel.read(buffer, position);

while (!future.isDone()) {
    ;
}

buffer.flip();
//        while (buffer.remaining() > 0) {
//            System.out.println(buffer.get());
//        }

byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println(new String(data));

buffer.clear();
fileChannel.close();

上述代码:

(1)创建了一个 AsynchronousFileChannel

(2)创建一个 ByteBuffer , 它被传递给了 read() 方法作为参数,以及第一个 0 的位置。

(3)在调用 read() 之后,循环,知道返回的 isDeme() 方法返回 true。

(4)读取操作完成之后,数据读取到 ByteBuffer 中,然后打印到 System.out 中。

3、通过 CompletionHandler 读取数据

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

示例:

Path path = Paths.get("/xxx/01.txt");
AsynchronousFileChannel fileChannel = null;
try {

    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);
                         buffer.flip();

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

                     }

                     @Override
                     public void failed(Throwable exc, ByteBuffer attachment) {

                     }
                 });
TimeUnit.SECONDS.sleep(1000);

4、通过 Futrue 写数据

示例:

Path path = Paths.get("/xxx/01.txt");
AsynchronousFileChannel fileChannel = null;
try {

    fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
} catch (IOException e) {
    e.printStackTrace();
}

ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;

buffer.put("hello! 2022".getBytes(StandardCharsets.UTF_8));
buffer.flip();

Future<Integer> write = fileChannel.write(buffer, 0);

while (!write.isDone()) ;

System.out.println("写数据完成");

TimeUnit.SECONDS.sleep(1000);

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

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

java.nio.file.NoSuchFileException

5、通过 CompletionHandler 写数据

示例:

Path path = Paths.get("/xxx/01.txt");
AsynchronousFileChannel fileChannel = null;
try {

    fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
} catch (IOException e) {
    e.printStackTrace();
}

ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;

buffer.put("hello ! 2022".getBytes(StandardCharsets.UTF_8));
buffer.flip();

fileChannel.write(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        System.out.println("write completed!!!");
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        System.out.println("write failed!!!");

    }
});
TimeUnit.SECONDS.sleep(1000);

当写操作完成时,将会调用 CompletionHandler 的 completed() 方法。如果写失败则会调用 failed() 方法

字符集(Charset)

java 中的 Charset 来表示字符集编码对象。

Charset 常用静态方法

// 通过编码类型获取 Charset 对象
public static Charset forName(String charsetName)
    
// 获得系统支持的素有编码方式
public static SortedMap<String, Charset> availableCharsets()    
    
// 获得虚拟机默认的编码方式
public static defaultCharset()    
    
// 判断是否支持该编码类型    
public static boolean isSupported(String charsetName)    

Charset 常用普通方法

// 获得 Charset 对象编码类型(String)
public final String name()

// 获得编码器对象
public abstract CharsetEncoder newEncoder()    

// 获得解码器对象
public abstract CharsetDecoder newDecoder()     

代码演示

public class CharsetDemo {


    public static void main(String[] args) throws CharacterCodingException {

        // 1、获取 charset  对象
        Charset charset = Charset.forName("UTF-8");

        // 2、获得编码器对象
        CharsetEncoder charsetEncoder = charset.newEncoder();

        // 3、创建缓冲区
        CharBuffer charBuffer = CharBuffer.allocate(1025);
        charBuffer.put("hello 2020 ! 加油!");

        // 4、编码
        charBuffer.flip();
        ByteBuffer buffer = charsetEncoder.encode(charBuffer);
        System.out.println("编码后的字符:");
        for (int i = 0; i < buffer.limit(); i++) {
            System.out.println(buffer.get());
        }

        // 5、获取解码器对象
        buffer.flip();
        CharsetDecoder charsetDecoder = charset.newDecoder();


        // 6、解码
        CharBuffer charBuffer1 = charsetDecoder.decode(buffer);
        System.out.println("解码后的字符:");
        System.out.println(charBuffer1.toString());

        // 7、通过其他类型的解码器解码
        Charset charset1 = Charset.forName("GBK");
        CharsetDecoder charsetDecoder1 = charset1.newDecoder();
        CharBuffer charBuffer2 = charsetDecoder1.decode(buffer);
        System.out.println("解码后的字符(GBK):");
        System.out.println(charBuffer2.toString());

        // 8、获取所有的字符串集
        Map<String, Charset> map = Charset.availableCharsets();
        map.forEach((k, v) -> System.out.println(k + "=" + v.toString()));
    }

}

Java NIO 综合案例

通过 Java NIO 完成一个多人聊天室的案例:

服务端代码:


// 服务端
public class ChatServer {

    // 服务启动
    public void startServer() throws IOException, InterruptedException {

        // 1、创建 Selector 选择器
        Selector selector = Selector.open();

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

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


        // 4、 把 channel 注册到到 selector 选择器上
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务器已经启动成功了");

        // 5、循环,等待新的连接介入
        for (; ; ) {


            // 获取 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();


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


    }

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

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

        //3 循环读取客户端发送过来的信息
        int readLen = channel.read(buffer);
        String message = "";
        if (readLen > 0) {
            buffer.flip();

            // 读取内容
            message += Charset.forName("UTF-8").decode(buffer);
        }

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

        //5 把客户端发送的消息,广播到其他的客户端上
        if (message != null && message.length() > 0) {

            // 广播到其他客户端
            System.out.println("message: " + message);

            castOtherClient(message, selector, channel);
        }
    }

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

        // 2 循环向所有的 channel 广播消息
        for (SelectionKey selectionKey : keys) {
            // 获取里面的每个通道

            SelectableChannel otherChannel = selectionKey.channel();
            // 不需要给自己发送
            if (otherChannel instanceof SocketChannel &&
                channel != otherChannel) {
                ((SocketChannel) otherChannel).write(Charset.forName("UTF-8").encode(message));
            }
        }

    }

    // 处理接入状态操作
    private void acceptOperator(ServerSocketChannel serverSocketChannel, Selector selector) throws IOException {
        // 1 接入状态,状态 创建 socketChannel
        SocketChannel socketChannel = serverSocketChannel.accept();

        // 2 把 socketChannel 设置为非阻塞模式
        socketChannel.configureBlocking(false);

        // 3 把 channel 注册到 selector 选择器上,监听可读状态
        socketChannel.register(selector, SelectionKey.OP_READ);

        // 4 客户端回复信息
        socketChannel.write(Charset.forName("UTF-8").encode("欢迎进入聊天室!"));

    }

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

客户端代码

// 客户端
// 客户端
public class ChatClient {


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

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

        // 向服务器发送消息
        System.out.println("聊天室客户端启动成功!!");
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String msg = scanner.nextLine();
            if (msg != null && msg.length() > 0) {
                socketChannel.write(Charset.forName("UTF-8").encode(name + " : " + msg));
            }
        }
        // 接收服务器的消息
    }

}


// 客户端处理线程
public class ClientThread implements Runnable {

    private Selector selector;

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

    @Override
    public void run() {
        try {
            // 循环,等待新的连接介入
            for (; ; ) {

                // 获取 channel 数量
                int readChannels = 0;

                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);
                    }
                }
                // TimeUnit.SECONDS.sleep(1);
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

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

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

        //3 循环读取客户端发送过来的信息
        int readLen = channel.read(buffer);
        String message = "";
        if (readLen > 0) {
            buffer.flip();

            // 读取内容
            message += Charset.forName("UTF-8").decode(buffer);
        }
        //4 将 channel 再次注册到选择器上,监听可读状态。
        channel.register(selector, SelectionKey.OP_READ);
        if (message.length() > 0) {
            // 输出
            System.out.println("收到 message: " + message);
        }

    }
}