Java7-NIO2-高级教程-全-

Java7 NIO2 高级教程(全)

协议:CC BY-NC-SA 4.0

零、前言

这本书涵盖了开发基于 NIO.2 的应用所涉及的所有重要方面。它为充分利用 NIO.2 提供了清晰的说明,并提供了许多练习和案例研究,用新的 I/O 功能来丰富 Java 7 应用。您将学习开发 NIO.2 应用,从简单但基本的东西开始,逐渐过渡到复杂的特性,如套接字和异步通道。

这本书是给谁的

这本书既适用于不熟悉 Java 7 的有经验的 Java 程序员,也适用于对 Java 7 有一些经验的人。对于开篇章节(章节 1 - 5 ),熟悉 Java 语法,知道如何打开和运行 NetBeans 项目就足够了。对于第六章-10 章),了解一些基本的编程概念是必不可少的,比如递归、多线程和并发、互联网协议和网络应用。

这本书涵盖了什么

本节包含每章内容的简要总结。

进行Path类操作

第一章:在这里,你会看到新的用于操作文件路径的 API 您现在可以使用java.nio.file.Path类来操作任何文件系统中的文件。在这一章中,我将介绍一些重要的主题,比如声明路径实例和语法操作。

通过新的 java.nio.file.attribute API(包括 POSIX)获取/设置文件元数据

第二章:使用 NIO.2,你可以管理比以前更多的关于文件元数据的细节。属性被分成不同的类别,现在它们也包含了 POSIX 系统。第二章深入探讨了每一个类别。

管理符号链接和硬链接

第三章:nio . 2 展示了 Java 的一个未开发领域。本章向您展示了如何创建、跟踪和操作符号链接和硬链接。

通过新的 java.nio.file.Files API 处理文件和目录

第四章:在这里,您将学习涉及文件/目录的最常见任务,例如创建、读取、写入、更新等等。您将学习如何检查文件状态和循环文件存储,如何处理临时文件,以及如何删除、复制和移动文件和目录。

使用 FileVisitor API 开发递归文件操作

第五章:需要复制、移动或删除整个目录吗?你来对地方了。第五章向您展示了如何通过全新的 FileVisitor API 完成所有这些工作。您还将了解如何开发一个搜索文件工具。

探索监视服务 API 和文件更改通知

第六章:想监控文件/目录的变化,比如条目的创建、删除或修改?这是监视服务做得最好的。在本章中,我还将介绍如何观察打印托盘和摄像机。在这里,您会发现新的监视服务 API 是多么灵活和通用。

使用新的 SeekableByteChannel API 处理随机访问文件

第七章:随机访问文件(RAF)在合适的人手里是一个强大的工具。本章介绍了新的SeekableByteChannel API,并提供了大量利用其方法的例子。实践,实践,实践,并超越成为皇家空军学徒!

开发基于阻塞/非阻塞套接字的应用

第八章:学习如何以阻塞和非阻塞方式开发基于 Java 网络的应用。我将详细介绍 TCP 和 UDP,并在本章中介绍套接字编程的重要方面。

使用 NIO.2 皇冠上的宝石:异步通道 API

第九章:这是我自己个人最喜欢的一章。写作是一种乐趣,我希望你会发现它很有用,就像我发现它很有趣一样。使用异步通道 API,您可以使用一套类和选项开发基于异步网络的 Java 应用。异步通道 API 棒极了!

使用 Zip 文件系统提供程序并编写一个定制的文件系统提供程序

第十章:这最后一章以一个使用新的 Zip 文件系统提供者的例子结束了这本书。我还提出了一些关于编写定制文件系统提供者的考虑事项。第十章还包含了一个表格,详细说明了java.io.Filejava.nio.file.PathAPI 之间的转换。

一、使用Path

开始探索 NIO.2 API(也称为“JSR 203:Java 平台的更多新 I/O API”(nio . 2))的推荐切入点是新的抽象类java.nio.file.Path。这个类是 NIO.2 的一个里程碑,每个涉及 I/O 操作的应用都将利用这个类的强大功能。实际上,它是 NIO.2 中最常用的类,因为许多 I/O 操作都基于一个Path资源。

Path类支持两种类型的操作:语法操作(几乎任何涉及操纵路径而不访问文件系统的操作;这些是在内存中完成的逻辑操作)和对路径引用的文件的操作。本章涵盖了第一种类型的操作,并向您介绍了Path API。在第四章的中,我将重点探讨第二种类型的操作。本章介绍的概念将在本书的其余部分非常有用。

引入Path

路径驻留在文件系统中,该文件系统“将文件存储和组织在某种形式的介质上,通常是一个或多个硬盘驱动器上,以便于检索。” 1 可以通过java.nio.file.FileSystems final 类访问文件系统,该类用于获取我们想要处理的java.nio.file.FileSystem的实例。FileSystems包含以下两个重要的方法,以及一组newFileSystem()方法,用于构建新的文件系统:

  • getDefault():这是一个静态方法,将默认的FileSystem返回给 JVM——通常是操作系统的默认文件系统。
  • getFileSystem(URI uri):这是一个静态方法,它从与给定的 URI 模式相匹配的一组可用文件系统提供者中返回一个文件系统。Path类操纵任何文件系统(FileSystem)中的文件,该文件系统可以使用任何存储位置(java.nio.file.FileStore);这个类代表底层存储)。默认情况下(通常情况下),Path指的是默认文件系统(计算机的文件系统)中的文件,但 NIO.2 是完全模块化的——针对内存、网络或虚拟文件系统中的数据实现FileSystem完全符合 NIO.2 的要求。NIO.2 为我们提供了可能需要通过文件、目录或链接执行的所有文件系统功能。

1 甲骨文,Java 教程,什么是路径?(以及其他文件系统事实),download . Oracle . com/javase/tutorial/essential/io/path . html

Path类是众所周知的java.io.File类的升级版本,但File类保留了一些特定的操作,因此它没有被弃用,也不能被认为已经过时。此外,从 Java 7 开始,这两个类都是可用的,这意味着程序员可以混合他们的能力来获得最好的 I/O API。Java 7 为它们之间的转换提供了一个简单的 API。还记得你必须做以下事情的日子吗?

import java.io.File;
…
File file = new File("index.html");

好了,那些日子已经一去不复返了,因为有了 Java 7,你可以这样做:

import java.nio.file.Path;
import java.nio.file.Paths;
…
Path path = Paths.get("index.html");

仔细看,Path是文件系统中路径的编程表示。路径字符串包含文件名、目录列表和操作系统相关的文件分隔符(例如,Microsoft Windows 上的反斜杠“\”和 Solaris 和 Linux 上的正斜杠“/”),这意味着Path不是独立于系统的,因为它基于系统相关的字符串路径。因为Path基本上是一个字符串,引用的资源可能不存在。

定义路径

一旦确定了文件系统以及文件或目录的位置,就可以为它创建一个Path对象。绝对路径、相对路径、用符号“.”(表示当前目录)或“..”(表示父目录)定义的路径,以及仅包含文件/目录名的路径包含在Path类中。定义Path最简单的解决方案是调用Paths助手类的get()方法之一。下面的小节介绍了几种不同的方法来定义同一个文件的路径(在 Windows 上)——C:\rafaelnadal\tournaments\2009\BNP.txt

定义一个绝对路径

绝对路径(也称为完整路径或文件路径)是包含根目录和所有其他包含文件或文件夹的子目录的路径。在 NIO.2 中定义绝对路径是一个只有一行代码的任务,正如您在下面的例子中所看到的,它指向了在C:\rafaelnadal\tournaments\2009目录中名为BNP.txt的文件(该文件可能不存在以测试这段代码):

Path path = Paths.get("C:/rafaelnadal/tournaments/2009/BNP.txt");

get()也允许你将一个路径分割成一组块。NIO 将为您重建路径,不管有多少块。请注意,如果您为路径的每个组件定义了一个块,则可以省略文件分隔符。前面的绝对路径可以被分块为“如下”:

Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
Path path = Paths.get("C:", "rafaelnadal/tournaments/2009", "BNP.txt");
Path path = Paths.get("C:", "rafaelnadal", "tournaments", "2009", "BNP.txt");

定义相对于文件存储根目录的路径

相对路径(也称为非绝对路径或部分路径)只是完整路径的一部分。创建网页时经常使用相对路径。相对路径比绝对路径使用得更频繁。定义相对于当前文件存储根目录的路径应以文件分隔符开始。在以下示例中,如果当前文件存储根目录是C:,则绝对路径是C:\rafaelnadal\tournaments\2009\BNP.txt:

Path path = Paths.get("/rafaelnadal/tournaments/2009/BNP.txt");
Path path = Paths.get("/rafaelnadal","tournaments/2009/BNP.txt");

定义相对于工作文件夹的路径

当你定义一个相对于当前工作文件夹的路径时,该路径应该而不是以文件分隔符开始。如果当前文件夹是C:根目录下的/ATP,那么下面这段代码返回的绝对路径是C:\ATP\rafaelnadal\tournaments\2009\BNP.txt:

Path path = Paths.get("rafaelnadal/tournaments/2009/BNP.txt");
Path path = Paths.get("rafaelnadal","tournaments/2009/BNP.txt");

使用快捷键定义路径

使用符号“.”(表示当前目录)或“..”(表示父目录)来定义路径是一种常见的做法。NIO.2 可以处理这些类型的路径,以消除可能的冗余情况,如果您调用Path.normalize()方法(该方法删除任何冗余元素,包括任何".或"目录 /.."出现):

Path path = Paths.get("C:/rafaelnadal/tournaments/2009/dummy/../BNP.txt").normalize();
Path path = Paths.get("C:/rafaelnadal/tournaments/./2009/dummy/../BNP.txt").normalize();

如果你想看看normalize()方法的效果,试着定义相同的Path和不定义normalize(),如下,并将结果打印到控制台:

Path noNormalize = Paths.get("C:/rafaelnadal/tournaments/./2009/dummy/../BNP.txt");
Path normalize = Paths.get("C:/rafaelnadal/tournaments/./2009/dummy/../BNP.txt").normalize();

如果您使用System.out.println()打印前面的路径,您将会看到下面的结果,其中normalize()已经删除了多余的元素:


C:\rafaelnadal\tournaments\.\2009\dummy\..\BNP.txt


C:\rafaelnadal\tournaments\2009\BNP.txt

从 URI 定义一条路径

在某些情况下,您可能需要从统一资源标识符(URI)创建一个Path。您可以通过使用URI.create()方法从给定的字符串创建一个 URI,并通过使用将 URI 对象作为参数的Paths.get()方法来实现。如果您需要封装可以输入到 web 浏览器地址栏中的路径字符串,这将非常有用:

import java.net.URI;
…
Path path = Paths.get(URI.create("file:///rafaelnadal/tournaments/2009/BNP.txt"));
Path path = Paths.get(URI.create("file:///C:/rafaelnadal/tournaments/2009/BNP.txt"));

使用 FileSystems.getDefault()定义路径。getPath()方法

创建Path的另一个常见解决方案是使用FileSystems类。首先,调用getDefault()方法获得默认的FileSystem——nio . 2 将提供一个通用对象,它能够访问默认的文件系统。然后,您可以如下调用getPath()方法(前面例子中的Paths.get()方法只是这个解决方案的简写):

import java.nio.file.FileSystems;
…
Path path = FileSystems.getDefault().getPath("/rafaelnadal/tournaments/2009", "BNP.txt");
Path path = FileSystems.getDefault().getPath("/rafaelnadal/tournaments/2009/BNP.txt");
Path path = FileSystems.getDefault().getPath("rafaelnadal/tournaments/2009", "BNP.txt");
Path path = FileSystems.getDefault().
                        getPath("/rafaelnadal/tournaments/./2009","BNP.txt").normalize();

获取主目录的路径

当您需要指向主目录的路径时,您可以按照下面的示例进行操作(返回的主目录取决于每台计算机和每个操作系统):

Path path = Paths.get(System.getProperty("user.home"), "downloads", "game.exe");

在我的 Windows 7 机器上,这个返回C:\Users\Leo\downloads\game.exe,而在我朋友的 CentOS 系统(Linux)上,这个返回/home/simpa/downloads/game.exe

获取关于路径的信息

在定义了一个Path对象之后,您可以访问一组方法,这些方法提供了关于路径元素的有用信息。这些方法基于以下事实:NIO.2 将路径字符串拆分成一组元素(一个元素是表示目录或文件的子路径),并将索引 0 分配给最高的元素,将索引n–1 分配给最低的元素,其中 n 是路径元素的数量;通常,最高的元素是根文件夹,最低的元素是文件。本节提供了将这些信息获取方法应用于路径C:\rafaelnadal\tournaments\2009\BNP.txt的示例:

Path path = Paths.get("C:", "rafaelnadal/tournaments/2009", "BNP.txt");

获取路径文件/目录名

由路径指示的文件/目录由getFileName()方法返回,它是目录层次结构中离根最远的元素:

//output: BNP.txt
System.out.println("The file/directory indicated by path: " + path.getFileName());

获取路径根

路径的根可以用getRoot()方法获得(如果Path没有根,则返回null):

//output: C:\
System.out.println("Root of this path: " + path.getRoot());

获取路径父项

该路径的父路径(路径的根组件)由getParent()方法返回(如果Path没有父路径,则返回null):

//output: C:\rafaelnadal\tournaments\2009
System.out.println("Parent: " + path.getParent());

获取路径名元素

您可以使用getNameCount()方法获得路径中元素的数量,使用getName()方法获得每个元素的名称:

//output: 4
System.out.println("Number of name elements in path: " + path.getNameCount());

//output: rafaelnadal  tournaments  2009  BNP.txt
for (int i = 0; i < path.getNameCount(); i++) {
  System.out.println("Name element " + i + " is: " + path.getName(i));
}

获取路径子路径

您可以使用subpath()方法提取一个相对路径,该方法获得两个参数,开始索引和结束索引,表示元素的子序列:

//output: rafaelnadal\tournaments\2009
System.out.println("Subpath (0,3): " + path.subpath(0, 3));

转换路径

在本节中,您将看到如何将一个Path对象转换成一个字符串、一个 URI、一个绝对路径、一个真实路径和一个File对象。Path类包含了每一个转换的专用方法,如下面的小节所示。以下是我们将要使用的路径:

Path path = Paths.get("/rafaelnadal/tournaments/2009", "BNP.txt");

将路径转换成字符串

路径的字符串转换可以通过toString()方法实现:

//output: \rafaelnadal\tournaments\2009\BNP.txt
String path_to_string = path.toString();
System.out.println("Path to String: " + path_to_string);

将路径转换为 URI

您可以通过应用toURI()方法将Path转换为 web 浏览器格式字符串,如下例所示。结果是一个 URI 对象,它封装了一个可以输入到 web 浏览器地址栏中的路径字符串。

//output: file:///C:/rafaelnadal/tournaments/2009/BNP.txt
URI path_to_uri = path.toUri();
System.out.println("Path to URI: " + path_to_uri);

将相对路径转换为绝对路径

从相对路径获取绝对路径是一项非常常见的任务。NIO.2 可以用toAbsolutePath()方法做到这一点(注意,如果您将这个方法应用到一个已经是绝对的路径,那么将返回相同的路径):

//output: C:\rafaelnadal\tournaments\2009\BNP.txt
Path path_to_absolute_path = path.toAbsolutePath();
System.out.println("Path to absolute path: " + path_to_absolute_path.toString());

将路径转换为真实路径

toRealPath()方法返回现有文件的真实路径——这意味着该文件必须存在,如果使用toAbsolutePath()方法,这是不必要的。如果没有参数传递给此方法,并且文件系统支持符号链接,此方法将解析路径中的任何符号链接。如果你想忽略符号链接,那么给方法传递LinkOption.NOFOLLOW_LINKS枚举常量。此外,如果Path是相对的,它返回一个绝对路径,如果Path包含任何冗余元素,它返回一个删除了这些元素的路径。如果文件不存在或者不能被访问,这个方法抛出一个IOException

以下代码片段通过不跟随符号链接来返回文件的真实路径:

import java.io.IOException;
…
//output: C:\rafaelnadal\tournaments\2009\BNP.txt
try {
    Path real_path = path.toRealPath(LinkOption.NOFOLLOW_LINKS);
    System.out.println("Path to real path: " + real_path);
} catch (NoSuchFileException e) {
    System.err.println(e);
} catch (IOException e) {
    System.err.println(e);
}

将路径转换成文件

也可以使用toFile()方法将Path转换成File对象,如下所示。这是PathFile之间的一座巨大桥梁,因为File类也包含一个名为toPath()的方法用于恢复。

//output: BNP.txt
File path_to_file = path.toFile();

//output: \rafaelnadal\tournaments\2009\BNP.txt
Path file_to_path = path_to_file.toPath();
System.out.println("Path to file name: " + path_to_file.getName());
System.out.println("File to path: " + file_to_path.toString());

结合两条路径

组合两个路径是一种技术,它允许您定义一个固定的根路径,并向它附加一个部分路径。这对于基于公共零件定义路径非常有用。NIO.2 通过resolve()方法提供了这种操作。以下是其工作原理的一个示例:

//define the fixed path
Path base = Paths.get("C:/rafaelnadal/tournaments/2009");

//resolve BNP.txt file
Path path_1 = base.resolve("BNP.txt");
//output: C:\rafaelnadal\tournaments\2009\BNP.txt
System.out.println(path_1.toString());

//resolve AEGON.txt file
Path path_2 = base.resolve("AEGON.txt");
//output: C:\rafaelnadal\tournaments\2009\AEGON.txt
System.out.println(path_2.toString());

还有一个方法专用于兄弟路径,名为resolveSibling()。它根据当前路径的父路径解析传递的路径。实际上,这个方法用给定路径的文件名替换当前路径的文件名。

下面的例子阐明了这个观点:

//define the fixed path
Path base = Paths.get("C:/rafaelnadal/tournaments/2009/BNP.txt");

//resolve sibling AEGON.txt file
Path path = base.resolveSibling("AEGON.txt");
//output: C:\rafaelnadal\tournaments\2009\AEGON.txt
System.out.println(path.toString());

在两个地点之间建造一条道路

当需要构造一个从一个位置到另一个位置的路径时,可以调用relativize()方法,该方法在这个路径和给定路径之间构造一个相对路径。此方法构造一个路径,该路径从原始路径开始,在传入路径指定的位置结束。新路径相对于原始路径。为了更好地理解这个强大的工具,考虑一个简单的例子。假设您有以下两条相对路径:

Path path01 = Paths.get("BNP.txt");
Path path02 = Paths.get("AEGON.txt");

在这种情况下,假设BNP.txtAEGON.txt是兄弟,这意味着您可以通过向上一级然后向下一级从一个导航到另一个。应用relativize()方法输出..\AEGON.txt..\BNP.txt:

//output:  ..\AEGON.txt
Path path01_to_path02 = path01.relativize(path02);
System.out.println(path01_to_path02);

//output:  ..\BNP.txt
Path path02_to_path01 = path02.relativize(path01);
System.out.println(path02_to_path01);

另一种典型的情况是包含根元素的两条路径。考虑以下路径:

Path path01 = Paths.get("/tournaments/2009/BNP.txt");
Path path02 = Paths.get("/tournaments/2011");

在这种情况下,两个路径包含相同的根元素/tournaments。要从path01导航到path02,您将向上两级,向下一级(..\..\2011)。要从path02导航到path01,您将上升一级并下降两级(..\2009\BNP.txt)。这正是relativize()方法的工作原理:

//output:  ..\..\2011
Path path01_to_path02 = path01.relativize(path02);
System.out.println(path01_to_path02);

//output:  ..\2009\BNP.txt
Path path02_to_path01 = path02.relativize(path01);
System.out.println(path02_to_path01);

注意如果只有一条路径包含根元素,那么就不能构造相对路径。两条路径都必须包含一个根元素。即使这样,相对路径的构造也是依赖于系统的。

比较两条路径

出于不同的目的,可以用不同的方式来检验两个Paths是否相等。您可以通过调用Path.equals()方法来测试两条路径是否相等。这种方法符合Object.equals()规范。它不访问文件系统,因此不需要存在比较的路径,并且它不检查路径是否是同一个文件。在某些操作系统实施中,路径通过忽略大小写进行比较,而在其他实施中,比较区分大小写,实施将指定是否考虑大小写。这里我显示了一个相对于当前文件存储的路径和一个绝对路径,两者代表相同的文件,但不相等:

Path path01 = Paths.get("/rafaelnadal/tournaments/2009/BNP.txt");
Path path02 = Paths.get("C:/rafaelnadal/tournaments/2009/BNP.txt");

if(path01.equals(path02)){
    System.out.println("The paths are equal!");
} else {
    System.out.println("The paths are not equal!"); //true
}

有时你会想检查两个路径是否是同一个文件/文件夹。您可以通过调用java.nio.File.Files.isSameFile()方法(如下例所示)轻松实现这一点,该方法返回一个布尔值。在幕后,这个方法使用了Path.equals()方法。如果Path.equals()返回true,则路径相等,因此无需进一步比较。如果返回false,则isSameFile()方法进入动作以再次检查。请注意,此方法要求比较的文件存在于文件系统中;否则,它抛出一个IOException

try {
    boolean check = Files.isSameFile(path01, path02);
    if(check){
        System.out.println("The paths locate the same file!"); //true
    } else {
        System.out.println("The paths does not locate the same file!");
    }
} catch (IOException e) {
    System.out.println(e.getMessage());
}

由于Path类实现了Comparable接口,您可以通过使用compareTo()方法来比较路径,该方法按字典顺序比较两个抽象路径。这对于排序很有用。如果参数等于此路径,则方法返回零;如果此路径在字典序上小于参数,则返回小于零的值;如果此路径在字典序上大于参数,则返回大于零的值。下面是使用compareTo()方法的一个例子:

//output: 24
int compare = path01.compareTo(path02);
System.out.println(compare);

使用startsWith()endsWith()方法可以完成部分比较,如下例所示。使用这些方法,您可以分别测试当前路径是以给定路径开始还是结束。这两种方法都返回布尔值。

boolean sw = path01.startsWith("/rafaelnadal/tournaments");
boolean ew = path01.endsWith("BNP.txt");
System.out.println(sw);  //output:  true
System.out.println(ew);  //output:  true

遍历路径的名称元素

由于Path类实现了Iterable接口,您可以获得一个对象,使您能够迭代路径中的元素。您可以通过使用显式迭代器或者使用每次迭代返回一个Path对象的foreach循环进行迭代。下面是一个例子:

Path path = Paths.get("C:", "rafaelnadal/tournaments/2009", "BNP.txt");

for (Path name : path) {
    System.out.println(name);
}

这将从最接近根的元素开始输出元素,如下所示:


rafaelnadal

tournaments

2009

BNP.txt

总结

在本章中,您已经迈出了进入 NIO.2 API 的第一步。除了学习基本的 NIO.2 概念,比如文件系统和文件存储,您还了解了对Path类的概述,这些知识对于每个想要学习如何使用 NIO.2 API 的开发人员来说都是必不可少的。知道如何获得默认文件系统以及如何定义和操作文件路径是很重要的,因为Path类将贯穿全书,并且通常是应用的入口点。

二、元数据文件属性

如果您对文件或目录有疑问,例如它是否隐藏、它是否是目录、它的大小以及它的所有者是谁,您可以从元数据中获得这些问题(以及许多其他问题)的答案,元数据是关于其他数据的数据。

NIO.2 将元数据的概念与属性相关联,并通过java.nio.file.attribute包提供对它们的访问。由于不同的文件系统对于应该跟踪哪些属性有不同的概念,NIO.2 将属性分组到视图中,每个视图映射到一个特定的文件系统实现。通常,视图通过一个通用的方法readAttributes()批量提供属性。此外,您可以分别使用getAttribute()setAttribute()方法提取和设置单个属性,这些方法在java.nio.file.Files类中可用。根据视图,其他方法可用于其他任务。

在本章中,您将学习如何使用 NIO.2 提供的视图。您将看到如何确定文件是只读的还是隐藏的,最后一次访问或修改它的时间,谁拥有它,以及如何获得它的所有权。您还将了解如何查看文件的访问控制列表(ACL ),以及如何设置文件的 Unix 权限。此外,您将探索文件存储属性,并学习如何定义自己的属性。

nio . 2 中支持的视图

NIO.2 附带了一组六个视图,概述如下:

  • BasicFileAttributeView:这是所有文件系统实现必须支持的基本属性的视图。属性视图名称为basic
  • DosFileAttributeView:这个视图在支持 DOS 属性的文件系统上提供了标准的四个受支持的属性。属性视图名称为dos
  • PosixFileAttributeView:这个视图用支持 POSIX(Unix 可移植操作系统接口)系列标准的文件系统上支持的属性扩展了基本属性视图,比如 Unix。属性视图名称为posix
  • 任何支持文件所有者概念的文件系统实现都支持这个视图。属性视图名称为owner
  • AclFileAttributeView:该视图支持读取或更新文件的 ACL。支持 NFSv4 ACL 模型。属性视图名称为acl
  • UserDefinedFileAttributeView:该视图支持用户定义的元数据。

确定特定文件系统支持的视图

在尝试访问视图的属性之前,请确保您的文件系统支持相应的视图。NIO.2 允许您按名称查看受支持视图的完整列表,或者检查文件存储——由映射任何类型的存储(如分区、设备、卷等)的FileStore类表示——是否支持特定的视图。

一旦获得了对默认文件系统的访问权——通过调用FileSystems.getDefault()方法——就可以轻松地遍历由FileSystem.supportedFileAttributeViews()方法返回的受支持的视图。以下代码片段显示了如何做到这一点:

import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.util.Set;
…
FileSystem fs = FileSystems.getDefault();        
Set<String> views = fs.supportedFileAttributeViews();

for (String view : views) {
      System.out.println(view);
}

例如,对于 Windows 7,上述代码返回以下结果:


acl

basic

owner

user

dos

Image 注意所有文件系统都支持基本视图,所以在输出中至少应该得到basic名称。

您可以通过调用FileStore.supportsFileAttributeView()方法来测试文件存储上的特定视图。您可以将所需的视图作为一个String或一个类名来传递。以下代码检查所有可用的文件存储是否都支持基本视图:

import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.attribute.BasicFileAttributeView;
…
FileSystem fs = FileSystems.getDefault();
for (FileStore store : fs.getFileStores()) {
   boolean supported = store.supportsFileAttributeView(BasicFileAttributeView.class);
   System.out.println(store.name() + " ---" + supported);
}

此外,您可以检查特定文件所在的文件存储是否支持单一视图,如下例所示:

import java.io.IOException;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
…
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");

try {
    FileStore store = Files.getFileStore(path);
    boolean supported = store.supportsFileAttributeView("basic");
    System.out.println(store.name() + " ---" + supported);
} catch (IOException e) {
    System.err.println(e);
}

既然您已经确定了文件系统支持哪些视图,那么是时候深入研究每个视图的属性了,从基本视图开始。

基本观点

大多数文件系统实现支持一组公共属性(大小、创建时间、上次访问时间、上次修改时间等。).这些属性被分组到一个名为BasicFileAttributeView的视图中,可以按照下面的小节进行提取和设置。

用 readAttributes()获取批量属性

您可以使用readAttributes()方法批量提取属性,如下所示(varargs参数目前支持LinkOption.NOFOLLOW_LINKS枚举——不要使用符号链接):

import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; `import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;

BasicFileAttributes attr = null;
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");

try {
    attr = Files.readAttributes(path, BasicFileAttributes.class);
} catch (IOException e) {
    System.err.println(e);
}

System.out.println("File size: " + attr.size());
System.out.println("File creation time: " + attr.creationTime());
System.out.println("File was last accessed at: " + attr.lastAccessTime());
System.out.println("File was last modified at: " + attr.lastModifiedTime());

System.out.println("Is directory? " + attr.isDirectory());
System.out.println("Is regular file? " + attr.isRegularFile());
System.out.println("Is symbolic link? " + attr.isSymbolicLink());
System.out.println("Is other? " + attr.isOther());`

用 getAttribute()获取单个属性

如果您需要提取单个属性,而不是批量提取所有属性,请使用getAttribute()方法。您需要传递文件路径和属性名,并指定是否需要跟随符号链接。下面的代码片段展示了如何提取size属性值。请记住,getAttribute()方法返回一个Object,因此您需要根据属性的值类型进行显式转换。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
…
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
try {
    long size = (Long)Files.getAttribute(path, "basic:size", NOFOLLOW_LINKS);                    
    System.out.println("Size: " + size);
} catch (IOException e) {
    System.err.println(e);
}

基本属性名称如下所示:

  • lastModifiedTime
  • lastAccessTime
  • creationTime
  • size
  • isRegularFile
  • isDirectory
  • isSymbolicLink
  • isOther
  • fileKey

普遍接受的检索单个属性的形式是[view-name:]attribute-name。这个view-name就是basic

更新基本属性

更新文件的最后修改时间、最后访问时间和创建时间属性中的任何一个或全部可以使用setTimes()方法来完成,该方法将表示最后修改时间、最后访问时间和创建时间的三个参数作为FileTime的实例,这是 Java 7 中的一个新类,表示文件的时间戳属性的值。如果lastModifiedTimelastAccessTimecreationTime中的任何一个具有值null,则相应的时间戳不改变。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.FileTime;
…
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
long time = System.currentTimeMillis();
FileTime fileTime = FileTime.fromMillis(time);
try {
    Files.getFileAttributeView(path,
    BasicFileAttributeView.class).setTimes(fileTime, fileTime, fileTime);
} catch (IOException e) {
    System.err.println(e);
}

更新文件的最后修改时间也可以通过Files.setLastModifiedTime()方法完成:

long time = System.currentTimeMillis();
FileTime fileTime = FileTime.fromMillis(time);
try {
    Files.setLastModifiedTime(path, fileTime);
} catch (IOException e) {
    System.err.println(e);
}

更新文件的最后修改时间也可以用setAttribute()方法来完成。实际上,这个方法可以用来更新文件的最后修改时间、最后访问时间,或者创建时间属性,就像调用setTimes()方法一样:

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
…
try {
    Files.setAttribute(path, "basic:lastModifiedTime", fileTime, NOFOLLOW_LINKS);
    Files.setAttribute(path, "basic:creationTime", fileTime, NOFOLLOW_LINKS);
    Files.setAttribute(path, "basic:lastAccessTime", fileTime, NOFOLLOW_LINKS);
} catch (IOException e) {
    System.err.println(e);
}

显然,现在您必须提取这三个属性的值才能看到变化。您可以通过使用getAttribute()方法来实现:

try {
    FileTime lastModifiedTime = (FileTime)Files.getAttribute(path,
                                 "basic:lastModifiedTime", NOFOLLOW_LINKS);
    FileTime creationTime = (FileTime)Files.getAttribute(path,
                                 "basic:creationTime", NOFOLLOW_LINKS);
    FileTime lastAccessTime = (FileTime)Files.getAttribute(path,
                                 "basic:lastAccessTime", NOFOLLOW_LINKS);

    System.out.println("New last modified time: " + lastModifiedTime);
    System.out.println("New creation time: " + creationTime);
    System.out.println("New last access time: " + lastAccessTime);

} catch (IOException e) {
      System.err.println(e);
}

DOS 视图

具体到 DOS 文件系统(或 Samba),DosFileAttributeView视图用 DOS 属性扩展了基本视图(这意味着可以直接从 DOS 视图访问基本视图)。共有四种属性,通过以下方法进行映射:

  • isReadOnly():返回readonly属性的值(如果true,文件不能被删除或更新)
  • isHidden():返回hidden属性的值(如果true,文件对用户不可见)
  • isArchive():返回archive属性的值(特定于备份程序)
  • isSystem():返回system属性的值(如果true,则该文件属于操作系统)

下面的清单大量提取了给定路径的前四个属性:

import java.io.IOException; `import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.DosFileAttributes;
...
DosFileAttributes attr = null;
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");

try {
    attr = Files.readAttributes(path, DosFileAttributes.class);
} catch (IOException e) {
    System.err.println(e);
}

System.out.println("Is read only ? " + attr.isReadOnly());
System.out.println("Is Hidden ? " + attr.isHidden());
System.out.println("Is archive ? " + attr.isArchive());
System.out.println("Is system ? " + attr.isSystem());`

设置属性值和通过名称提取单个属性可以分别通过setAttribute()getAttribute()方法来完成,如下所示(我随机选择了hidden属性):

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
…
//setting the hidden attribute to true
try {
    Files.setAttribute(path, "dos:hidden", true, NOFOLLOW_LINKS);
} catch (IOException e) {
    System.err.println(e);
}

//getting the hidden attribute
try {
    boolean hidden = (Boolean) Files.getAttribute(path, "dos:hidden", NOFOLLOW_LINKS);
    System.out.println("Is hidden ? " + hidden);
} catch (IOException e) {
     System.err.println(e);
}

可以使用以下名称获取 DOS 属性:

  • hidden
  • readonly
  • system
  • archive

普遍接受的形式是[view-name:]attribute-name。这个view-name就是dos

文件所有者视图

大多数文件系统接受文件所有者的概念,将其作为一种身份来确定对文件系统中对象的访问权限。NIO.2 将这一概念映射到一个名为UserPrincipal的接口中,并允许您通过文件所有者视图(由FileOwnerAttributeView接口表示)来获取或设置文件的所有者。实际上,正如您将在下面的代码示例中看到的,NIO.2 有多种方式来设置和获取文件所有者。

Image 注意在本节的示例中使用了一个名为“进程的主体,但是这个主体在您的机器上不可用。为了在没有获得java.nio.file.attribute.UserPrincipalNotFoundException的情况下测试代码,您需要添加您的主体名称(您的机器的管理员用户或具有适当操作系统特权的用户)。

使用 Files.setOwner()设置文件所有者

您可以通过调用Files.setOwner()方法来设置文件所有者。除了文件路径之外,这个方法还获得了一个UserPrincipal实例,该实例映射了一个表示文件所有者的字符串。默认文件系统的用户主体查找服务可以通过调用FileSystem.getUserPrincipalLookupService()方法获得。下面是一个设置文件所有者的简单示例:

import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.UserPrincipal; ... UserPrincipal owner = null; Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt"); try {     owner = path.getFileSystem().getUserPrincipalLookupService().                                        lookupPrincipalByName("apress");     Files.setOwner(path, owner); } catch (IOException e) {     System.err.println(e); }

使用 FileOwnerAttributeView.setOwner()设置文件所有者

FileOwnerAttributeView映射了一个支持读取或更新文件所有者的文件属性视图。owner属性由名称owner标识,属性值是一个UserPrincipal对象。以下代码片段向您展示了如何使用此接口设置所有者:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileOwnerAttributeView;
import java.nio.file.attribute.UserPrincipal;
...
UserPrincipal owner = null;
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
FileOwnerAttributeView foav = Files.getFileAttributeView(path,
                                           FileOwnerAttributeView.class);
try {
    owner = path.getFileSystem().getUserPrincipalLookupService().
                                 lookupPrincipalByName("apress");
    foav.setOwner(owner);
} catch (IOException e) {
    System.err.println(e);
}

使用 Files.setAttribute()设置文件所有者

与大多数视图一样,文件所有者视图可以访问setAttribute()方法。属性的完整名称是owner:owner,正如您在这里看到的:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.UserPrincipal;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
…
UserPrincipal owner = null;
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
try {
    owner = path.getFileSystem().getUserPrincipalLookupService().
                                 lookupPrincipalByName("apress");
    Files.setAttribute(path, "owner:owner", owner, NOFOLLOW_LINKS);
} catch (IOException e) {
    System.err.println(e);
}

使用 FileOwnerAttributeView.getOwner()获取文件所有者

在确定文件系统中对象的访问权限时,读取文件所有者是一项常见任务。getOwner()方法以UserPrincipal方法的形式返回文件的所有者,代表文件所有者的String可以通过调用UserPrincipal.getName()方法获得:

import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileOwnerAttributeView; … Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt"); FileOwnerAttributeView foav = Files.getFileAttributeView(path,                                         FileOwnerAttributeView.class); try {     String owner = foav.getOwner().getName();     System.out.println(owner); } catch (IOException e) {     System.err.println(e); }

使用 Files.getAttribute()获得文件所有者

本节的最后一个例子涉及到了Files.getAttribute()方法。我相信从上面的章节中你已经非常熟悉这个方法了,所以下面是代码片段:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.UserPrincipal;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
…
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
try {            
    UserPrincipal owner = (UserPrincipal) Files.getAttribute(path,
                                               "owner:owner", NOFOLLOW_LINKS);
    System.out.println(owner.getName());
    } catch (IOException e) {
        System.err.println(e);
    }

Image 警告如果无法获得默认文件系统的用户主体查找服务或者指定了无效的用户名,则会抛出java.nio.file.attribute.UserPrincipalNotFoundException

文件所有者属性可能需要以下名称:

  • owner

普遍接受的形式是[view-name:]attribute-name。这个view-name就是owner

POSIX 视图

Unix 爱好者的好消息!POSIX 用 Unix 及其风格支持的属性扩展了基本视图——文件所有者、组所有者和九个相关的访问权限(读、写、同一组的成员等)。).

基于PosixFileAttributes类,您可以如下提取 POSIX 属性:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFileAttributes;
…
PosixFileAttributes attr = null;
Path path = Paths.get("/home/rafaelnadal/tournaments/2009/BNP.txt");
try {
    attr = Files.readAttributes(path, PosixFileAttributes.class);
} catch (IOException e) {
    System.err.println(e);
}

System.out.println("File owner: " + attr.owner().getName());
System.out.println("File group: " + attr.group().getName());
System.out.println("File permissions: " + attr.permissions().toString());

或者您可以通过调用Files.getFileAttributeView()方法来使用“长方法”:

import java.nio.file.attribute.PosixFileAttributeView;
…
try {
    attr = Files.getFileAttributeView(path,
                  PosixFileAttributeView.class).readAttributes();
} catch (IOException e) {
    System.err.println(e);
}

POSIX 属性可能需要以下名称:

  • group
  • permissions

普遍接受的形式是[view-name:]attribute-name。这个view-name就是posix

POSIX 权限

permissions()方法返回一组PosixFilePermissions对象。PosixFilePermissions是一个权限助手类。这个类最有用的方法之一是asFileAttribute(),它接受文件权限的Set,并构造一个可以传递给Path.createFile()方法或Path.createDirectory()方法的文件属性。例如,您可以提取一个文件的 POSIX 权限,并创建另一个具有相同属性的文件,如下所示(本例使用前面例子中的attr对象):

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;
…
Path new_path = Paths.get("/home/rafaelnadal/tournaments/2009/new_BNP.txt");
FileAttribute<Set<PosixFilePermission>> posixattrs =  
                       PosixFilePermissions.asFileAttribute(attr.permissions());
try {
    Files.createFile(new_path, posixattrs);
} catch (IOException e) {
    System.err.println(e);
}

此外,您可以通过调用fromString()方法将文件的权限设置为硬编码的字符串:

Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rw-r--r--");
try {
    Files.setPosixFilePermissions(new_path, permissions);
} catch (IOException e) {
    System.err.println(e);
}

POSIX 组所有者

可以用名为group的 POSIX 属性设置文件组所有者。setGroup()方法获取文件路径和一个GroupPrincipal实例,该实例映射一个表示组所有者的字符串——该类扩展了UserPrincipal接口:

import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.GroupPrincipal; import java.nio.file.attribute.PosixFileAttributeView; … Path path = Paths.get("/home/rafaelnadal/tournaments/2009/BNP.txt"); try {     GroupPrincipal group = path.getFileSystem().                getUserPrincipalLookupService().lookupPrincipalByGroupName("apressteam");     Files.getFileAttributeView(path, PosixFileAttributeView.class).setGroup(group); } catch (IOException e) {     System.err.println(e); }

Image 注意在前面的例子中使用了一个名为“apress team”的组主体,但是这个组在您的机器上不可用。要在没有获得java.nio.file.attribute.UserPrincipalNotFoundException的情况下测试前面的代码,您需要添加您的组主体名称(您的机器的管理组或具有适当操作系统特权的组)。

您可以通过调用Files.getAttribute()方法轻松找到该组:

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
…
try {
    GroupPrincipal group = (GroupPrincipal) Files.getAttribute(path, "posix:group",
                                                                       NOFOLLOW_LINKS);
    System.out.println(group.getName());
} catch (IOException e) {
    System.err.println(e);
}

Image 注意你可以通过调用FileOwnerAttributeView.getOwner()FileOwnerAttributeView.setOwner()来获得对所有者的访问,这在 POSIX 视图中是继承的。

前交叉韧带视图

访问控制列表(ACL)是权限的集合,旨在对文件系统对象的访问实施严格的规则。在 ACL 中,控制每个对象的所有者、权限和不同种类的标志。NIO.2 通过由AclFileAttributeView接口表示的 ACL 视图提供对 ACL 的控制,这是一个文件属性视图,支持读取或更新文件的 ACL 或文件所有者属性。

使用 Files.getFileAttributeView()读取 ACL

如果您从未见过 ACL 的内容,那么尝试下面的代码,它使用Files.getFileAttributeView()提取 ACL 作为List<AclEntry>:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.AclEntry;
import java.nio.file.attribute.AclFileAttributeView;
import java.util.List;
…
List<AclEntry> acllist = null;
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");

AclFileAttributeView aclview = Files.getFileAttributeView(path, AclFileAttributeView.class);
try {
    acllist = aclview.getAcl();
} catch (IOException e) {
    System.err.println(e);
}

使用 Files.getAttribute()读取 ACL

您还可以使用getAttribute()方法来读取 ACL:

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
…
List<AclEntry> acllist = null;
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");

try {
    acllist = (List<AclEntry>) Files.getAttribute(path, "acl:acl", NOFOLLOW_LINKS);
} catch (IOException e) {
    System.err.println(e);
}

ACL 属性可能需要以下名称:

  • acl
  • owner

普遍接受的形式是[view-name:]attribute-name。这个view-name就是acl

读取 ACL 条目

前面的两个例子向您展示了如何提取指定路径的 ACL。结果是一个列表AclEntry——一个从 ACL 映射条目的类。每个条目有四个组成部分:

  • 类型:确定条目是允许还是拒绝访问。可以是ALARMALLOWAUDITDENY
  • 主体:条目授予或拒绝访问的身份。这被映射为一个UserPrincipal
  • 权限:一组权限。映射为Set<AclEntryPermission>
  • 标志:一组标志,指示条目如何被继承和传播。映射为Set<AclEntryFlag>

您可以遍历列表并提取每个条目的组件,如下所示——这是在前面几节中提取的列表:

for (AclEntry aclentry : acllist) {
       System.out.println("++++++++++++++++++++++++++++++++++++++++++++++++++++");
       System.out.println("Principal: " + aclentry.principal().getName());
       System.out.println("Type: " + aclentry.type().toString());
       System.out.println("Permissions: " + aclentry.permissions().toString());
       System.out.println("Flags: " + aclentry.flags().toString());
}

以下是此代码的输出示例(在 Windows 7 上测试):


++++++++++++++++++++++++++++++++++++++++++++++++++++

Principal: BUILTIN\Administrators

Type: ALLOW

Permissions: [WRITE_OWNER, READ_ACL, EXECUTE, WRITE_NAMED_ATTRS, READ_ATTRIBUTES,
READ_NAMED_ATTRS, WRITE_DATA, WRITE_ACL, READ_DATA, WRITE_ATTRIBUTES, SYNCHRONIZE, DELETE,
DELETE_CHILD, APPEND_DATA]

Flags: []

++++++++++++++++++++++++++++++++++++++++++++++++++++

Principal: NT AUTHORITY\SYSTEM

Type: ALLOW

Permissions: [WRITE_OWNER, READ_ACL, EXECUTE, WRITE_NAMED_ATTRS, READ_ATTRIBUTES,
READ_NAMED_ATTRS, WRITE_DATA, WRITE_ACL, READ_DATA, WRITE_ATTRIBUTES, SYNCHRONIZE, DELETE,
DELETE_CHILD, APPEND_DATA]

Flags: []

++++++++++++++++++++++++++++++++++++++++++++++++++++

Principal: NT AUTHORITY\Authenticated Users

Type: ALLOW


Permissions: [READ_ACL, EXECUTE, READ_DATA, WRITE_ATTRIBUTES, WRITE_NAMED_ATTRS,
SYNCHRONIZE, DELETE, READ_ATTRIBUTES, READ_NAMED_ATTRS, WRITE_DATA, APPEND_DATA]

Flags: []

++++++++++++++++++++++++++++++++++++++++++++++++++++

Principal: BUILTIN\Users

Type: ALLOW

Permissions: [READ_ACL, EXECUTE, READ_DATA, SYNCHRONIZE, READ_ATTRIBUTES, READ_NAMED_ATTRS]

Flags: []

在 ACL 中授予新的访问权限

ACL 条目是通过调用相关联的AclEntry.Builder对象的build()方法来创建的。例如,如果您要授予承担者新的访问权限,则必须遵循以下流程:

  1. 通过调用FileSystem.getUserPrincipalLookupService()方法来查找主体。
  2. 获取 ACL 视图(如前所述)。
  3. 使用AclEntry.Builder对象创建一个新条目。
  4. 读取 ACL(如前所述)。
  5. 插入新条目(建议放在任何DENY条目之前)。
  6. 使用setAcl()setAttribute()重写 ACL。

按照这些步骤,您可以编写一个代码片段,用于向名为apress的主体授予读数据访问权和追加数据访问权:

import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.AclEntry; import java.nio.file.attribute.AclEntryPermission; import java.nio.file.attribute.AclEntryType; import java.nio.file.attribute.AclFileAttributeView; import java.nio.file.attribute.UserPrincipal; import java.util.List; import static java.nio.file.LinkOption.NOFOLLOW_LINKS; … try {     //Lookup for the principal     UserPrincipal user = path.getFileSystem().getUserPrincipalLookupService() `                                                   .lookupPrincipalByName("apress");

//Get the ACL view
    AclFileAttributeView view = Files.getFileAttributeView(path,  
                                             AclFileAttributeView.class);

//Create a new entry
    AclEntry entry = AclEntry.newBuilder().setType(AclEntryType.ALLOW).
               setPrincipal(user).setPermissions(AclEntryPermission.READ_DATA,  
               AclEntryPermission.APPEND_DATA).build();

//read ACL
    List acl = view.getAcl();

//Insert the new entry
    acl.add(0, entry);

//rewrite ACL
    view.setAcl(acl);            
    //or, like this
    //Files.setAttribute(path, "acl:acl", acl, NOFOLLOW_LINKS);

} catch (IOException e) {
    System.err.println(e);
}`

Image 注意上例中使用的名为“apress”的主体在您的机器上不可用。要在没有获得java.nio.file.attribute.UserPrincipalNotFoundException的情况下测试代码,添加您的主体名称(您机器的管理员用户或具有适当操作系统权限的用户)。

上述代码在现有文件的 ACL 中添加了一个新条目。在一般情况下,您可能会在创建新文件时这样做。

Image 注意你可以通过调用FileOwnerAttributeView.getOwner()FileOwnerAttributeView.setOwner()来获得对所有者的访问,这两个值在 ACL 视图中是继承的。

文件存储属性

如果您将计算机视为一个文件存储容器,那么您可以轻松地识别更多类型的存储,如分区、设备、卷等等。NIO.2 可以通过FileStore抽象类获得每种存储类型的信息。对于一个特定的商店,您可以获得它的名称、类型、总空间、已用空间和可用空间。在下面的小节中,您将看到如何为默认文件系统中的所有存储以及包含指定文件的存储获取该信息。

获取所有文件存储的属性

一旦获得了对默认文件系统的访问权——通过调用FileSystems.getDefault()方法——就可以轻松地遍历由FileSystem.getFileStores()方法提供的文件存储列表。因为每个实例(名称、类型、总空间、已用空间和可用空间)都是一个FileStore对象,所以可以调用相应的专用方法,如name()type()getTotalSpace()等等。以下代码片段打印了关于您的商店的信息:

import java.io.IOException;
import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
…
FileSystem fs = FileSystems.getDefault();
for (FileStore store : fs.getFileStores()) {
  try {
        long total_space = store.getTotalSpace() / 1024;
        long used_space = (store.getTotalSpace() - store.getUnallocatedSpace()) / 1024;
        long available_space = store.getUsableSpace() / 1024;
        boolean is_read_only = store.isReadOnly();

        System.out.println("--- " + store.name() + " --- " + store.type());
        System.out.println("Total space: " + total_space);
        System.out.println("Used space: " + used_space);
        System.out.println("Available space: " + available_space);
        System.out.println("Is read only? " + is_read_only);

  } catch (IOException e) {
      System.err.println(e);
  }
}

以下是此代码的输出示例:


---  --- NTFS

Total space: 39070048

Used space: 31775684

Available space: 7294364

---  --- NTFS


Total space: 39070048

Used space: 8530348

Available space: 30539700

--- SAMSUNG DVD RECORDER VOLUME --- UDF

Total space: 2936192

Used space: 2936192

Available space: 0

Image 注意正如您在前面的例子中所看到的,如果一个商店没有名称,则返回一个空字符串。此外,返回的磁盘空间量的值以字节表示,因此您可能希望将这些数字转换为千字节、兆字节或千兆字节,以便于人们阅读。

获取文件所在的文件存储的属性

基于FileStore类,您可以获得特定文件所在的文件存储的属性。这个任务可以通过调用Files.getFileStore()方法来完成,该方法获得一个表示文件的参数(一个Path对象)。NIO.2 为您确定文件存储并提供对信息的访问。以下代码显示了一种可能的方法:

`import java.io.IOException;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
try {
    FileStore store = Files.getFileStore(path);

long total_space = store.getTotalSpace() / 1024;
   long used_space = (store.getTotalSpace() - store.getUnallocatedSpace()) / 1024;
   long available_space = store.getUsableSpace() / 1024;
   boolean is_read_only = store.isReadOnly();

System.out.println("--- " + store.name() + " --- " + store.type());
   System.out.println("Total space: " + total_space);    System.out.println("Used space: " + used_space);
   System.out.println("Available space: " + available_space);
   System.out.println("Is read only? " + is_read_only);
} catch (IOException e) {
   System.err.println(e);
}`

此代码的输出示例如下:


---  --- NTFS

Total space: 39070048

Used space: 8530348

Available space: 30539700

Is read only? false

文件存储可能支持一个或多个FileStoreAttributeView类,这些类提供一组文件存储属性的只读或可更新视图,如下所示:

FileStoreAttributeView fsav =
         store.getFileStoreAttributeView(FileStoreAttributeView.class);

Image 注意此外,您可以使用getAttribute()方法读取文件存储属性的值。

用户自定义文件属性视图

如果您发现没有足够的内置属性来满足您的需要,或者如果您有一些唯一的元数据(对文件系统有意义)要与文件关联,您可以定义自己的属性。NIO.2 通过UserDefinedFileAttributeView接口提供了用户定义的文件属性视图——扩展属性。这个工具允许您将您认为对您的用例有用的任何属性关联到一个文件。例如,如果您开发分布式文件系统,这可能很有用。例如,您可以添加一个布尔属性来验证文件是否被复制或分发到其他位置。

检查用户定义属性的可支持性

在您尝试创建自己的属性之前,请检查您的文件系统是否支持此功能。因为这是通过文件存储来检查的,而不是通过文件本身,所以首先您需要获得所需的文件存储。然后,您可以调用supportsFileAttributeView()方法,该方法使用一个String参数来表示文件属性视图的名称或视图名称为UserDefinedFileAttributeView.class。它返回一个布尔值,如下所示:

import java.io.IOException;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.UserDefinedFileAttributeView;
…
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");

try {
    FileStore store = Files.getFileStore(path);
    if (!store.supportsFileAttributeView(UserDefinedFileAttributeView.class)) {
      System.out.println("The user defined attributes are not supported on: " + store);
    } else {
      System.out.println("The user defined attributes are supported on: " + store);
    }
} catch (IOException e) {
    System.err.println(e);
}

Image 注意你可以直接从默认的文件系统中获取所有的文件存储,或者一组文件存储。不需要从文件所在的位置获取文件存储。

对用户定义属性的操作

如果您的文件系统支持用户定义的属性,那么您就可以创建自己的属性了。接下来,您将看到如何定义属性,如何列出用户定义的属性,以及如何删除用户定义的属性。本节的重点应该是用户定义属性的生命周期。

定义一个用户属性

首先,您将定义一个名为file.description的属性,其值为"This file contains private information!"。通过调用Files.getFileAttributeView()获得视图后,您可以如下编写这个用户定义的属性:

import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.UserDefinedFileAttributeView; … Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt"); UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path,                                          UserDefinedFileAttributeView.class); try {     int written = udfav.write("file.description", Charset.defaultCharset().                encode("This file contains private information!")); } catch (IOException e) {     System.err.println(e); }

write()方法将来自给定缓冲区的属性值作为一个字节序列写入。它接收两个参数:属性名和包含属性值的缓冲区。如果给定名称的属性已经存在,则替换其值。如您所见,该方法返回了一个int,它表示写入的字节数,可能为零。

Image 注意另外,你可以用setAttribute()的方法写一个属性。可以从缓冲区或者字节数组(byte[])写入。

列出用户定义的属性名称和值大小

在任何时候,您都可以通过调用UserDefinedFileAttributeView.list()方法来查看用户定义的属性名称和值大小的列表。返回的列表是表示属性名称的字符串集合。将它们的名称传递给UserDefinedFileAttributeView.size()方法将导致属性值的大小。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.UserDefinedFileAttributeView;
…
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path,
                                         UserDefinedFileAttributeView.class);

try {
    for (String name : udfav.list()) {
        System.out.println(udfav.size(name) + "     " + name);
    }
} catch (IOException e) {
    System.err.println(e);
}
获取用户定义属性的值

读取用户定义属性的值是通过使用UserDefinedFileAttributeView.read()方法完成的。您向它传递属性名和目标缓冲区,它返回指定缓冲区中的值。以下代码片段向您展示了如何做到这一点:

import java.io.IOException; `import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.UserDefinedFileAttributeView;

Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path,
                                         UserDefinedFileAttributeView.class);

try {
    int size = udfav.size("file.description");
    ByteBuffer bb = ByteBuffer.allocateDirect(size);
    udfav.read("file.description", bb);
    bb.flip();
    System.out.println(Charset.defaultCharset().decode(bb).toString());
} catch (IOException e) {
    System.err.println(e);
}`

Image 注意使用UserDefinedFileAttributeView.size()方法,您可以很容易地设置表示用户定义属性的值的缓冲区的正确大小。

Image 注意你也可以使用getAttribute()方法读取一个属性。该值作为字节数组返回(byte[])。

删除文件的自定义属性

当用户定义的属性不再有用时,您可以通过调用UserDefinedFileAttributeView.delete()方法轻松删除它。您只需要向方法提供属性的名称,它就会为您完成剩下的工作。下面显示了如何删除前面定义的属性:

import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.UserDefinedFileAttributeView; … Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt"); UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path,                                          UserDefinedFileAttributeView.class); try {     udfav.delete("file.description"); } catch (IOException e) {     System.err.println(e); }

总结

在本章中,您已经探索了 NIO.2 提供的视图。您看到了如何操作各种属性,如何针对不同的目的查询文件或文件存储,以及如何定义自己的元数据。

在介绍了 NIO.2 视图以及如何确定特定文件系统支持哪些视图之后,本章介绍了基本和 DOS 属性,这些属性应该适用于每个文件。这些属性提供了主要的元数据,比如大小、创建时间、最后修改时间、只读等等。本章接下来介绍了文件所有者属性,它为设置和获取文件所有者提供了支持,接着介绍了 Unix 用户的 POSIX 属性和 ACL 属性,它们提供了对权限集合的访问,这些权限控制着对文件系统对象的访问。本章最后讨论了文件存储属性和用户定义的属性。

三、管理符号链接和硬链接

Linux 和 Unix 用户(尤其是管理员)应该熟悉链接的概念。有两种类型的链接:符号链接和硬链接。链接通常通过几个名称到达一个文件,而不是从根目录通过一系列目录和子目录进行导航——可以将链接想象为一个映射文件/目录路径的实体,并通过一组名称进行标识。如果你是一个 Windows 的忠实用户,你可能不熟悉链接,尽管 Windows 本身完全知道它们,尤其是符号链接,它很像 Windows 的快捷方式。

NIO.2 同时支持硬链接和符号链接。Path类的每个方法都知道如何检测一个链接,如果没有指定行为的配置,它们将以默认的方式运行。在本章中,你将学习如何通过java.nio.file API 操作链接,包括如何创建一个链接以及如何找到一个链接的目标。大多数操作都是通过java.nio.file.Files类实现的,该类提供了createLink()createSymbolicLink()isSymbolicLink()readSymbolicLink()等方法。本章将详细介绍每一种方法。

介绍链接

当您通过一组名称(从命令行、应用或其他方式)访问一个文件时,您正在处理一个链接。链接可以设置为硬链接(有时拼写为 hardlink )或符号链接(也称为 symlinksoftlink )。当一个文件有两个相同权重的名称和 inode 表时(Linux 文件实际上不存在于目录中;它们被分配一个索引节点号,Linux 用它来定位它们)直接指向磁盘上包含数据的块,该链接是一个硬链接。把硬链接想象成指向文件的目录引用或指针。当一个文件有一个主名称,并且在文件名表中有一个额外的条目将任何访问引用回主名称时,该链接就是一个符号链接。符号链接比硬链接更灵活,使用频率也更高。以下是这两种类型的链接之间的主要差异/相似之处:

  • 只能为文件创建硬链接,不能为目录创建。符号链接可以链接到文件或目录。
  • 硬链接不能跨文件系统存在。符号链接可以跨文件系统存在。
  • 硬链接的目标必须存在。符号链接的目标可能不存在。
  • 移除你的硬链接所指向的原始文件并不会移除硬链接本身,硬链接仍然提供底层文件的内容。移除符号链接指向的原始文件不会移除附加的符号链接,但是如果没有原始文件,符号链接就没有用。
  • 如果删除硬链接或符号链接本身,原始文件将保持不变。
  • 硬链接是与原始文件相同的实体。所有属性都相同。符号链接没有这么大的限制。
  • 硬链接的外观和行为都像普通文件,因此很难找到硬链接。符号链接的目标甚至可能不存在,因此它非常灵活。

从命令行创建链接

Windows 用户可以使用mklink命令从命令行创建符号链接和硬链接。该命令根据您需要创建的链接类型获得一组选项。其中一些选项如下:

/D      Creates a directory symbolic link.  Default is a file symbolic link.
/H      Creates a hard link instead of a symbolic link.
/J      Creates a Directory Junction.
Link    specifies the new symbolic link name.
Target  specifies the path (relative or absolute) that the new link refers to.

例如,如果您想让文件夹C:\rafaelnadal\photos也可以从C:\rafaelnadal中获得,您可以使用以下命令:

mklink /D C:\rafaelnadal C:\rafaelnadal\photos

现在,如果您查看C:\rafaelnadal目录,您还会看到C:\rafaelnadal\photos目录中的所有文件。

Unix (Linux)用户可以使用名为ln的命令来实现与前面的 Windows 示例相同的效果(注意,在本例中,目标文件首先列出,链接名称其次列出):

ln –s /home/rafaelnadal/photos /home/rafaelnadal

此外,在 Unix (Linux)中,您可以使用rm命令删除链接:

rm /home/rafaelnadal

创建符号链接

在 NIO.2 中创建符号链接非常容易。您只需调用Files.createSymbolicLink()方法,该方法使用符号链接的路径来创建、符号链接的目标以及在创建符号链接时自动设置的属性数组。它返回符号链接的路径。

如果你的文件系统不支持符号链接,那么会抛出一个UnsupportedOperationException异常。此外,请记住,符号链接的目标可以是绝对的,也可以是相对的(如第一章中所述),可能存在也可能不存在。

下面的代码片段用默认属性创建了一个简单的符号链接。它为文件C:\rafaelnadal\photos\rafa_winner.jpg创建一个名为rafael.nadal.1的符号链接(建议该文件存在,并且文件系统必须有创建符号链接的权限)。

…
Path link = FileSystems.getDefault().getPath("rafael.nadal.1");
Path target= FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_winner.jpg");

try {
    Files.createSymbolicLink(link, target);
    } catch (IOException | UnsupportedOperationException | SecurityException e) {
      if (e instanceof SecurityException) {
          System.err.println("Permission denied!");
      }
      if (e instanceof UnsupportedOperationException) {
          System.err.println("An unsupported operation was detected!");
      }
      if (e instanceof IOException) {
          System.err.println("An I/O error occurred!");
      }
System.err.println(e);
}

当你想修改链接的默认属性时,你可以使用createSymbolicLink()方法的第三个参数。这个参数是一个类型为FileAttribute的属性数组,这个类封装了一个文件属性的值,可以在创建新文件、目录或链接时自动设置这个值。下面的代码片段读取目标文件的属性并创建一个链接,将目标文件的属性分配给链接。它为文件C:\rafaelnadal\photos\rafa_winner.jpg创建一个名为rafael.nadal.2的符号链接(该文件必须存在,并且文件系统必须有创建符号链接的权限)。

…
Path link = FileSystems.getDefault().getPath("rafael.nadal.2");
Path target = FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_winner.jpg");

try {
    PosixFileAttributes attrs = Files.readAttributes(target, PosixFileAttributes.class);
    FileAttribute<Set<PosixFilePermission>> attr =
                               PosixFilePermissions.asFileAttribute(attrs.permissions());

    Files.createSymbolicLink(link, target, attr);
    } catch (IOException | UnsupportedOperationException | SecurityException e) {
      if (e instanceof SecurityException) {
          System.err.println("Permission denied!");
      }
      if (e instanceof UnsupportedOperationException) {
          System.err.println("An unsupported operation was detected!");
      }
      if (e instanceof IOException) {
          System.err.println("An I/O error occured!");
      }
    System.err.println(e);
}

此外,您可以使用setAttribute()方法修改创建后的链接属性。例如,下面的代码片段读取目标的lastModifiedTimelastAccessTime属性,并将它们设置为链接。它为文件C:\rafaelnadal\photos\rafa_winner.jpg创建一个名为rafael.nadal.3的符号链接(该文件必须存在,并且文件系统必须有创建符号链接的权限)。

…
Path link = FileSystems.getDefault().getPath("rafael.nadal.3");
Path target = FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_winner.jpg");

try {
    Files.createSymbolicLink(link, target);

    FileTime lm = (FileTime) Files.getAttribute(target,
                                                 "basic:lastModifiedTime", NOFOLLOW_LINKS);
    FileTime la = (FileTime) Files.getAttribute(target,
                                                   "basic:lastAccessTime", NOFOLLOW_LINKS);
    Files.setAttribute(link, "basic:lastModifiedTime", lm, NOFOLLOW_LINKS);
    Files.setAttribute(link, "basic:lastAccessTime", la, NOFOLLOW_LINKS);
    } catch (IOException | UnsupportedOperationException | SecurityException e) {
      if (e instanceof SecurityException) {
          System.err.println("Permision denied!");
      }
      if (e instanceof UnsupportedOperationException) {
          System.err.println("An unsupported operation was detected!");
      }
      if (e instanceof IOException) {
          System.err.println("An I/O error occured!");
      }
    System.err.println(e);
}

Image 注意如果符号链接已经存在,那么会抛出一个FileAlreadyExistsException异常。

创建硬链接

您可以通过调用createLink()方法来创建一个硬链接,该方法使用链接来创建一个现有文件的路径。它返回链接的路径,表示新的目录条目。然后,您可以使用该链接作为路径来访问该文件。

如果你的文件系统不支持硬链接,那么会抛出一个UnsupportedOperationException异常。此外,请记住,只能为现有文件创建硬链接。

以下代码片段为文件C:\rafaelnadal\photos\rafa_winner.jpg创建一个名为rafael.nadal.4的硬链接(该文件必须存在,并且文件系统必须具有创建硬链接的权限):

import java.io.IOException; `import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;

public class Main {

public static void main(String[] args) {

Path link = FileSystems.getDefault().getPath("rafael.nadal.4");
  Path target = FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_winner.jpg");

try {
      Files.createLink(link, target);
            System.out.println("The link was successfully created!");
      } catch (IOException | UnsupportedOperationException | SecurityException e) {
        if (e instanceof SecurityException) {
            System.err.println("Permission denied!");
        }
        if (e instanceof UnsupportedOperationException) {
            System.err.println("An unsupported operation was detected!");
        }
        if (e instanceof IOException) {
            System.err.println("An I/O error occured!");
        }
        System.err.println(e);
  }
 }
}`

Image 注意如果硬链接已经存在,那么会抛出FileAlreadyExistsException异常。

检查符号链接

不同的Path实例可以指向文件或链接,所以你可以通过调用Files.isSymbolicLink()方法来检测一个Path实例是否指向一个符号链接。它接收一个参数,代表要测试的Path,并返回一个布尔值。下面的代码片段是测试符号链接的Path的一个简单例子。它为文件C:\rafaelnadal\photos\rafa_winner.jpg创建一个名为rafael.nadal.5的符号链接(该文件必须存在,并且文件系统必须有创建符号链接的权限)。

`…
Path link = FileSystems.getDefault().getPath("rafael.nadal.5");
Path target = FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_winner.jpg");

try {
    Files.createSymbolicLink(link, target);
    } catch (IOException | UnsupportedOperationException | SecurityException e) {     …
}

//check if a path is a symbolic link - solution 1
boolean link_isSymbolicLink_1 = Files.isSymbolicLink(link);
boolean target_isSymbolicLink_1 = Files.isSymbolicLink(target);

System.out.println(link.toString() + " is a symbolic link ? " + link_isSymbolicLink_1);
System.out.println(target.toString() + " is a symbolic link ? " + target_isSymbolicLink_1);
…`

该代码输出以下结果:


rafael.nadal.5 is a symbolic link ? true

C:\rafaelnadal\photos\rafa_winner.jpg is a symbolic link ? false

正如你在第二章中读到的,你可以通过使用属性视图来测试符号链接的Path。基本视图提供了一个名为isSymbolicLink的属性,如果指定的Path定位到一个符号链接的文件,它将返回true。您可以通过readAttributes()方法查看isSymbolicLink属性(在这种情况下不推荐,因为它会返回大量的属性列表),或者更容易地通过getAttribute()方法查看,方法如下:

…
try {
    boolean link_isSymbolicLink_2 = (boolean) Files.getAttribute(link,
                                                                "basic:isSymbolicLink");
    boolean target_isSymbolicLink_2 = (boolean) Files.getAttribute(target,
                                                                "basic:isSymbolicLink");

    System.out.println(link.toString() + " is a symbolic link ? " + link_isSymbolicLink_2);
    System.out.println(target.toString() + " is a symbolic link ? "+ target_isSymbolicLink_2);
    } catch (IOException | UnsupportedOperationException e) {
      System.err.println(e);
}
…

同样,输出是


rafael.nadal.5 is a symbolic link ? true

C:\rafaelnadal\photos\rafa_winner.jpg is a symbolic link ? false

定位链接的目标

从一个链接开始,您可以通过调用readSymbolicLink()方法来定位它的目标(可能不存在)。这个方法从用户那里接收链接,作为一个Path,并返回一个代表链接目标的Path对象。如果传递的路径不是链接,那么将抛出一个NotLinkException异常。下面的代码片段使用这个方法为文件C:\rafaelnadal\photos\rafa_winner.jpg创建一个名为rafael.nadal.6的符号链接(该文件必须存在,并且文件系统必须具有创建符号链接的权限):

…
Path link = FileSystems.getDefault().getPath("rafael.nadal.6");
Path target = FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_winner.jpg");…
…
try {
    Path linkedpath = Files.readSymbolicLink(link);
    System.out.println(linkedpath.toString());
} catch (IOException e) {
    System.err.println(e);
}

检查链接和目标是否指向同一个文件

有时您可能需要检查链接和目标是否指向同一个文件(位置)。您可以通过不同的方式获得这些信息,但是一个简单的解决方案是使用Files.isSameFile()方法。该方法(从用户处)接收两个要比较的Path,并返回一个布尔值。下面的代码片段创建了一个目标和该目标的符号链接,然后应用了isSameFile()方法。它为文件C:\rafaelnadal\photos\rafa_winner.jpg创建一个名为rafael.nadal.7的符号链接(该文件必须存在,并且文件系统必须有创建符号链接的权限)。

…
Path link = FileSystems.getDefault().getPath("rafael.nadal.7");
Path target = FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_winner.jpg");

try {
    Files.createSymbolicLink(link, target);
    } catch (IOException | UnsupportedOperationException | SecurityException e) {
     …
}

try {
    Path linkedpath = Files.readSymbolicLink(link);
    System.out.println(linkedpath.toString());
    } catch (IOException e) {
      System.err.println(e);
}

输出如下:


rafael.nadal.7 and C:\rafaelnadal\photos\rafa_winner.jpg point to the same location

总结

在本章中,您看到了 NIO.2 如何处理符号链接和硬链接。在简要概述了这两个概念以及如何在 Windows 和 Unix (Linux)中创建它们的一些简短示例之后,您看到了 NIO.2 方法。您学习了如何直接从 Java 创建符号链接和硬链接,如何检查路径是否是链接,如何检测链接的目标,以及如何检查链接和目标是否指向同一个文件。

四、文件和目录

现在您已经知道了如何使用Path类指向文件或目录,您已经准备好学习如何完成管理文件和目录的最常见任务,比如创建、读取、写入、移动、删除等等。NIO.2 附带了一组全新的方法来完成这些任务,其中大部分都可以在java.nio.file.Files类中找到。

本章首先探索一些专门检查Path是否可读、可写、可执行、常规或隐藏的方法。这些检查使您能够在应用写或读之类的操作之前,确定正在处理的文件或目录的类型。然后这一章将重点放在目录操作上,向您展示如何列出、创建和读取目录。您将看到如何列出文件系统的根目录,使用诸如createDirectory()createTempDirectory()的方法创建目录,编写目录过滤器,以及使用newDirectoryStream()方法列出目录的内容。熟悉目录操作后,您将探索文件操作,如读、写、创建和打开文件。正如您将看到的,有许多文件 I/O 方法可供选择。在本章中,您将看到缓冲和非缓冲流的工作方法,将通道方法的讨论留到下一章,在下一章中,您将看到 NIO 的真正威力。本章以众所周知的删除、复制和移动操作结束。

这些任务中的每一个都有详细的介绍,正如您将看到的,许多方面都是从以前的 Java 6“重新设计”的,但是您也将认识到许多来自java.io.File类的介绍方法。

文件和目录的检查方法

Files类提供了一组is方法,您可以使用这些方法在实际操作文件或目录之前执行各种检查。其中一些方法在前面的章节中已经介绍过了,其余的在这里介绍。建议利用这些方法,因为它们非常有助于帮助您避免应用中的异常或其他奇怪行为。例如,在尝试将文件移动到另一个位置之前,最好先检查该文件是否存在。同样,在尝试读取文件之前,检查文件是否可以读取也是一个好主意。这些检查中的一些也可以通过元数据属性来执行,正如你在第二章中看到的。

*#### 检查文件或目录是否存在

正如您在前面章节中所知道的,即使映射的文件或目录实际上并不存在,Path实例也是完全有效的。此外,语法Path方法在这种情况下可以成功应用,因为它们不操作文件或目录本身。但是在某些时候,知道一个文件或目录是否存在是非常重要的,这就是为什么Files类为这种类型的检查提供了以下两种方法:

  • exists():检查文件是否存在
  • notExists():检查文件是否不存在

这两种方法都接收两个参数,表示要测试的文件的路径和指示如何处理符号链接的选项。如果文件存在,exists()方法返回true,否则返回false(文件不存在或无法执行检查)。

以下代码片段检查文件AEGON.txt是否存在于C:\rafaelnadal\tournaments\2009目录中(在我们假设的目录结构中,该文件存在):

Path path = FileSystems.getDefault().getPath("C:/rafaelnadal/tournaments/2009","AEGON.txt");
…
boolean path_exists = Files.exists(path, new LinkOption[]{LinkOption.NOFOLLOW_LINKS});

如果您只需要在文件不存在的情况下采取行动,那么调用notExists()方法,如果文件不存在,该方法返回true,否则返回false(文件存在或者无法执行检查):

Path path = FileSystems.getDefault().getPath("C:/rafaelnadal/tournaments/2009",
"AEGON.txt");
…
boolean path_notexists = Files.notExists(path, new LinkOption[]{LinkOption.NOFOLLOW_LINKS});

Image 注意如果两种方法都应用于同一个Path,并且都返回false,则无法进行检查。例如,如果应用不能访问文件,那么状态是未知的,两种方法都返回false。从这里,很容易得出结论,文件/目录的存在状态可以是:存在、不存在或未知。在检查该状态后,结果立即过期,因为存在的文件可以在检查后立即删除,因此结果必须立即“过期”。如果此方法指示文件存在,则不能保证后续访问会成功。此外,如果这些方法之一没有读取文件的权限,可能会抛出一个SecurityException

Image 注意 !Files.exists(…)不等同于Files.notExists(…)notExists()方法不是exists()方法的补充。

检查文件可访问性

在访问文件之前,另一个好的做法是使用isReadable()isWritable()isExecutable()方法检查其可访问性级别。在您传递了要验证的Path之后,这些方法将分别检查它是否是可读的Path(文件存在,JVM 有权限打开它进行读取)、可写的Path(文件存在,JVM 有权限打开它进行写入)、可执行的Path(文件存在,JVM 有权限执行它)。

此外,您可以通过调用isRegularFile()方法来检查Path是否指向常规文件。常规文件是没有特殊特征的文件(它们不是符号链接、目录等。)并包含真实数据,如文本或二进制文件。isReadable()isWritable()isExecutable()isRegularFile()都返回布尔值:true如果文件存在并且可读、可写、可执行和正常,或者false如果文件不存在,读取、写入、执行和正常访问将被拒绝,因为 JVM 没有足够的权限,或者无法确定访问。

将这些方法放入检查C:\rafaelnadal\tournaments\2009目录中的AEGON.txt文件的可访问性的代码片段中(该文件必须存在),如下所示:

Path path = FileSystems.getDefault().getPath("C:/rafaelnadal/tournaments/2009","AEGON.txt");

boolean is_readable = Files.isReadable(path);
boolean is_writable = Files.isWritable(path);
boolean is_executable = Files.isExecutable(path);
boolean is_regular = Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS);

if ((is_readable) && (is_writable) && (is_executable) && (is_regular)) {
     System.out.println("The checked file is accessible!");
} else {
     System.out.println("The checked file is not accessible!");
}

或者,您可以使用这个较短的版本:

boolean is_accessible = Files.isRegularFile(path) & Files.isReadable(path) &  
                        Files.isExecutable(path) & Files.isWritable(path);
if (is_accessible) {
    System.out.println("The checked file is accessible!");
} else {
    System.out.println("The checked file is not accessible!");
}

Image 注意前面的例子通过对一个Path应用所有四种方法来检查可访问性,但是你可以根据你需要获得的可访问性级别以不同的方式组合这四种方法。例如,您可能不关心Path是否可写,在这种情况下,您可以排除这个检查。

Image 注意即使这些方法确认了可访问性,也不能保证文件可以被访问。原因在于一个众所周知的软件错误,名为“检查时间到使用时间”(TOCTTOU,发音为“TOCK too”),这意味着在检查和使用检查结果之间的时间内,系统可能会发生不同类型的变化。Unix 爱好者可能熟悉这个概念,但是它也适用于任何其他系统。

检查两条路径是否指向同一个文件

在前一章中,你看到了如何检查一个符号链接和一个目标是否指向同一个文件。使用isSameFile()方法可以执行的另一个常见测试是检查两个以不同方式表达的Path是否指向同一个文件。例如,一个相对的Path和一个绝对的Path可能指向同一个文件,即使这不是很明显。调用isSameFile()方法将在下面的代码片段中揭示这一点,它以三种不同的方式表达了到MutuaMadridOpen.txt文件的路径(该文件必须存在于C:\rafaelnadal\tournaments\2009 directory中):

Path path_1 = FileSystems.getDefault().getPath("C:/rafaelnadal/tournaments/2009",
                                                                     "MutuaMadridOpen.txt");
Path path_2 = FileSystems.getDefault().getPath("/rafaelnadal/tournaments/2009",
                                                                     "MutuaMadridOpen.txt");
Path path_3 = FileSystems.getDefault().getPath("/rafaelnadal/tournaments/dummy/../2009",
                                                                     "MutuaMadridOpen.txt");
try {
    boolean is_same_file_12 = Files.isSameFile(path_1, path_2);
    boolean is_same_file_13 = Files.isSameFile(path_1, path_3);
    boolean is_same_file_23 = Files.isSameFile(path_2, path_3);

    System.out.println("is same file 1&2 ? " + is_same_file_12);
    System.out.println("is same file 1&3 ? " + is_same_file_13);
    System.out.println("is same file 2&3 ? " + is_same_file_23);
} catch (IOException e) {
    System.err.println(e);
}

输出如下所示:


is same file 1&2 ? true

is same file 1&3 ? true

is same file 2&3 ? true

检查文件可见性

如果你需要找出一个文件是否被隐藏,你可以调用Files.isHidden()方法。记住“隐藏”的概念是依赖于平台/提供商的,你只需要通过Path检查并得到truefalse响应。下面的代码片段检查MutuaMadridOpen.txt文件是否是隐藏文件(该文件必须存在于C:\rafaelnadal\tournaments\2009目录中):

Path path = FileSystems.getDefault().getPath("C:/rafaelnadal/tournaments/2009",
                                                                       "MutuaMadridOpen.txt");
…
try {
    boolean is_hidden = Files.isHidden(path);
    System.out.println("Is hidden ? " + is_hidden);
} catch (IOException e) {
    System.err.println(e);
}

创建和读取目录

谈到创建和读取目录,NIO.2 在Files类中提供了一组专用方法。在本节中,您将了解如何列出文件系统根目录、创建目录(包括临时目录)、列出目录的内容,以及编写和使用目录过滤器。

列出文件系统根目录

在 Java 6 中,文件系统根目录被提取为一组File对象。从 Java 7 开始,NIO.2 将文件系统根目录作为Path对象的Iterable来获取。这个IterablegetRootDirectories()方法返回,如下所示:

Iterable<Path> dirs = FileSystems.getDefault().getRootDirectories();
for (Path name : dirs) {
     System.out.println(name);
}

可能的输出如下:


C:\

D:\

E:\

你可以很容易地从Iterable进入一个数组,如下所示:

Iterable<Path> dirs = FileSystems.getDefault().getRootDirectories(); ArrayList<Path> list = new ArrayList<Path>(); for (Path name : dirs) {      // System.out.println(name); `     list.add(name);
}
Path[] arr = new Path[list.size()];
list.toArray(arr);

for(Path path : arr) {
    System.out.println(path);
}`

如果您需要将文件系统根目录提取为一个数组File,请使用 Java 6 解决方案:

File[] roots = File.listRoots();
for (File root : roots) {
     System.out.println(root);
}

创建新目录

创建一个新目录是一个常见的任务,可以通过调用Files.createDirectory()方法来完成。这个方法获取要创建的目录(Path)和一个可选的文件属性列表(FileAttribute<?>),以便在创建时自动设置。它返回创建的目录。下面的代码片段使用默认属性在C:\rafaelnadal\tournaments目录下创建一个名为\2010的新目录(该目录必须不存在):

Path newdir = FileSystems.getDefault().getPath("C:/rafaelnadal/tournaments/2010/");
…
try {
    Files.createDirectory(newdir);
} catch (IOException e) {
    System.err.println(e);
}

您可以在创建时添加一组属性,如下面的示例代码片段所示,它在具有特定权限的 POSIX 文件系统上创建一个新目录:

Path newdir = FileSystems.getDefault().getPath("/home/rafaelnadal/tournaments/2010/");
…
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwxr-x---");
FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms);
try {
    Files.createDirectory(newdir, attr);
} catch (IOException e) {
       System.err.println(e);
}

Image 注意如果目录存在,那么createDirectory()方法会抛出异常。

有时你需要创建不止一个目录。例如,您可能需要创建一系列分层目录,如\statistics\win\prizes。您可以调用一系列createDirectory()方法,或者更优雅地使用Files.createDirectories()方法,这将在一次调用中创建目录序列;根据需要,从上到下创建目录,将\statistics作为相对根,\prizes作为最后一片叶子。目录序列作为一个Path实例传递,在创建目录时可以自动设置文件属性列表,也可以不设置。以下代码片段显示了如何在C:\rafaelnadal目录下创建一系列分层目录:

Path newdir= FileSystems.getDefault().getPath("C:/rafaelnadal/", "statistics/win/prizes");
…
try {
    Files.createDirectories(newdir);
} catch (IOException e) {
    System.err.println(e);
}

Image 注意如果目录序列中已经存在一个或多个目录,那么createDirectories()方法不会抛出异常,而只是“跳转”该目录并进入下一个目录。这种方法在创建一些目录后可能会失败,但不是全部。

列出目录的内容

使用目录和文件通常涉及为了不同的目的循环目录的内容。NIO.2 通过一个名为DirectoryStream的可迭代流提供了这个工具,这是一个实现Iterable的接口。通过Files.newDirectoryStream()方法可以直接访问目录流,该方法获取目录的Path并返回一个新的打开的目录流。

列举全部内容

以下代码片段将以链接、文件、子目录和隐藏文件的形式返回目录的全部内容(列出的目录是C:\rafaelnadal\tournaments\2009):

Path path = Paths.get("C:/rafaelnadal/tournaments/2009");

//no filter applied
System.out.println("\nNo filter applied:");
try (DirectoryStream<Path> ds = Files.newDirectoryStream(path)) {
     for (Path file : ds) {
          System.out.println(file.getFileName());
     }
}catch(IOException e) {
   System.err.println(e);
}

一个可能的输出如下(这是C:\rafaelnadal\tournaments\2009 directory的全部内容):


No filter applied:

AEGON.txt

BNP.txt

MutuaMadridOpen.txt

supershot.bmp

Tickets.zip

TournamentsCalendar.xls

Videos

…

通过应用 Glob 模式列出内容

有时,您可能需要仅列出符合特定标准的内容,这需要对目录内容应用过滤器。通常,您只需要提取名称与特定模式匹配的文件和子目录。NIO.2 将这种特殊模式定义为内置的 glob 过滤器。根据 NIO.2 文档,glob 模式只是一个与其他字符串匹配的字符串——在本例中是目录和文件名。因为这是一种模式,所以它必须遵守一些规则,如下所示:

  • *:表示(匹配)任意数量的字符,包括无。
  • **:类似于*,但是跨越了目录的界限。
  • ?:精确表示(匹配)一个字符。
  • {}:表示由逗号分隔的子模式的集合。例如,{A,B,C}匹配 A、B 或 c。
  • []:传达一组单个字符或一系列字符(如果有连字符的话)。一些常见的例子包括:
    • [0-9]:匹配任意数字
    • [A-Z]:匹配任何大写字母
    • [a-z,A-Z]:匹配任何大写或小写字母
    • [12345]:匹配 1、2、3、4 或 5 中的任意一个
  • 方括号内,*?\匹配自己。
  • 其他所有字符都匹配自己。
  • 为了匹配*?或其他特殊字符,您可以使用反斜杠字符\对它们进行转义。例如,\\匹配单个反斜杠,\?匹配问号。

既然您已经知道了如何构建 glob 模式,那么是时候引入newDirectoryStream()方法了,该方法将Path获取到目录并应用 glob 过滤器。以下示例将从C:\rafaelnadal\tournaments\2009目录中提取所有 PNG、JPG 和 BMP 类型的文件(不管它们的名称如何):

Path path = Paths.get("C:/rafaelnadal/tournaments/2009");
…
//glob pattern applied
System.out.println("\nGlob pattern applied:");
try (DirectoryStream<Path> ds = Files.newDirectoryStream(path, "*.{png,jpg,bmp}")) {
     for (Path file : ds) {
          System.out.println(file.getFileName());
     }
} catch (IOException e) {
    System.err.println(e);
}

输出如下所示:


Glob pattern applied:

supershot.bmp

通过应用用户定义的过滤器来列出内容

如果 glob 模式不能满足您的需求,那么是时候编写您自己的过滤器了。这是一个简单的任务,需要实现DirectoryStream.Filter<T>接口,它只有一个名为accept()的方法。根据您的实现,接受或拒绝一个Path。例如,以下代码片段只接受最终结果中的目录:

Path path = Paths.get("C:/rafaelnadal/tournaments/2009");
…
//user-defined filter - only directories are accepted
DirectoryStream.Filter<Path> dir_filter = new DirectoryStream.Filter<Path>() {

public boolean accept(Path path) throws IOException {
      return (Files.isDirectory(path, NOFOLLOW_LINKS));
  }
};

接下来,创建的过滤器作为参数传递给newDirectoryStream()方法:

System.out.println("\nUser defined filter applied:"); try (DirectoryStream<Path> ds = Files.newDirectoryStream(path, dir_filter)) { for (Path file : ds) {       System.out.println(file.getFileName());      } } catch (IOException e) {     System.err.println(e); }

输出如下所示:


User defined filter applied:

videos

下表列出了一组常用的过滤器:

  • 仅接受大于 200KB 的文件/目录的过滤器:`DirectoryStream.Filter size_filter = new DirectoryStream.Filter() {

    public boolean accept(Path path) throws IOException {
         return (Files.size(path) > 204800L);
      }
    };`

  • 仅接受当天修改的文件的过滤器:`DirectoryStream.Filter time_filter = new DirectoryStream.Filter() {

    public boolean accept(Path path) throws IOException {
         long currentTime = FileTime.fromMillis(System.currentTimeMillis()).to(TimeUnit.DAYS);
         long modifiedTime = ((FileTime) Files.getAttribute(path, "basic:lastModifiedTime",
                                                           NOFOLLOW_LINKS)).to(TimeUnit.DAYS);
         if (currentTime == modifiedTime) {
                 return true;
            }

    return false;
      }
    };`

  • 仅接受隐藏文件/目录的过滤器:`DirectoryStream.Filter hidden_filter = new DirectoryStream.Filter() {

    public boolean accept(Path path) throws IOException {
         return (Files.isHidden(path));
      }
    };`

创建、读取和写入文件

对文件最常见的操作可能包括创建、读取和/或写入操作。NIO.2 附带了许多专用方法,用于以不同的复杂性和性能级别执行这些操作,从常用的小文件方法(将所有字节读入字节数组很方便的情况)到高级功能的方法,如文件锁定和内存映射 I/O。本节从小文件的方法开始,以缓冲和非缓冲流的方法结束。

一个代表一个输入源或一个输出目的地(它可以是从磁盘文件到内存数组的任何东西)。流支持不同种类的数据,如字符串、字节、原始数据类型、本地化字符和对象。在无缓冲流中,每个读或写请求都由底层操作系统直接处理,而在缓冲流中,数据是从称为缓冲区的内存区域读取的;并且只有当缓冲区为空时才调用本地输入 API。类似地,缓冲的输出流将数据写入缓冲区,并且仅当缓冲区已满时才调用本机输出 API。当一个缓冲区没有等待它被填满就被写出来时,我们说这个缓冲区被刷新

使用标准开放选项

从 NIO.2 开始,专用于创建、读取和写入动作(或任何其他涉及打开文件的动作)的方法支持可选参数OpenOption,该参数配置如何打开或创建文件。实际上,OpenOption是来自java.nio.file包的一个接口,它有两个实现:LinkOption类(还记得众所周知的NOFOLLOW_LINKS枚举常量)和StandardOpenOption类,后者定义了以下枚举:

Image

Image

在您看了创建一个新文件之后,这些常量中的一些将在接下来的部分中显示。

创建新文件

创建新文件是一个常见的任务,可以通过调用Files.createFile()方法来完成。这个方法获取要创建的文件(Path)和一个可选的文件属性列表(FileAttribute<?>),以便在创建时自动设置。它返回创建的文件。下面的代码片段在C:\rafaelnadal\tournaments\2010目录(该目录必须存在)中创建一个名为SonyEricssonOpen.txt的新文件,该文件具有默认属性(最初,该文件必须不存在;否则将抛出FileAlreadyExistsException异常):

Path newfile = FileSystems.getDefault().
                           getPath("C:/rafaelnadal/tournaments/2010/SonyEricssonOpen.txt");
…
try {
    Files.createFile(newfile);
} catch (IOException e) {
    System.err.println(e);
}

您可以在创建时添加一组属性,如下面的代码片段所示。这段代码在具有特定权限的 POSIX 文件系统上创建一个新文件。

Path newfile = FileSystems.getDefault().
               getPath("/home/rafaelnadal/tournaments/2010/SonyEricssonOpen.txt");

Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------");
FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms);
try {
    Files.createFile(newfile, attr);
} catch (IOException e) {
    System.err.println(e);
}

您很快就会看到,这不是创建新文件的唯一方法。

编写小文件

NIO.2 为编写小型二进制/文本文件提供了一个优雅的解决方案。该功能通过两种Files.write()方法提供。这两种方法都打开文件进行写入(如果文件不存在,这可能涉及到创建文件),或者最初将现有的常规文件截断为 0 字节大小。在所有字节或行都被写入后,该方法关闭文件(即使发生 I/O 错误或异常,它也会关闭文件)。简而言之,这个方法就像CREATETRUNCATE_EXISTINGWRITE选项存在一样——当然,当没有指定其他选项时,这是默认适用的。

使用 write()方法写入字节

将字节写入文件可以通过Files.write()方法完成。此方法获取文件的路径、包含要写入的字节的字节数组以及指定如何打开文件的选项。它返回写入文件的路径。

下面的代码片段用默认的打开选项(文件名为ball.png,将被写入C:\rafaelnadal\photos目录)写一个字节数组(代表一个小网球图片):

Path ball_path = Paths.get("C:/rafaelnadal/photos", "ball.png");
…
byte[] ball_bytes = new byte[]{
(byte)0x89,(byte)0x50,(byte)0x4e,(byte)0x47,(byte)0x0d,(byte)0x0a,(byte)0x1a,(byte)0x0a,
(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x0d,(byte)0x49,(byte)0x48,(byte)0x44,(byte)0x52,
(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x10,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x10,
(byte)0x08,(byte)0x02,(byte)0x00,            
…
(byte)0x49,(byte)0x45,(byte)0x4e,(byte)0x44,(byte)0xae,(byte)0x42,(byte)0x60,(byte)0x82
};

try {
    Files.write(ball_path, ball_bytes);
} catch (IOException e) {
    System.err.println(e);
}

现在,如果你检查相应的路径,你会发现一个代表网球的小图片。

而且,如果你需要写文本(String)并且你想使用这个方法,那么把文本转换成一个字节数组如下(文件名是wiki.txt,创建于C:\rafaelnadal\wiki):

Path rf_wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt");
…
String rf_wiki = "Rafael \"Rafa\" Nadal Parera (born 3 June 1986) is a Spanish professional
tennis " + "player and a former World No. 1\. As of 29 August 2011 (2011 -08-29)[update], he is
ranked No. 2 " + "by the Association of Tennis Professionals (ATP). He is widely regarded as
one of the greatest players " + "of all time; his success on clay has earned him the nickname
\"The King of Clay\", and has prompted " + "many experts to regard him as the greatest clay
court player of all time. Some of his best wins are:";

try {
    byte[] rf_wiki_byte = rf_wiki.getBytes("UTF-8");
    Files.write(rf_wiki_path, rf_wiki_byte);
} catch (IOException e) {
    System.err.println(e);
}

即使这可行,使用下面描述的write()方法将文本写入文件也容易得多。

用 write()方法书写线条

将行写入文件可以通过使用Files.write()方法来完成(一个“行”是一个字符序列)。在每一行之后,这个方法追加平台的行分隔符(line.separator系统属性)。该方法获取文件的路径、char 序列上的 iterable 对象、用于编码的字符集以及指定文件打开方式的选项。它返回写入文件的路径。

下面的代码片段将一些行写入一个文件(实际上,它将一些行附加到前面部分创建的文件wiki.txt的末尾):

Path rf_wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt");
…
Charset charset = Charset.forName("UTF-8");
ArrayList<String> lines = new ArrayList<>();
lines.add("\n");
lines.add("Rome Masters - 5 titles in 6 years");
lines.add("Monte Carlo Masters - 7 consecutive titles (2005-2011)");
lines.add("Australian Open - Winner 2009");
lines.add("Roland Garros - Winner 2005-2008, 2010, 2011");
lines.add("Wimbledon - Winner 2008, 2010");
lines.add("US Open - Winner 2010");

try {
    Files.write(rf_wiki_path, lines, charset, StandardOpenOption.APPEND);
} catch (IOException e) {
    System.err.println(e);
}

阅读小文件

NIO.2 提供了一种快速读取小字节/文本文件的方法。该工具通过Files.readAllBytes()Files.readAllLines()方法提供。这些方法将整个文件的字节或行分别读入一次读取,并在文件被读取或发生 I/O 错误或异常后为您打开和关闭流。

使用 readAllBytes()方法读取

Files.readAllBytes()方法将整个文件读入一个字节数组,而Files.readAllLines()方法将整个文件读入一个String的集合(如下一节所述)。关注readAllBytes()方法,下面的代码片段将先前创建的ball.png二进制文件(该文件必须存在)读入一个字节数组(文件路径作为参数传递):

Path ball_path = Paths.get("C:/rafaelnadal/photos", "ball.png");
…
try {
    byte[] ballArray = Files.readAllBytes(ball_path);            
} catch (IOException e) {
    System.out.println(e);
}

如果您想确保返回的字节数组包含图片,您可以运行(作为测试)下面的代码片段,它将字节写入同一目录中名为bytes_to_ball.png的文件中:

…
Files.write(ball_path.resolveSibling("bytes_to_ball.png"), ballArray);
…

或者你也可以如下使用ImageIO。行ImageIO.write()将把您的bufferedImage数据作为 PNG 类型的文件写入您的磁盘,并将它存储在C:\rafaelnadal\photos目录中。

BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(ballArray));
ImageIO.write(bufferedImage, "png", (ball_path.resolveSibling("bytes_to_ball.png")).toFile());

readAllBytes()方法也可以读取文本文件。这一次,字节数组应该被转换为String,如下例所示(您可以使用任何适合您的文本文件的字符集):

Path wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt");
…
try {
    byte[] wikiArray = Files.readAllBytes(wiki_path);
    String wikiString = new String(wikiArray, "ISO-8859-1");
    System.out.println(wikiString);
} catch (IOException e) {
    System.out.println(e);
}

Image 注意如果文件太大(大于 2GB),那么数组的大小无法分配,会抛出OutOfMemory错误。这取决于 JVM 上的Xmx参数:对于 32 位 JVM,它不能大于 2GB(但默认情况下通常会更小,256MB,这取决于平台)。对于 64 位 JVM 来说,它可以大得多——可能有几十亿字节。

使用 readAllLines()方法读取

在前面的例子中,您看到了如何通过readAllBytes()方法读取文本文件。一个更方便的解决方案是使用readAllLines()方法,因为该方法将读取整个文件并返回一个StringList,可以很容易地如下循环(将文件的Path和用于解码的字符集传递给该方法):

Path wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt"); … Charset charset = Charset.forName("ISO-8859-1"); try {     List<String> lines = Files.readAllLines(wiki_path, charset);     for (String line : lines) {          System.out.println(line);     } } catch (IOException e) {     System.out.println(e); }

根据官方文件,该方法将以下内容识别为行终止符:

  • \u000D后接\u000A:回车后接换行
  • \u000A:换行
  • \u000D:回车

使用缓冲流

在大多数操作系统中,读取或写入数据的系统调用是一项开销很大的操作。缓冲区可以通过在缓冲的方法和操作系统之间提供一个内存空间来解决这个问题。在调用本机 API 之前,这些方法从操作系统和应用之间的缓冲区获取数据或将数据放入缓冲区,这提高了应用的效率,因为它减少了系统调用的数量-仅当缓冲区满或空时才访问磁盘,这取决于它是写操作还是读操作。NIO.2 提供了两种通过缓冲区读写文件的方法:分别是Files.newBufferedReader()Files.newBufferedWriter()。这两种方法都获得一个Path实例,并返回一个旧的 JDK 1.1 BufferedReaderBufferedWriter实例。

使用 newBufferedWriter()方法

newBufferedWriter()方法获取文件的路径、用于编码的字符集和指定文件打开方式的选项。它返回一个新的默认缓冲写入器(这是一个特定于java.ioBufferedWriter)。该方法打开文件进行写入(如果文件不存在,这可能涉及到创建文件),或者最初将现有的常规文件截断为 0 字节大小。简而言之,该方法就像CREATETRUNCATE_EXISTINGWRITE选项一样(当没有指定其他选项时,这是默认适用的)。

下面的代码片段使用一个缓冲区将数据追加到先前创建的wiki.txt文件中(该文件存在;您应该在C:\rafaelnadal\wiki目录中找到它):

Path wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt");
…
Charset charset = Charset.forName("UTF-8");
String text = "\nVamos Rafa!";
try (BufferedWriter writer = Files.newBufferedWriter(wiki_path, charset,
                                                                 StandardOpenOption.APPEND)) {
     writer.write(text);
} catch (IOException e) {
     System.err.println(e);
}
使用 newBufferedReader()方法

newBufferedReader()方法可用于通过缓冲区读取文件。方法获取文件的路径和用于将字节解码为字符的字符集。它返回一个新的默认缓冲读取器(这是一个特定于java.ioBufferedReader)。

下面的代码片段使用 UTF-8 字符集读取wiki.txt文件:

Path wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt");
…
Charset charset = Charset.forName("UTF-8");
try (BufferedReader reader = Files.newBufferedReader(wiki_path, charset)) {
     String line = null;
     while ((line = reader.readLine()) != null) {
             System.out.println(line);
     }
} catch (IOException e) {
     System.err.println(e);
}

如果您按照前面几节中的示例并创建了整个wiki.txt文件,那么前面的代码将输出以下内容:


Rafael "Rafa" Nadal Parera (born 3 June 1986) is a Spanish professional tennis player and a
former World No. 1\. As of 29 August 2011 (2011 -08-29)[update], he is ranked No. 2 by the
Association of Tennis Professionals (ATP). He is widely regarded as one of the greatest
players of all time; his success on clay has earned him the nickname "The King of Clay", and
has prompted many experts to regard him as the greatest clay court player of all time. Some
of his best wins are:

Rome Masters - 5 titles in 6 years

Monte Carlo Masters - 7 consecutive titles (2005-2011)

Australian Open - Winner 2009

Roland Garros - Winnner 2005-2008, 2010, 2011

Wimbledon - Winner 2008, 2010

US Open - Winner 2010

Vamos Rafa!

使用无缓冲流

非缓冲流可以通过新的 NIO.2 方法获得,既可以逐字使用,也可以使用java.io API 提供的包装习惯用法转换成缓冲流。无缓冲流方法有Files.newInputStream()(从文件中读取的输入流)和Files.newOutputStream()(写入文件的输出流)。

使用 newOutputStream()方法

newOutputStream()方法获取文件的路径和指定如何打开文件的选项。它返回一个新的默认线程安全的非缓冲流,可以用来向文件写入字节(这是一个特定于java.ioOutputStream)。该方法打开文件进行写入(如果文件不存在,这可能涉及到创建文件),或者最初将现有的常规文件截断为 0 字节大小。简而言之,该方法就像CREATETRUNCATE_EXISTINGWRITE选项一样(当没有指定其他选项时,这是默认适用的)。

以下代码片段将把文本行“球拍:Babolat AeroPro Drive GT”写入文件C:\rafaelnadal\equipment\racquet.txt(该文件最初并不存在,但由于没有指定选项,它将被自动创建):

Path rn_racquet = Paths.get("C:/rafaelnadal/equipment", "racquet.txt");
String racquet = "Racquet: Babolat AeroPro Drive GT";

byte data[] = racquet.getBytes();
try (OutputStream outputStream = Files.newOutputStream(rn_racquet)) {
     outputStream.write(data);
} catch (IOException e) {
     System.err.println(e);
}

此外,如果您决定使用缓冲流而不是前面的代码是一个更好的主意,建议使用基于java.io API 的转换,如下面的代码所示,它将文本“String: Babolat RPM Blast 16”附加到文件racquet.txt(该文件必须存在):

Path rn_racquet = Paths.get("C:/rafaelnadal/equipment", "racquet.txt");
String string = "\nString: Babolat RPM Blast 16";

try (OutputStream outputStream = Files.newOutputStream(rn_racquet, StandardOpenOption.APPEND);
     BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream))) {
      writer.write(string);
} catch (IOException e) {
     System.err.println(e);
}
使用 newInputStream()方法

newInputStream()方法获取要打开的文件的路径和指定如何打开文件的选项。它返回一个新的默认线程安全的非缓冲流,可以用来从文件中读取字节(这是一个特定于java.ioInputStream)。方法打开文件进行读取;如果没有选项,则相当于用READ选项打开文件。

以下代码片段读取文件racquet.txt的内容(该文件必须存在):

Path rn_racquet = Paths.get("C:/rafaelnadal/equipment", "racquet.txt"); … int n;     try (InputStream in = Files.newInputStream(rn_racquet)) {      while ((n = in.read()) != -1) {        System.out.print((char)n);                     } } catch (IOException e) {     System.err.println(e); }

正如您可能已经从java.io API 中了解到的那样,InputStream类还提供了一个read()方法,用于填充 byte 类型的缓冲数组。因此,您可以修改前面的代码,如下所示(请记住,您仍然在处理一个无缓冲的流):

Path rn_racquet = Paths.get("C:/rafaelnadal/equipment", "racquet.txt");
…
int n;    
byte[] in_buffer = new byte[1024];
try (InputStream in = Files.newInputStream(rn_racquet)) {
     while ((n = in.read(in_buffer)) != -1) {
            System.out.println(new String(in_buffer));
     }
} catch (IOException e) {
     System.err.println(e);
}

Image 注意调用read(in_buffer)方法和调用read(in_buffer,0,in_buffer.length)方法是一回事。

此外,您可以通过与java.io API 交互操作,将非缓冲流转换为缓冲流。下面的示例与前面的示例效果相同,但效率更高:

Path rn_racquet = Paths.get("C:/rafaelnadal/equipment", "racquet.txt");
…
try (InputStream in = Files.newInputStream(rn_racquet);
     BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
     String line = null;
     while ((line = reader.readLine()) != null) {
             System.out.println(line);
     }
} catch (IOException e) {
     System.err.println(e);
}

过去的三个示例将具有相同的输出:


Racquet: Babolat AeroPro Drive GT

String: Babolat RPM Blast 16

创建临时目录和文件

临时目录是存储临时文件的目录。临时目录的位置取决于操作系统。在 Windows 中,临时目录是通过 TEMP 环境变量设置的,通常是C:\Temp%Windows%\Temp,或者是Local Settings\Temp中的每个用户一个临时目录。在 Linux/Unix 中,全局临时目录是/tmp/var/tmp

创建临时目录

在 NIO.2 中,可以用createTempDirectory()方法创建一个临时目录。在默认操作系统位置创建一个临时目录可以通过调用带有两个参数的createTempDirectory()方法来完成:一个用于生成目录名的前缀字符串(可以是null)和一个可选的文件属性列表,在创建目录时自动设置。以下代码片段创建了两个临时目录,一个带前缀,一个不带前缀:

String tmp_dir_prefix = "nio_";
try {
    //passing null prefix
    Path tmp_1 = Files.createTempDirectory(null);
    System.out.println("TMP: " + tmp_1.toString());

    //set a prefix
    Path tmp_2 = Files.createTempDirectory(tmp_dir_prefix);
    System.out.println("TMP: " + tmp_2.toString());
} catch (IOException e) {
    System.err.println(e);
}

以下是可能的输出:


TMP: C:\Users\Leo\AppData\Local\Temp\3238630399269555448

TMP: C:\Users\Leo\AppData\Local\Temp\nio_1097550355199661257

Image 注意如果不知道临时目录的默认位置是什么,可以使用下面的代码:

//output: C:\Users\Leo\AppData\Local\Temp\
String default_tmp = System.getProperty("java.io.tmpdir");
System.out.println(default_tmp);

更进一步,您可以通过调用另一个createTempDirectory()方法来指定创建临时目录的默认目录。除了临时目录前缀和可选的属性列表,这个方法还获得了一个代表临时目录的默认目录的Path。以下示例在C:\rafaelnadal\tmp目录中创建一个临时目录:

Path basedir = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp/");
String tmp_dir_prefix = "rafa_";
…
try {
    //create a tmp directory in the base dir
    Path tmp = Files.createTempDirectory(basedir, tmp_dir_prefix);
    System.out.println("TMP: " + tmp.toString());
} catch (IOException e) {
    System.err.println(e);
}

以下是可能的输出:


TMP: C:\rafaelnadal\tmp\rafa_1753327229539718259

使用关机挂钩删除临时目录

大多数操作系统会自动删除临时目录(如果没有,您可以使用几种清理软件中的一种)。但是,有时您可能需要以编程方式控制删除过程。方法只做了一半的工作,因为删除是你的责任。为此,您可以附加一个关闭挂钩机制,一个用于执行任何资源清理或保存的运行时机制,这些必须在 JVM 关闭之前发生。这个钩子可以作为 Java Thread来实现。当钩子在关机时被 JVM 执行时,Threadrun()方法将被执行。图 4-1 中的显示了一个漂亮而简单的关断钩流程设计。

Image

图 4-1 。关机挂钩的简单流程设计

将图 4-1 中所示的图放入代码行中,提供如下框架代码:

`Runtime.getRuntime().addShutdownHook(new Thread() {

@Override
public void run() {   System.out.println("Shutdown-hook activated ...");

//… here, cleanup/save resources

System.out.println("Shutdown-hook successfully executed ...");
  }
});`

Image 注意注意添加一个关闭挂钩作为ThreadRuntime可以作为一个匿名的内部类来完成,就像前面的代码一样,或者作为一个单独的类来实现Runnable或者扩展Thread

当 JVM 关闭时,shutdown-hook 是删除临时目录的一个很好的解决方案,但是,您可能知道,如果目录不是空的,就不能删除它;因此,您需要遍历临时目录的内容,并在删除临时目录本身之前删除每个条目。至此,您已经知道如何向下遍历一个目录的内容,所以现在假设您的临时目录只包含临时文件(在许多实际情况下都是如此)和其他空的临时目录。在本书的后面,你将看到如何实现递归操作来浏览层次结构的所有级别。

以下示例将上一节中列出目录内容的代码与 shutdown-hook 结合在一起:

`final Path basedir = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp/");
final String tmp_dir_prefix = "rafa_";

try {
//create a tmp directory in the base dir
final Path tmp_dir = Files.createTempDirectory(basedir, tmp_dir_prefix);

Runtime.getRuntime().addShutdownHook(new Thread() {

@Override
public void run() {
  System.out.println("Deleting the temporary folder ...");

try (DirectoryStream ds = Files.newDirectoryStream(tmp_dir)) {
       for (Path file : ds) {
               Files.delete(file);
       }

Files.delete(tmp_dir);

} catch (IOException e) {
       System.err.println(e);
   }

System.out.println("Shutdown-hook completed...");
} });

//simulate some I/O operations over the temporary file by sleeping 10 seconds
//when the time expires, the temporary file is deleted            
Thread.sleep(10000);
//operations done

} catch (IOException | InterruptedException e) {
    System.err.println(e);
}`

Image 注意前面的例子使用了一个Thread.sleep()方法在临时目录的创建时间和 JVM 关闭时间之间添加一个延迟。显然,代替它的是,您将提供使用临时目录的业务逻辑,该临时目录是为作业创建的。

使用 deleteOnExit()方法删除临时目录

删除临时目录的另一个解决方案是调用deleteOnExit()方法。这个方法在java.io.File类中可用(不特定于 NIO.2 ),它将在 JVM 关闭时删除传递的文件或目录。因为必须为每个临时文件或目录调用此方法,所以它被认为是最没有吸引力的选择,因为它将为每个临时实体消耗内存。

Image 注意如果你的系统长时间处于活动状态或者在短时间内创建了许多临时文件或目录,那么使用deleteOnExit()是个坏主意!在选择使用deleteOnExit()之前,要考虑到它会使用大量的内存,直到 JVM 终止才会释放。

以下代码片段向您展示了如何使用deleteOnExit():

`Path basedir = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp/");
String tmp_dir_prefix = "rafa_";

try {
    //create a tmp directory in the base dir
    Path tmp_dir = Files.createTempDirectory(basedir, tmp_dir_prefix);

File asFile = tmp_dir.toFile();
    asFile.deleteOnExit();

//simulate some I/O operations over the temporary file by sleeping 10 seconds
    //when the time expires, the temporary file is deleted            
    //EACH CREATED TEMPORARY ENTRY SHOULD BE REGISTERED FOR DELETE ON EXIT
    Thread.sleep(10000);     //operations done

} catch (IOException | InterruptedException e) {
   System.err.println(e);
}`

Image 注意因为deleteOnExit()适用于File实例,而不是Path,所以需要通过调用Path.toFile()方法将Path转换为File

创建临时文件

这一节将详细介绍临时文件以及 NIO.2 处理这些文件的方法。在实际应用中,临时文件通常会提供非常有用的帮助。当您需要在应用或应用执行之外使用的非缩进文件时,它们非常有用。在 Java 中称为“工作文件”,它们可以放在从应用中选择的任何目录中,或者放在由 Java 属性java.io.tmpdir返回的默认位置中。

在 NIO.2 中,可以用createTempFile()方法创建一个临时文件。在默认的操作系统位置创建一个临时文件可以通过调用带有三个参数的createTempFile()方法来完成:一个前缀字符串连接在文件名前面(可以是null),一个后缀字符串连接在文件名后面(可以是null);缺省值是.tmp,以及创建文件时自动设置的可选文件属性列表。以下代码片段创建了两个临时文件,一个没有前缀和后缀,另一个具有指定的前缀和后缀:

String tmp_file_prefix = "rafa_";
String tmp_file_sufix=".txt";

try {
    //passing null prefix/suffix
    Path tmp_1 = Files.createTempFile(null,null);
    System.out.println("TMP: " + tmp_1.toString());

    //set a prefix and a suffix
    Path tmp_2 = Files.createTempFile(tmp_file_prefix, tmp_file_sufix);
    System.out.println("TMP: " + tmp_2.toString());

} catch (IOException e) {
    System.err.println(e);
}

输出将是操作系统默认位置中的两个空临时文件:


TMP: C:\Users\Leo\AppData\Local\Temp\6873427319542945524.tmp

TMP: C:\Users\Leo\AppData\Local\Temp\rafa_6168226983257408796.txt

Image 注意如果不知道临时文件的默认位置是什么,可以使用下面的代码:

//output: C:\Users\Leo\AppData\Local\Temp\
String default_tmp = System.getProperty("java.io.tmpdir");

更进一步,您可以通过调用另一个createTempFile()方法来指定创建临时文件的默认目录。除了临时文件的前缀和后缀以及可选的属性列表,这个方法还获得了一个代表临时文件的默认目录的Path。下面是一个在C:\rafaelnadal\tmp目录下创建临时文件的例子:

Path basedir = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp");
String tmp_file_prefix = "rafa_";
String tmp_file_sufix=".txt";

try {
    Path tmp_3 = Files.createTempFile(basedir, tmp_file_prefix, tmp_file_sufix);
    System.out.println("TMP: " + tmp_3.toString());
} catch (IOException e) {
    System.err.println(e);
}

输出将是C:\rafaelnadal\tmp目录中的一个空临时文件:


TMP: C:\rafaelnadal\tmp\rafa_512352743612949417.txt

使用关机挂钩删除临时文件

临时文件只是一个简单的文件,直到您确定它确实是临时的,这意味着自动机制必须定期或在指定的时间删除临时文件。shutdown-hook 机制在本章前面的“使用 Shutdown-Hook 删除临时目录”一节中已经介绍过了这种机制对于临时文件也是一样的,所以我们将跳过这里的演示,直接看代码示例。

下面的代码片段将在C:\rafaelnadal\tmp目录中创建一个临时文件,等待 10 秒钟(模拟一些文件使用),并在 JVM 通过 shutdown-hook 机制关闭时删除该文件:

`Path basedir = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp");
String tmp_file_prefix = "rafa_";
String tmp_file_sufix = ".txt";

try {
    final Path tmp_file = Files.createTempFile(basedir, tmp_file_prefix, tmp_file_sufix);

Runtime.getRuntime().addShutdownHook(new Thread() { @Override
    public void run() {
    System.out.println("Deleting the temporary file ...");

try {
        Files.delete(tmp_file);
    } catch (IOException e) {
        System.err.println(e);
    }

System.out.println("Shutdown hook completed...");
  }
});

//simulate some I/O operations over the temporary file by sleeping 10 seconds
//when the time expires, the temporary file is deleted            
Thread.sleep(10000);
//operations done

} catch (IOException | InterruptedException e) {
    System.err.println(e);
}`

Image 注意前面的代码使用了一个Thread.sleep()方法在临时文件的创建时间和 JVM 关闭时间之间添加一个延迟。显然,代替它的是,您将为创建临时文件的作业提供使用临时文件的业务逻辑。

用 deleteOnExit()方法删除临时文件

删除临时文件的另一个解决方案是调用deleteOnExit()方法。这种机制在前面的“使用 deleteOnExit()方法删除临时目录”一节中有详细介绍,对于临时文件也是如此,所以我们在这里将跳过它,直接看代码示例。

下面的代码片段将在C:\rafaelnadal\tmp目录中创建一个临时文件,等待 10 秒钟(模拟一些文件使用),并在 JVM 通过deleteOnExit()机制关闭时删除它:

`Path basedir = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp");
String tmp_file_prefix = "rafa_";
String tmp_file_sufix = ".txt";

try {
    final Path tmp_file = Files.createTempFile(basedir, tmp_file_prefix, tmp_file_sufix);

File asFile = tmp_file.toFile();
    asFile.deleteOnExit();

//simulate some I/O operations over the temporary file by sleeping 10 seconds     //when the time expires, the temporary file is deleted
    Thread.sleep(10000);
    //operations done

} catch (IOException | InterruptedException e) {
    System.err.println(e);
}`

Image 注意因为deleteOnExit()适用于File实例,而不是Path,所以需要通过调用Path.toFile()方法将Path转换为File

用 DELETE_ON_CLOSE 删除临时文件

删除临时文件的一个巧妙的解决方案是使用DELETE_ON_CLOSE选项。顾名思义,这个选项在流关闭时删除文件。例如,下面的代码片段使用createTempFile()方法在C:\rafaelnadal\tmp目录中创建一个临时文件,并使用显式指定的DELETE_ON_CLOSE为其打开一个流,因此当流关闭时,该文件应该被删除:

Path basedir = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp");
String tmp_file_prefix = "rafa_";
String tmp_file_sufix = ".txt";
Path tmp_file = null;

try {
    tmp_file = Files.createTempFile(basedir, tmp_file_prefix, tmp_file_sufix);
} catch (IOException e) {
    System.err.println(e);
}

try (OutputStream outputStream = Files.newOutputStream(tmp_file,
                                      StandardOpenOption.DELETE_ON_CLOSE);
     BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream))) {

     //simulate some I/O operations over the temporary file by sleeping 10 seconds
     //when the time expires, the temporary file is deleted            
     Thread.sleep(10000);
     //operations done
} catch (IOException | InterruptedException e) {
     System.err.println(e);
}

此外,即使不调用createTempFile()方法,也可以模拟一个临时文件。只需定义一个文件名,并结合使用DELETE_ON_CLOSE选项和CREATE选项,如下面的代码片段所示(效果与前面的示例相同):

String tmp_file_prefix = "rafa_"; String tmp_file_sufix = ".txt"; `Path tmp_file = null;

tmp_file = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp", tmp_file_prefix +
                                                                "temporary" + tmp_file_sufix);

try (OutputStream outputStream = Files.newOutputStream(tmp_file, StandardOpenOption.CREATE,
                                                          StandardOpenOption.DELETE_ON_CLOSE);
     BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream))) {

//simulate some I/O operations over the temporary file by sleeping 10 seconds
     //when the time expires, the temporary file is deleted            
     Thread.sleep(10000);
     //operations done
} catch (IOException | InterruptedException e) {
     System.err.println(e);
}`

删除、复制和移动目录和文件

删除、复制和移动是对文件和目录最常用的三种操作。NIO.2 提供了专门的方法来支持这些操作的不同方法。正如您将在本节中看到的,它们中的大多数来自于Files类。

删除文件和目录

NIO.2 提供了两种删除文件或目录的方法,Files.delete()Files.deleteIfExits()。两者都采用单个参数表示要删除的Path,但是Files.delete()返回 void,而Files.deleteIfExits()返回一个布尔值表示删除过程的成功或失败。delete()方法尝试删除传递的Path,如果失败,抛出以下异常之一:NoSuchFileException(如果传递的Path不存在)、DirectoryNotEmptyException(如果传递的Path是一个不为空的目录)、IOException(如果发生 I/O 错误),或者SecurityException(如果删除的访问被拒绝)。

以下代码片段从C:\rafaelnadal\photos\目录中删除文件rafa_1.jpg(该文件必须存在):

Path path = FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_1.jpg");

//delete the file
try {
     Files.delete(path);
} catch (NoSuchFileException | DirectoryNotEmptyException | IOException |
         SecurityException e) {
     System.err.println(e);
}

顾名思义,Files.deleteIfExists()方法只删除存在的文件,这意味着如果文件因为不存在而无法删除,那么返回的布尔值将是false(而不是抛出NoSuchFileException异常)。当您有多个线程删除文件,并且您不想仅仅因为一个线程先这么做就抛出异常时,这很有用。记住前面的代码刚刚删除了rafa_1.jpg文件,下面的代码将返回false:

try {
    boolean success = Files.deleteIfExists(path);
    System.out.println("Delete status: " + success);
} catch (DirectoryNotEmptyException | IOException | SecurityException e) {
    System.err.println(e);
}

Image 注意如果删除的资源是一个目录,那么它一定是空的。删除整个目录内容(可能包含其他目录、文件等)是一项通常作为递归操作实现的任务。该操作在第五章的中介绍。

Image 注意如果文件是符号链接,那么删除的是符号链接本身,而不是链接的最终目标。

复制文件和目录

在 NIO.2 中,复制文件和目录是小菜一碟。它提供了三个Files.copy()方法来完成这项任务,并提供了一组选项来控制复制过程——这些方法采用由这些选项表示的一个varargs参数。这些选项在StandardCopyOptionLinkOption菜单下提供,如下所示:

  • REPLACE_EXISTING:如果复制的文件已经存在,那么它被替换(在非空目录的情况下,抛出FileAlreadyExistsException)。当处理一个符号链接时,链接的目标是而不是复制的;仅复制链接。
  • COPY_ATTRIBUTES:复制文件及其关联属性(至少支持复制lastModifiedTime属性)。
  • NOFOLLOW_LINKS:符号链接不应该跟随。

如果您不熟悉枚举类型,那么您应该知道它们可以按如下方式导入到应用中。这些被称为静态导入,可以导入任何静态字段或方法,而不仅仅是来自枚举类型的字段(例如来自java.lang.Math的方法)。

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;

Image 注意默认情况下,复制符号链接时,复制的是该链接的目标。仅复制链接本身可以通过REPLACE_EXISTINGNOFOLLOW_LINKS选项完成。而且,不需要复制文件属性。

Image 警告试图复制一个非空的目录会导致一个空的目录。这是一个通常以递归操作实现的任务,正如你将在第五章中看到的。此外,复制文件不是原子操作,这意味着即使目标文件不完整或属性没有完全复制,也会抛出IOException异常并中止复制。

在两个路径之间复制

通常,当您复制文件时,您需要一个源路径(复制自)和一个目标路径(复制到)。基于这个简单的例子,NIO.2 提供了一个Files.copy()方法,该方法获取要复制的文件的路径、目标文件的路径以及一组用于控制复制过程的选项。它返回目标文件的路径。如果未指定选项,则仅当目标文件不存在并且不是符号链接时,复制才会成功结束。否则,除非源和目标不同,否则将抛出异常(方法isSameFile()返回true)。

下面的代码片段将文件draw_template.txtC:\rafaelnadal\grandslam\AustralianOpen复制到C:\rafaelnadal\grandslam\USOpen(该文件必须存在)。它替换现有文件,将源的属性复制到目标,并且不遵循链接。

Path copy_from = Paths.get("C:/rafaelnadal/grandslam/AustralianOpen", "draw_template.txt");
Path copy_to= Paths.get("C:/rafaelnadal/grandslam/USOpen",copy_from.getFileName().toString());

try {

    Files.copy(copy_from, copy_to, REPLACE_EXISTING, COPY_ATTRIBUTES, NOFOLLOW_LINKS);

} catch (IOException e) {
    System.err.println(e);
}
从输入流复制到文件

当您需要将输入流中的所有字节复制到一个文件中时,您可以调用Files.copy()方法来获取要读取的输入流、文件的路径以及一组用于控制复制过程的选项。它返回读取或写入的字节数。默认情况下,如果目标文件已经存在或者是符号链接,复制将失败。

下面的代码片段将通过输入流将文件draw_template.txtC:\rafaelnadal\grandslam\AustralianOpen复制到C:\rafaelnadal\grandslam\Wimbledon(该文件必须存在)。它将替换现有文件。

Path copy_from = Paths.get("C:/rafaelnadal/grandslam/AustralianOpen", "draw_template.txt");
Path copy_to = Paths.get("C:/rafaelnadal/grandslam/Wimbledon", "draw_template.txt");

try (InputStream is = new FileInputStream(copy_from.toFile())) {

     Files.copy(is, copy_to, REPLACE_EXISTING);

} catch (IOException e) {
     System.err.println(e);
}

可以用其他方式提取输入流。例如,以下代码片段将从 Internet URL 获取输入流(仅当文件不存在时,它才会将 URL 指示的图片复制到C:\rafaelnadal\photos目录):

Path copy_to = Paths.get("C:/rafaelnadal/photos/rafa_winner_2.jpg");
URI u = URI.create("https://lh6.googleusercontent.com/--
                        udGIidomAM/Tl8KTbYd34I/AAAAAAAAAZw/j2nH24PaZyM/s800/rafa_winner.jpg");

try (InputStream in = u.toURL().openStream()) {

     Files.copy(in, copy_to);

} catch (IOException e) {
     System.err.println(e);
}

Image 注意强烈建议您在发生 I/O 错误后立即关闭输入流。

从文件复制到输出流

当您需要将文件中的所有字节复制到输出流中时,您可以调用Files.copy()方法来获取文件的路径和要写入的输出流。它将返回读取或写入的字节数。

下面的代码片段将文件draw_template.txtC:\rafaelnadal\grandslam\AustralianOpen复制到C:\rafaelnadal\grandslam\RolandGarros。目标文件表示为输出流(如果目标文件存在,将被替换)。

`Path copy_from = Paths.get("C:/rafaelnadal/grandslam/AustralianOpen", "draw_template.txt");
Path copy_to = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "draw_template.txt");

try (OutputStream os = new FileOutputStream(copy_to.toFile())) {

Files.copy(copy_from, os); } catch (IOException e) {
     System.err.println(e);
}`

Image 注意强烈建议您在发生 I/O 错误后立即关闭输出流。

移动文件和目录

在本节中,您将看到如何使用Files.move()方法移动文件和目录。此方法获取要移动的文件的路径、目标文件的路径以及控制移动过程的一组选项。这些选项在StandardCopyOption枚举下提供,在此列出:

  • REPLACE_EXISTING:如果目标文件已经存在,则仍然执行移动,并替换目标。处理符号链接时,符号链接被替换,但它指向的内容不受影响。
  • ATOMIC_MOVE:文件移动将作为一个原子操作来执行,这保证了任何监视文件目录的进程都将访问一个完整的文件。

同样,这些枚举类型可以像这样导入到应用中:

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;

默认情况下(没有显式指定选项时),move()方法尝试将文件移动到目标文件,如果目标文件存在则失败(FileAlreadyExistsException被抛出),除非源和目标是同一个文件(isSameFile()方法返回true),在这种情况下该方法无效。

Image 注意默认情况下,移动符号链接时,移动的是符号链接本身,而不是那个链接的目标。

Image 注意move()方法也可以用来移动空目录。试图移动一个非空的目录通常是一个递归复制操作,正如你在第五章中看到的。然而,如果不需要移动目录中的条目,也可以移动非空的目录。在某些情况下,目录包含创建目录时创建的特殊文件(如链接)的条目,如果目录只包含这些条目,则认为它是空的。

下面的代码片段试图将名为rafa_2.jpg(该文件必须存在)的文件从C:\rafaelnadal移动到C:\rafaelnadal\photos。如果目标已经存在,那么它将被替换,因为指定了REPLACE_EXISITING选项。

Path movefrom = FileSystems.getDefault().getPath("C:/rafaelnadal/rafa_2.jpg");
Path moveto = FileSystems.getDefault().getPath("C:/rafaelnadal/photos/rafa_2.jpg");

try {
    Files.move(movefrom, moveto, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
    System.err.println(e);
}

您可以通过使用Path.resolve()方法跳过对moveto路径中的文件名进行硬编码(更多详细信息,请参见第一章)。使用这种方法,您可以通过直接从movefrom路径中提取文件名来移动文件(在测试这段代码之前,不要忘记恢复C:\rafaelnadal中的rafa_2.jpg文件):

Path movefrom = FileSystems.getDefault().getPath("C:/rafaelnadal/rafa_2.jpg");
Path moveto_dir = FileSystems.getDefault().getPath("C:/rafaelnadal/photos");

try {
    Files.move(movefrom, moveto_dir.resolve(movefrom.getFileName()),
                                                         StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
    System.err.println(e);
}

重命名文件

最后,通过一个小技巧,您可以使用Files.move()Path.resolveSibling()方法重命名文件。下面的代码片段在C:\rafaelnadal\photos目录中将文件rafa_2.jpg重命名为rafa_renamed_2.jpg。如果您已经测试了前面的代码,那么rafa_2.jpg应该出现在这个目录中。

Path movefrom = FileSystems.getDefault().getPath("C:/rafaelnadal/photos/rafa_2.jpg");

try {
    Files.move(movefrom, movefrom.resolveSibling("rafa_2_renamed.jpg"),
                                                         StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
    System.err.println(e);
}

总结

本章一开始探索了一些专门检查Path是否可读、可写、常规或隐藏的方法。然后重点讨论了目录操作以及如何列出、创建和读取目录。您看到了如何列出文件系统根目录,如何使用createDirectory()createTempDirectory()等方法创建目录,如何编写目录过滤器,以及如何使用newDirectoryStream()方法列出目录的内容。本章还探讨了文件操作,比如读、写、创建和打开文件。如您所见,有大量的文件 I/O 方法可供选择(针对缓冲和非缓冲流)。本章以众所周知的删除、复制和移动操作结束。*

五、递归运算:遍历

您可能知道,递归编程是一种有争议的技术,因为它通常需要大量内存,但它简化了一些编程任务。基本上,递归编程是这样一种情况:过程调用自身,传入一个或多个参数的修改值,这些参数被传入到过程的当前迭代中。计算阶乘、斐波那契数、字谜和 Sierpinski 地毯等编程任务只是可以通过递归编程技术完成的一些众所周知的任务。下面的代码片段使用这种技术来计算阶乘(n!= 1 * 2 * 3 …… n)—请注意该过程如何调用自己:

/**
  * Calculate the factorial of n (n! = 1 * 2 * 3 * … * n).
  *
  * @param n the number to calculate the factorial of.
  * @return n! - the factorial of n.
  */
static int fact(int n) {

   // Base Case:
   // If n <= 1 then n! = 1.
   if (n <= 1) {
       return 1;
   }
   // Recursive Case:  
   // If n > 1 then n! = n * (n-1)!
   else {
        return n * fact(n-1);
        }
   }

如果您已经熟悉这种编程技术,那么请继续阅读本章,看看 NIO.2 如何利用它。否则,在您继续阅读一些致力于递归编程的教程之前,这是一个好主意,例如 Jonathan Bartlett 的“掌握递归编程”,可在[www.ibm.com/developerworks/linux/library/l-recurs/index.html](http://www.ibm.com/developerworks/linux/library/l-recurs/index.html)获得。

许多涉及使用文件的编程任务需要访问文件树中的所有文件,这是使用递归编程机制的好机会,因为每个文件都应该单独“接触”。在执行删除、复制或移动文件树等任务时,这是一种非常常见的方法。基于这种机制,NIO.2 将文件树的遍历过程封装在一个名为FileVisitor的接口中,在java.nio.file包中。

本章首先介绍FileVisitor的范围和方法。一旦您熟悉了FileVisitor,本章将帮助您开发一套应用,您可以用它来执行涉及遍历文件树的任务,比如查找、复制、删除和移动文件。

file visitor 接口

如前所述,FileVisitor接口支持递归遍历文件树。该接口的方法表示遍历过程中的关键点,使您能够在访问文件时、访问目录前、访问目录后以及发生故障时进行控制;换句话说,这个接口在访问文件之前、期间和之后都有挂钩,在出现故障时也有挂钩。一旦您拥有了控制权(在这些关键点的任何一点上),您就可以选择如何处理被访问的文件,并通过FileVisitResult枚举指示访问结果来决定接下来应该对它做什么,该枚举包含四个枚举常量:

  • FileVisitResult.CONTINUE:这个访问结果表示遍历过程应该继续。根据返回哪个FileVisitor方法,它可以被翻译成不同的动作。例如,遍历过程可以通过访问下一个文件、访问目录条目或跳过失败来继续。
  • FileVisitResult.SKIP_SIBLINGS:这个访问结果表示遍历过程应该继续,而不访问这个文件或目录的兄弟。
  • FileVisitResult.SKIP_SUBTREE:这个访问结果表示遍历过程应该继续,而不访问这个目录中的其余条目。
  • FileVisitResult.TERMINATE:该访问结果表示遍历过程应该终止。

此枚举类型的常数可以迭代如下:

for (FileVisitResult constant : FileVisitResult.values())
    System.out.println(constant);

以下小节讨论了如何通过实现各种FileVisitor方法来控制遍历过程。

FileVisitor.visitFile()方法

为目录中的文件调用visitFile()方法。通常,这个方法返回一个CONTINUE结果或者一个TERMINATE结果。例如,当搜索一个文件时,该方法应该返回CONTINUE直到找到该文件(或者完全遍历该树),并在找到该文件后返回TERMINATE

当这个方法被调用时,它接收对文件的引用和文件的基本属性。如果发生 I/O 错误,那么它抛出一个IOException异常。以下是此方法的签名:

FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException

file visitor . previsitdirectory()方法

在访问目录条目之前,为目录调用preVisitDirectory()方法。如果方法返回CONTINUE将访问条目,如果返回SKIP_SUBTREE将不访问条目(后一个访问的结果只有从该方法返回时才有意义)。此外,通过返回SKIP_SIBLINGS结果,您可以跳过访问该文件或目录的同级(以及任何后代)。

当这个方法被调用时,它获得对目录和目录的基本属性的引用。如果发生 I/O 错误,那么它抛出一个IOException异常。这个方法的特征是

FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException

file visitor . postvisit directory()方法

在已经访问了目录中的所有条目(以及任何后代)或者访问突然结束(即,发生了 I/O 错误或者访问以编程方式中止)之后,调用postVisitDirectory()方法。当这个方法被调用时,它获得一个对目录和IOException对象的引用——如果访问期间没有发生错误,它将是null,如果发生错误,它将返回相应的错误。如果发生 I/O 错误,那么它抛出一个IOException异常。下面是这个方法的签名

FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException

FileVisitor.visitFileFailed()方法

当文件由于各种不同的原因而无法访问时,例如无法读取文件的属性或无法打开目录,就会调用visitFileFailed()方法。当调用此方法时,它会获取对该文件的引用以及在尝试访问该文件时发生的异常。如果发生 I/O 错误,那么它抛出一个IOException异常。以下是此方法的签名:

FileVisitResult visitFileFailed(T file, IOException exc) throws IOException

简单文件访问者类

实现FileVisitor接口需要实现它的所有方法,如果您只需要实现其中的一个或几个方法,这可能是不可取的。在这种情况下,扩展实现了FileVisitor接口的SimpleFileVisitor类就简单多了。这种方法只需要覆盖所需的方法。

例如,您可能想要遍历文件树并列出所有目录的名称。要实现这一点,只使用postVisitDirectory()visitFileFailed()方法就足够了,如下面的代码片段所示(起始文件树在下一节中给出):

`class ListTree extends SimpleFileVisitor {

@Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) {

System.out.println("Visited directory: " + dir.toString());

return FileVisitResult.CONTINUE;
    }

@Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) { System.out.println(exc);

return FileVisitResult.CONTINUE;
    }
}`

如您所见,跳过了preVisitDirectory()visitFile()方法。

开始递归过程

一旦创建了递归机制(通过实现FileVisitor接口或扩展SimpleFileVisitor类),就可以通过调用两个Files.walkFileTree()方法之一来启动这个过程。最简单的walkFileTree()方法获取起始文件(这通常是文件树根)和每个文件要调用的文件访问者(这是递归机制类的一个实例)。例如,您可以通过如下方式调用walkFileTree()方法来启动上一节中的代码示例(传递的文件树是C:\rafaelnadal):

Path listDir = Paths.get("C:/rafaelnadal"); //define the starting file tree
ListTree walk = new ListTree();             //instantiate the walk

try{
   Files.walkFileTree(listDir, walk);       //start the walk
   } catch(IOException e){
     System.err.println(e);
   }

第二个walkFileTree()方法获取开始文件、定制遍历的选项、要访问的最大目录级别数(为了确保遍历所有级别,可以为最大深度参数指定Integer.MAX_VALUE)和遍历实例。接受的选项是FileVisitOption枚举的常量。实际上,这个枚举包含一个名为FOLLOW_LINKS的常量,表示遍历中遵循了符号链接(默认情况下,它们没有被遵循)。

为前面的遍历调用此方法可能如下所示:

Path listDir = Paths.get("C:/rafaelnadal");              //define the starting file
ListTree walk = new ListTree();                          //instantiate the walk
EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS); //follow links

try{
   Files.walkFileTree(listDir, opts, Integer.MAX_VALUE, walk); //start the walk
   } catch(IOException e){
     System.err.println(e);
   }

Image 呼叫walkFileTree(start, visitor)与呼叫walkFileTree(start, EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, visitor)效果相同。

以下几行是前面示例的输出:


Visited directory: C:\rafaelnadal\equipment

Visited directory: C:\rafaelnadal\grandslam\AustralianOpen

Visited directory: C:\rafaelnadal\grandslam\RolandGarros

Visited directory: C:\rafaelnadal\grandslam\USOpen

Visited directory: C:\rafaelnadal\grandslam\Wimbledon

Visited directory: C:\rafaelnadal\grandslam

…

Visited directory: C:\rafaelnadal

常见的散步

有一组常见的遍历,您可以通过FileVisitor接口轻松实现。本节向您展示如何编写和实现应用来执行文件搜索、递归复制、递归移动和递归删除。

编写文件搜索应用

大多数操作系统都提供了搜索文件的专用工具(例如,Linux 有find命令,而 Windows 有文件搜索工具)。从简单搜索到高级搜索,所有工具通常都以相同的方式工作:指定搜索条件,然后等待工具找到匹配的文件。但是,如果您需要以编程方式完成搜索,那么FileVisitor可以帮助您完成遍历过程。无论您是按名称、扩展名或 glob 模式查找文件,还是在文件内部查找一些文本或代码,方法总是访问文件存储中的每个文件并执行一些检查,以确定文件是否符合您的搜索标准。

当您基于FileVisitor编写文件搜索工具时,您需要记住以下几点:

  • visitFile()方法是在当前文件和您的搜索标准之间进行比较的最佳位置。此时,您可以提取每个文件名、扩展名或属性,或者打开文件进行阅读。您可以使用文件名、扩展名等来确定所访问的文件是否是所搜索的文件。有时,您会将这些信息混合到复杂的搜索标准中。这种方法找不到目录。
  • 如果您想要查找目录,那么比较必须在preVisitDirectory()postVisitDirectory()方法中进行,这取决于大小写。
  • 如果一个文件不能被访问,visitFileFailed()方法应该返回FileVisitResult.CONTINUE,因为这个问题不需要停止整个搜索过程。
  • 如果您按名称搜索一个文件,并且您知道在文件树中有一个同名的文件,那么一旦visitFile()方法找到它,您就可以返回FileVisitResult.TERMINATE。否则,FileVisitResult.CONTINUE应该被退回。
  • 搜索过程可以遵循符号链接,这可能是一个好主意,因为遵循符号链接可以在遍历符号链接的目标子树之前定位所搜索的文件。跟随符号链接并不总是一个好主意;例如,删除文件是不可取的。
按名称搜索文件

可以将前面的列表合并到下面的代码片段中,以生成一个按名称搜索文件的应用。该应用将在整个默认文件系统中搜索文件rafa_1.jpg,并在找到时停止搜索。

`import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;

class Search implements FileVisitor {

private final Path searchedFile;
    public boolean found;

public Search(Path searchedFile) {
        this.searchedFile = searchedFile;
        this.found = false;
    }

void search(Path file) throws IOException {
        Path name = file.getFileName();
        if (name != null && name.equals(searchedFile)) {
            System.out.println("Searched file was found: " + searchedFile +
                                                   " in " + file.toRealPath().toString());
            found = true;
        }
    }

@Override     public FileVisitResult postVisitDirectory(Object dir, IOException exc)
                                              throws IOException {
        System.out.println("Visited: " + (Path) dir);
        return FileVisitResult.CONTINUE;
    }

@Override
    public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs)
                                             throws IOException {
        return FileVisitResult.CONTINUE;
    }

@Override
    public FileVisitResult visitFile(Object file, BasicFileAttributes attrs)
                                     throws IOException {
        search((Path) file);
        if (!found) {
            return FileVisitResult.CONTINUE;
        } else {
            return FileVisitResult.TERMINATE;
        }
    }

@Override
    public FileVisitResult visitFileFailed(Object file, IOException exc)
                                           throws IOException {
        //report an error if necessary
        return FileVisitResult.CONTINUE;
    }
}

class Main {

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

Path searchFile = Paths.get("rafa_1.jpg");
        Search walk = new Search(searchFile);
        EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);

Iterable dirs = FileSystems.getDefault().getRootDirectories();
        for (Path root : dirs) {
            if (!walk.found) {
                Files.walkFileTree(root, opts, Integer.MAX_VALUE, walk);
            }
        }

if (!walk.found) {
            System.out.println("The file " + searchFile + " was not found!");
        }
    }
}`

输出的一个片段可能如下所示:


…

Visited: C:\Python25\Tools\webchecker

Visited: C:\Python25\Tools

Visited: C:\Python25

…

Visited: C:\rafaelnadal\equipment

Visited: C:\rafaelnadal\grandslam\AustralianOpen

Visited: C:\rafaelnadal\grandslam\RolandGarros

Visited: C:\rafaelnadal\grandslam\USOpen

Visited: C:\rafaelnadal\grandslam\Wimbledon

Visited: C:\rafaelnadal\grandslam

-------------------------------------------------------------

Searched file was found: rafa_1.jpg in C:\rafaelnadal\photos\rafa_1.jpg

通过全局模式搜索文件

有时,您可能只有关于您要搜索的文件的部分信息,例如只有它的名称或扩展名,或者甚至可能只有它的名称或扩展名的一部分。基于这一小段信息,您可以编写一个 glob 模式,如在第四章“通过应用 Glob 模式列出内容”一节中所描述的搜索将在文件存储中定位所有匹配 glob 模式的文件,从结果中您可能能够找到您需要定位的文件。

下面的代码片段在C:\rafaelnadal文件树中搜索所有类型为*.jpg的文件。只有遍历完整个树后,该过程才会停止。

import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; `import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;

class Search implements FileVisitor {

private final PathMatcher matcher;

public Search(String glob) {
        matcher = FileSystems.getDefault().getPathMatcher("glob:" + glob);        
    }

void search(Path file) throws IOException {
        Path name = file.getFileName();
        if (name != null && matcher.matches(name)) {
            System.out.println("Searched file was found: " + name +
                                                     " in " + file.toRealPath().toString());
        }
    }

@Override
    public FileVisitResult postVisitDirectory(Object dir, IOException exc)
                                                                      throws IOException {
        System.out.println("Visited: " + (Path) dir);
        return FileVisitResult.CONTINUE;
    }

@Override
    public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs)
                                                                      throws IOException {
        return FileVisitResult.CONTINUE;
    }

@Override
    public FileVisitResult visitFile(Object file, BasicFileAttributes attrs)
                                                                      throws IOException {
        search((Path) file);
        return FileVisitResult.CONTINUE;
    }

@Override
    public FileVisitResult visitFileFailed(Object file, IOException exc)
                                                                      throws IOException {
        //report an error if necessary
        return FileVisitResult.CONTINUE;
    }
}

class Main {

public static void main(String[] args) throws IOException {         String glob = "*.jpg";
        Path fileTree = Paths.get("C:/rafaelnadal/");
        Search walk = new Search(glob);
        EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);

Files.walkFileTree(fileTree, opts, Integer.MAX_VALUE, walk);

}
}`

输出片段显示了找到的文件:


Searched file was found: rafa_1.jpg in C:\rafaelnadal\photos\rafa_1.jpg

Searched file was found: rafa_winner.jpg in C:\rafaelnadal\photos\rafa_winner.jpg

…

如果您有关于要查找的文件的附加信息,那么您可以创建一个更复杂的搜索。例如,除了关于文件名和类型的小块信息之外,也许您知道文件大小小于某个千字节数,或者也许您知道诸如文件创建时间、文件最后修改时间、文件是隐藏的还是只读的,或者谁拥有它。附加信息可能是文件属性的一部分,如下面的代码片段所示,该代码片段将*.jpg glob 模式与小于 100KB 的文件大小相结合(您可能知道,大小是一个基本属性):

`import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;

class Search implements FileVisitor {

private final PathMatcher matcher;
    private final long accepted_size;

public Search(String glob, long accepted_size) {
        matcher = FileSystems.getDefault().getPathMatcher("glob:" + glob);
        this.accepted_size = accepted_size;        
    }

void search(Path file) throws IOException {
        Path name = file.getFileName();         long size = (Long) Files.getAttribute(file, "basic:size");

if (name != null && matcher.matches(name) && size <= accepted_size) {
            System.out.println("Searched file was found: " + name + " in " +
                                  file.toRealPath().toString() + " size (bytes):" + size);
        }
    }

@Override
    public FileVisitResult postVisitDirectory(Object dir, IOException exc)
                                                                      throws IOException {
        System.out.println("Visited: " + (Path) dir);
        return FileVisitResult.CONTINUE;
    }

@Override
    public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs)
                                                                      throws IOException {
        return FileVisitResult.CONTINUE;
    }

@Override
    public FileVisitResult visitFile(Object file, BasicFileAttributes attrs)
                                                                      throws IOException {
        search((Path) file);
        return FileVisitResult.CONTINUE;
    }

@Override
    public FileVisitResult visitFileFailed(Object file, IOException exc)
                                                                      throws IOException {
        //report an error if necessary
        return FileVisitResult.CONTINUE;
    }
}

class Main {

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

String glob = "*.jpg";
        long size = 102400; //100 kilobytes in bytes
        Path fileTree = Paths.get("C:/rafaelnadal/");
        Search walk = new Search(glob, size);
        EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);

Files.walkFileTree(fileTree, opts, Integer.MAX_VALUE, walk);      
    }
}`

以下是找到的文件输出的片段:


Searched file was found: rafa_winner.jpg in C:\rafaelnadal\photos\rafa_winner.jpg size (bytes):77718

…

按内容搜索文件

其中一种高级文件搜索包括按内容查找文件。您传递一系列单词或句子,搜索只返回包含该文本的文件。这是最耗时的文件搜索任务,因为它需要在每个访问过的文件中搜索文本,这意味着打开文件,阅读文件,最后关闭文件。此外,还有许多支持文本的文件格式,如 PDF、Microsoft Word、Excel 和 PowerPoint、简单文本文件、XML、HTML、XHTML 等等。每种格式都有不同的读取方式,这需要能够从中提取文本文件的专用代码。

在这一节中,我们将开发一个根据内容搜索文件的应用。要搜索的文本作为包含由逗号分隔的单词或句子序列的String传递;比如:“拉斐尔·纳达尔,网球,罗兰·加洛斯冠军,巴黎银行锦标赛平局”。使用StringTokenizer类,逗号作为分隔符,下面的示例将每个单词和句子提取到一个ArrayList中:

…
String words="Rafael Nadal,tennis,winner of Roland Garros,BNP Paribas tournament draws";
ArrayList<String> wordsarray = new ArrayList<>();
…
StringTokenizer st = new StringTokenizer(words, ",");
while (st.hasMoreTokens()) {
       wordsarray.add(st.nextToken());
}

下面的代码循环这个ArrayList,并将每个单词和句子与从被访问文件中提取的文本进行比较。注意在searchText()方法中,提取的文本作为参数传递。

//search text
private boolean searchText(String text) {

   boolean flag = false;
   for (int j = 0; j < wordsarray.size(); j++) {
        if ((text.toLowerCase()).contains(wordsarray.get(j).toLowerCase())) {
            flag = true;
            break;
       }
   }
   return flag;
}

下面的小节集中在从一些最常见的文件格式中提取文本并进行比较的一组方法。因为我们不打算在这里重新发明轮子,我们将利用一些专门为理解特定文件格式而编写的第三方库。然后我们将把我们开发的每一种方法组合成一个完整的搜索程序。

在 pdf 中搜索

为了阅读 PDF 文件,我们将使用两个最流行的第三方开源库,iText 和 Apache PDFBox。您可以从[itextpdf.com/](http://itextpdf.com/)下载 iText 库,从[pdfbox.apache.org/](http://pdfbox.apache.org/)下载 PDFBox 库。出于本章的目的,我使用了 iText 的 5.1.2 版本和 PDFBox 的 1.6.0 版本。基于 iText 文档,我编写了下面的方法来从 PDF 中提取文本。第一步是在被访问的文件上创建一个PdfReader。继续提取 PDF 的页数,从每一页提取文本,并将提取的文本传递给searchText()方法。如果在提取的文本中找到其中一个标记,那么在当前文件中的搜索将停止,该文件将被视为有效的搜索结果,其路径和名称将被存储,以便我们可以在整个搜索结束后将其打印出来。

//search in PDF files using iText library
boolean searchInPDF_iText(String file) {

   PdfReader reader = null;
   boolean flag = false;

   try {
       reader = new PdfReader(file);
       int n = reader.getNumberOfPages();

       OUTERMOST:
       for (int i = 1; i <= n; i++) {
         String str = PdfTextExtractor.getTextFromPage(reader, i);

         flag = searchText(str);
         if (flag) {
               break OUTERMOST;
         }
       }

   } catch (Exception e) {
      } finally {
          if (reader != null) {
              reader.close();
            }
            return flag;
      }
}

如果你对 PDFBox 比对 iText 更熟悉,那么试试下面的方法。首先在 PDF 文件上创建一个PDFParser,然后提取页数,最后提取每页的文本并将其传递给searchText()方法。

`boolean searchInPDF_PDFBox(String file) {

PDFParser parser = null;
   String parsedText = null;    PDFTextStripper pdfStripper = null;
   PDDocument pdDoc = null;
   COSDocument cosDoc = null;
   boolean flag = false;
   int page = 0;

File pdf = new File(file);

try {
       parser = new PDFParser(new FileInputStream(pdf));
       parser.parse();

cosDoc = parser.getDocument();
       pdfStripper = new PDFTextStripper();
       pdDoc = new PDDocument(cosDoc);

OUTERMOST:
       while (page < pdDoc.getNumberOfPages()) {
            page++;
            pdfStripper.setStartPage(page);
            pdfStripper.setEndPage(page + 1);
            parsedText = pdfStripper.getText(pdDoc);

flag = searchText(parsedText);
            if (flag) {
                 break OUTERMOST;
            }
       }
   } catch (Exception e) {
   } finally {
          try {
             if (cosDoc != null) {
                  cosDoc.close();
             }
             if (pdDoc != null) {
                  pdDoc.close();
             }
          } catch (Exception e) {}
   return flag;
   }
}`

在 Microsoft Word、Excel 和 PowerPoint 文件中搜索

Microsoft Office 套件的文件可以通过 Apache POI 库进行操作,Apache POI 库是 Microsoft 文档最常用的 Java API。你可以从[poi.apache.org/](http://poi.apache.org/)下载这个库。出于本章的目的,我使用了 3.7 版本。根据开发人员指南,我编写了以下从 Word 文档中提取文本的方法。Apache POI 提取一个包含 Word 文档所有段落的数组String。数组可以循环,每一段都可以传递给searchText()方法。

boolean searchInWord(String file) {

   POIFSFileSystem fs = null;
   boolean flag = false;

   try {
       fs = new POIFSFileSystem(new FileInputStream(file));

       HWPFDocument doc = new HWPFDocument(fs);
       WordExtractor we = new WordExtractor(doc);
       String[] paragraphs = we.getParagraphText();

       OUTERMOST:
       for (int i = 0; i < paragraphs.length; i++) {

             flag = searchText(paragraphs[i]);
             if (flag) {
                   break OUTERMOST;
             }
       }

     } catch (Exception e) {
     } finally {
            return flag;
     }
}

我们可以从 Excel 文件中提取文本,如下例所示。在为 Excel 文档创建了一个HSSFWorkbook之后,基本思想是遍历工作表,然后遍历行,最后遍历单元格。单元格应该包含我们要查找的特定文本。

`boolean searchInExcel(String file) {

Row row;
   Cell cell;
   String text;
   boolean flag = false;
   InputStream xls = null;

try {
       xls = new FileInputStream(file);
       HSSFWorkbook wb = new HSSFWorkbook(xls);

int sheets = wb.getNumberOfSheets();

OUTERMOST:
       for (int i = 0; i < sheets; i++) {
            HSSFSheet sheet = wb.getSheetAt(i);

Iterator row_iterator = sheet.rowIterator();
            while (row_iterator.hasNext()) {
                row = (Row) row_iterator.next();                 Iterator cell_iterator = row.cellIterator();
                while (cell_iterator.hasNext()) {
                    cell = cell_iterator.next();
                    int type = cell.getCellType();
                    if (type == HSSFCell.CELL_TYPE_STRING) {
                          text = cell.getStringCellValue();
                          flag = searchText(text);
                          if (flag) {
                                break OUTERMOST;
                          }
                    }
                }
            }
       }

} catch (IOException e) {
   } finally {
         try {
             if (xls != null) {
                    xls.close();
             }
         } catch (IOException e) {}
   return flag;
   }
}`

最后,我们可以从 PowerPoint 文件中提取文本,如下例所示;每张幻灯片可能包含文本和注释:

`boolean searchInPPT(String file) {

boolean flag = false;
   InputStream fis = null;
   String text;

try {
       fis = new FileInputStream(new File(file));
       POIFSFileSystem fs = new POIFSFileSystem(fis);
       HSLFSlideShow show = new HSLFSlideShow(fs);

SlideShow ss = new SlideShow(show);
       Slide[] slides = ss.getSlides();

OUTERMOST:
       for (int i = 0; i < slides.length; i++) {

TextRun[] runs = slides[i].getTextRuns();
          for (int j = 0; j < runs.length; j++) {
             TextRun run = runs[j];
             if (run.getRunType() == TextHeaderAtom.TITLE_TYPE) {
                 text = run.getText();
             } else {                  text = run.getRunType() + " " + run.getText();
             }

flag = searchText(text);
             if (flag) {
                    break OUTERMOST;
             }
          }

Notes notes = slides[i].getNotesSheet();
       if (notes != null) {
           runs = notes.getTextRuns();
           for (int j = 0; j < runs.length; j++) {
                text = runs[j].getText();
                flag = searchText(text);
                if (flag) {
                      break OUTERMOST;
                }
           }
       }
     }
   } catch (IOException e) {
   } finally {
         try {
           if (fis != null) {
                 fis.close();
             }
          } catch (IOException e) {}
   return flag;
   }
}`

Image 注意我任意选择了前面例子中使用的第三方库。还有许多其他的开源和商业库可以用来处理不同种类的文档。请随意使用任何方便您需求的东西。我们的搜索示例并不是最有效的搜索方式。在最坏的情况下,我们必须遍历整个数组(典型情况下是数组的一半)。也许使用 Apache Lucene ( [lucene.apache.org/java/docs/index.html](http://lucene.apache.org/java/docs/index.html))提供的索引搜索是更好的方法。这是一个你可以自己尝试的练习。

在文本文件中搜索

文本文件(.txt.html.xml等)。)不需要第三方库。可以使用纯 NIO.2 代码读取它们,如下所示:

boolean searchInText(Path file) { `boolean flag = false;
   Charset charset = Charset.forName("UTF-8");
   try (BufferedReader reader = Files.newBufferedReader(file, charset)) {
        String line = null;

OUTERMOST:
        while ((line = reader.readLine()) != null) {
              flag = searchText(line);
              if (flag) {
                   break OUTERMOST;
              }
        }

} catch (IOException e) {
    } finally {
          return flag;
    }
}`

编写一个完整的搜索程序

没错。馅饼做好了!把它扔进烤箱!我们有搜索的文本、从一组常见文件格式中提取的文本,以及检查提取的文本是否包含搜索的文本的方法。将所有东西放入遍历过程,应用就准备好了:

import com.itextpdf.text.pdf.PdfReader; import com.itextpdf.text.pdf.parser.PdfTextExtractor; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.nio.file.FileSystems; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.EnumSet; import java.util.Iterator; import java.util.StringTokenizer; import org.apache.pdfbox.cos.COSDocument; import org.apache.pdfbox.pdfparser.PDFParser; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.util.PDFTextStripper; import org.apache.poi.hslf.HSLFSlideShow; import org.apache.poi.hslf.model.Notes; import org.apache.poi.hslf.model.Slide; `import org.apache.poi.hslf.model.TextRun;
import org.apache.poi.hslf.record.TextHeaderAtom;
import org.apache.poi.hslf.usermodel.SlideShow;
import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.extractor.WordExtractor;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;

class Search implements FileVisitor {

ArrayList wordsarray = new ArrayList<>();
    ArrayList documents = new ArrayList<>();
    boolean found = false;

public Search(String words) {
        wordsarray.clear();
        documents.clear();

StringTokenizer st = new StringTokenizer(words, ",");
        while (st.hasMoreTokens()) {
            wordsarray.add(st.nextToken().trim());
        }
    }

void search(Path file) throws IOException {

found = false;

String name = file.getFileName().toString();
        int mid = name.lastIndexOf(".");
        String ext = name.substring(mid + 1, name.length());

if (ext.equalsIgnoreCase("pdf")) {
            found = searchInPDF_iText(file.toString());
            if (!found) {
                found = searchInPDF_PDFBox(file.toString());
            }
        }

if (ext.equalsIgnoreCase("doc") || ext.equalsIgnoreCase("docx")) {
            found = searchInWord(file.toString());
        }

if (ext.equalsIgnoreCase("ppt")) {
            searchInPPT(file.toString());
        }

if (ext.equalsIgnoreCase("xls")) { searchInExcel(file.toString());
        }

if ((ext.equalsIgnoreCase("txt")) || (ext.equalsIgnoreCase("xml")
                                          || ext.equalsIgnoreCase("html"))
                || ext.equalsIgnoreCase("htm") || ext.equalsIgnoreCase("xhtml")
                                               || ext.equalsIgnoreCase("rtf")) {
            searchInText(file);
        }

if (found) {
            documents.add(file.toString());
        }
    }

//search in text files
    boolean searchInText(Path file) {

boolean flag = false;
        Charset charset = Charset.forName("UTF-8");
        try (BufferedReader reader = Files.newBufferedReader(file, charset)) {
            String line = null;

OUTERMOST:
            while ((line = reader.readLine()) != null) {
                flag = searchText(line);
                if (flag) {
                    break OUTERMOST;
                }
            }

} catch (IOException e) {
        } finally {
            return flag;
        }
    }

//search in Excel files
    boolean searchInExcel(String file) {

Row row;
        Cell cell;
        String text;
        boolean flag = false;
        InputStream xls = null;

try {
            xls = new FileInputStream(file);
            HSSFWorkbook wb = new HSSFWorkbook(xls);

int sheets = wb.getNumberOfSheets(); OUTERMOST:
            for (int i = 0; i < sheets; i++) {
                HSSFSheet sheet = wb.getSheetAt(i);

Iterator row_iterator = sheet.rowIterator();
                while (row_iterator.hasNext()) {
                    row = (Row) row_iterator.next();
                    Iterator cell_iterator = row.cellIterator();
                    while (cell_iterator.hasNext()) {
                        cell = cell_iterator.next();
                        int type = cell.getCellType();
                        if (type == HSSFCell.CELL_TYPE_STRING) {
                            text = cell.getStringCellValue();
                            flag = searchText(text);
                            if (flag) {
                                break OUTERMOST;
                            }
                        }
                    }
                }
            }

} catch (IOException e) {
        } finally {
            try {
                if (xls != null) {
                    xls.close();
                }
            } catch (IOException e) {
            }
            return flag;
        }
    }

//search in PowerPoint files
    boolean searchInPPT(String file) {

boolean flag = false;
        InputStream fis = null;
        String text;

try {
            fis = new FileInputStream(new File(file));
            POIFSFileSystem fs = new POIFSFileSystem(fis);
            HSLFSlideShow show = new HSLFSlideShow(fs);

SlideShow ss = new SlideShow(show);
            Slide[] slides = ss.getSlides();

OUTERMOST:
            for (int i = 0; i < slides.length; i++) { TextRun[] runs = slides[i].getTextRuns();
                for (int j = 0; j < runs.length; j++) {
                    TextRun run = runs[j];
                    if (run.getRunType() == TextHeaderAtom.TITLE_TYPE) {
                        text = run.getText();
                    } else {
                        text = run.getRunType() + " " + run.getText();
                    }

flag = searchText(text);
                    if (flag) {
                        break OUTERMOST;
                    }

}

Notes notes = slides[i].getNotesSheet();
                if (notes != null) {
                    runs = notes.getTextRuns();
                    for (int j = 0; j < runs.length; j++) {
                        text = runs[j].getText();
                        flag = searchText(text);
                        if (flag) {
                            break OUTERMOST;
                        }
                    }
                }
            }

} catch (IOException e) {
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
            }
            return flag;
        }

}

//search in Word files
    boolean searchInWord(String file) {

POIFSFileSystem fs = null;
        boolean flag = false;

try {
            fs = new POIFSFileSystem(new FileInputStream(file));

HWPFDocument doc = new HWPFDocument(fs); WordExtractor we = new WordExtractor(doc);
            String[] paragraphs = we.getParagraphText();

OUTERMOST:
            for (int i = 0; i < paragraphs.length; i++) {

flag = searchText(paragraphs[i]);
                if (flag) {
                    break OUTERMOST;
                }
            } } catch (Exception e) {
        } finally {
            return flag;
        }
    }

//search in PDF files using PDFBox library
    boolean searchInPDF_PDFBox(String file) {

PDFParser parser = null;
        String parsedText = null;
        PDFTextStripper pdfStripper = null;
        PDDocument pdDoc = null;
        COSDocument cosDoc = null;
        boolean flag = false;
        int page = 0;

File pdf = new File(file);

try {
            parser = new PDFParser(new FileInputStream(pdf));
            parser.parse();

cosDoc = parser.getDocument();
            pdfStripper = new PDFTextStripper();
            pdDoc = new PDDocument(cosDoc);

OUTERMOST:
            while (page < pdDoc.getNumberOfPages()) {
                page++;
                pdfStripper.setStartPage(page);
                pdfStripper.setEndPage(page + 1);
                parsedText = pdfStripper.getText(pdDoc);

flag = searchText(parsedText);
                if (flag) {
                    break OUTERMOST;
                }
            }

} catch (Exception e) {
        } finally {
            try {
                if (cosDoc != null) {
                    cosDoc.close();
                }
                if (pdDoc != null) {
                    pdDoc.close();
                }
            } catch (Exception e) {
            }
            return flag;
        }
    }

//search in PDF files using iText library
    boolean searchInPDF_iText(String file) {

PdfReader reader = null;
        boolean flag = false;

try {
            reader = new PdfReader(file);
            int n = reader.getNumberOfPages();

OUTERMOST:
            for (int i = 1; i <= n; i++) {
                String str = PdfTextExtractor.getTextFromPage(reader, i);

flag = searchText(str);
                if (flag) {
                    break OUTERMOST;
                }
            }

} catch (Exception e) {
        } finally {
            if (reader != null) {
                reader.close();
            }
            return flag;
        }

}

//search text
    private boolean searchText(String text) {

boolean flag = false;
        for (int j = 0; j < wordsarray.size(); j++) {
            if ((text.toLowerCase()).contains(wordsarray.get(j).toLowerCase())) {
                flag = true; break;
            }
        }

return flag;
    }

@Override
    public FileVisitResult postVisitDirectory(Object dir, IOException exc)
                                                                      throws IOException {
        System.out.println("Visited: " + (Path) dir);
        return FileVisitResult.CONTINUE;
    }

@Override
    public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs)
                                                                      throws IOException {
        return FileVisitResult.CONTINUE;
    }

@Override
    public FileVisitResult visitFile(Object file, BasicFileAttributes attrs)
                                                                      throws IOException {
        search((Path) file);
        return FileVisitResult.CONTINUE;
    }

@Override
    public FileVisitResult visitFileFailed(Object file, IOException exc)
                                                                      throws IOException {
        //report an error if necessary

return FileVisitResult.CONTINUE;
    }
}

class Main {

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

String words = "Rafael Nadal, tennis, winner of Roland Garros, BNP Paribas tournament draws";
 Search walk = new Search(words);
 EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);

Iterable dirs = FileSystems.getDefault().getRootDirectories();
 for (Path root : dirs) {
      Files.walkFileTree(root, opts, Integer.MAX_VALUE, walk);
 }

System.out.println("");
 for(String path_string: walk.documents){
     System.out.println(path_string);  }
 System.out.println("
");

}
}`

请注意,有时这是一个相当慢的过程,可能需要几秒到几十分钟,运行时间会因文件树大小、检查的文件数量以及这些文件的大小而异。在前面的示例中,文件树包含默认文件系统中的所有文件存储,因此对于我们的搜索词集,将以任何受支持的格式打开、读取和浏览每个文件。根据匹配文件的大小和数量,在返回结果时,该过程可能会出现几秒钟的阻塞。您可以通过添加更多的文件格式、指示进程状态的进度条或标志以及加速进程的多线程来改进该应用。此外,显示找到的文件名可能比存储文件名和路径更好。

编写文件删除应用

删除单个文件是一个简单的操作,正如你在第四章中看到的“删除文件和目录”在您调用了delete()deleteIfExists()方法之后,该文件将从您的文件系统中删除。删除整个文件树是基于通过一个FileVisitor实现递归调用delete()deleteIfExists()方法的操作。在看到示例之前,您需要记住以下几点:

  • 删除目录之前,必须删除其中的所有文件。
  • visitFile()方法是执行每个文件删除的最佳位置。
  • 因为只有当目录为空时才能删除,所以建议使用postVisitDirectory()方法删除目录。
  • 如果一个文件不能被访问,visitFileFailed()方法应该返回FileVisitResult.CONTINUETERMINATE,这取决于您的决定。
  • 删除过程可以遵循符号链接,这可能是不可取的,因为符号链接可能会指向删除域之外的文件。但是如果你确定这种情况永远不会发生,或者一个补充条件阻止了不期望的删除,那么就跟随符号链接。

我们在这一节的目标是创建一个删除整个文件树的应用。以下代码删除了C:\rafaelnadal目录(为了进一步使用,请在运行以下代码之前备份该目录):

import java.io.IOException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet; `class DeleteDirectory implements FileVisitor {

boolean deleteFileByFile(Path file) throws IOException {
    return Files.deleteIfExists(file);
 }

@Override
 public FileVisitResult postVisitDirectory(Object dir, IOException exc)
                                                                  throws IOException {

if (exc == null) {
        System.out.println("Visited: " + (Path) dir);
        boolean success = deleteFileByFile((Path) dir);

if (success) {
             System.out.println("Deleted: " + (Path) dir);
        } else {
              System.out.println("Not deleted: " + (Path) dir);
        }
    } else {
        throw exc;
    }
    return FileVisitResult.CONTINUE;
 }

@Override
 public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs)
                                                                  throws IOException {
   return FileVisitResult.CONTINUE;
 }

@Override
 public FileVisitResult visitFile(Object file, BasicFileAttributes attrs)
                                                                  throws IOException {
   boolean success = deleteFileByFile((Path) file);

if (success) {
        System.out.println("Deleted: " + (Path) file);
   } else {
        System.out.println("Not deleted: " + (Path) file);
   }

return FileVisitResult.CONTINUE;
 }

@Override
 public FileVisitResult visitFileFailed(Object file, IOException exc)
                                                                  throws IOException {
   //report an error if necessary

return FileVisitResult.CONTINUE;
 } }

class Main {

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

Path directory = Paths.get("C:/rafaelnadal");
   DeleteDirectory walk = new DeleteDirectory();
   EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);

Files.walkFileTree(directory, opts, Integer.MAX_VALUE, walk);      
   }
}`

Image 注意将删除的文件发送到回收站可以通过使用 JNI 调用 Windows API SHFileOperation()方法来完成。查看大卫·谢伊在 www.jroller.com/ethdsy/entry/send_to_recycle_bin 的帖子了解更多细节。

编写复制文件应用

复制文件树需要为每个遍历的文件和目录调用Files.copy()方法。(有关在 NIO.2 中复制文件或目录的详细信息,请参考第四章的“复制文件和目录”一节)在你看到一个例子之前,这里有一些要点要记住:

  • 在从目录中复制任何文件之前,您必须复制目录本身。复制源目录(空或不空)将导致一个空的目标目录。该任务必须在preVisitDirectory()方法中完成。
  • visitFile()方法是复制每个文件的最佳地方。
  • 当您复制文件或目录时,您需要决定是否要使用REPLACE_EXISTINGCOPY_ATTRIBUTES选项。
  • 如果您想保留源目录的属性,您需要在文件被复制之后,在postVisitDirectory()方法中这样做。
  • 如果你选择跟随链接(FOLLOW_LINKS)并且你的文件树有一个到父目录的循环链接,循环目录在visitFileFailed()方法中被报告,带有FileSystemLoopException异常。
  • 如果一个文件不能被访问,visitFileFailed()方法应该返回FileVisitResult.CONTINUETERMINATE,这取决于您的决定。
  • 如果您指定了FOLLOW_LINKS选项,复制过程可以遵循符号链接。

下面的代码片段结合了前面的概念,并将the C:\rafaelnadal子树复制到C:\rafaelnadal_copy文件树中:

import java.nio.file.FileSystemLoopException; `import java.nio.file.attribute.FileTime;
import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;

class CopyTree implements FileVisitor {

private final Path copyFrom;
   private final Path copyTo;

public CopyTree(Path copyFrom, Path copyTo) {
        this.copyFrom = copyFrom;
        this.copyTo = copyTo;
    }

static void copySubTree(Path copyFrom, Path copyTo) throws IOException {
        try {
            Files.copy(copyFrom, copyTo, REPLACE_EXISTING, COPY_ATTRIBUTES);
        } catch (IOException e) {
            System.err.println("Unable to copy " + copyFrom + " [" + e + "]");
        }

}

@Override
   public FileVisitResult postVisitDirectory(Object dir, IOException exc)
                                                                     throws IOException {
        if (exc == null) {
            Path newdir = copyTo.resolve(copyFrom.relativize((Path) dir));
            try {
                FileTime time = Files.getLastModifiedTime((Path) dir);
                Files.setLastModifiedTime(newdir, time);
            } catch (IOException e) {
                System.err.println("Unable to copy all attributes to: " + newdir+" ["+e+ "]");
            }
        } else {
            throw exc;
        }

return FileVisitResult.CONTINUE;
    }

@Override
    public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs)
                                                                      throws IOException {         System.out.println("Copy directory: " + (Path) dir);
        Path newdir = copyTo.resolve(copyFrom.relativize((Path) dir));
        try {
            Files.copy((Path) dir, newdir, REPLACE_EXISTING, COPY_ATTRIBUTES);
        } catch (IOException e) {
            System.err.println("Unable to create " + newdir + " [" + e + "]");
            return FileVisitResult.SKIP_SUBTREE;
        }

return FileVisitResult.CONTINUE;
    }

@Override
    public FileVisitResult visitFile(Object file, BasicFileAttributes attrs)
                                                                      throws IOException {
        System.out.println("Copy file: " + (Path) file);
        copySubTree((Path) file, copyTo.resolve(copyFrom.relativize((Path) file)));
        return FileVisitResult.CONTINUE;
    }

@Override
    public FileVisitResult visitFileFailed(Object file, IOException exc)
                                                                     throws IOException {
        if (exc instanceof FileSystemLoopException) {
            System.err.println("Cycle was detected: " + (Path) file);
        } else {
            System.err.println("Error occurred, unable to copy:" +(Path) file+" ["+ exc + "]");
        }

return FileVisitResult.CONTINUE;
    }
}

class Main {

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

Path copyFrom = Paths.get("C:/rafaelnadal");
        Path copyTo = Paths.get("C:/rafaelnadal_copy");

CopyTree walk = new CopyTree(copyFrom, copyTo);
        EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);

Files.walkFileTree(copyFrom, opts, Integer.MAX_VALUE, walk);
    }
}`

运行前面的应用后,您会发现一个与C:\rafaelnadal源具有相同内容和属性的C:\rafaelnadal_copy目标。

编写一个移动文件应用

移动文件树是将复制和删除文件树的步骤组合到单个应用中的任务。(有关移动文件的更多详细信息,请参考第四章的“移动文件和目录”一节。)实际上,移动文件树通常有两种方法:组合使用Files.move()Files.copy()Files.delete(),或者只使用Files.copy()Files.delete()。根据您选择的方法,应该相应地执行FileVisitor来完成移动文件树的任务。在看到示例之前,您需要记住以下几点:

  • 在从目录中移动任何文件之前,您必须移动目录本身。由于不能移动非空目录(只能移动空目录),您需要使用Files.copy()方法,它将复制一个空目录。该任务必须在preVisitDirectory()方法中完成。
  • visitFile()方法是移动每个文件的最佳位置。为此,您可以使用Files.move()方法,或者将Files.copy()Files.delete()结合使用。
  • 当一个源目录中的所有文件都被移动到目标目录中后,你需要调用Files.delete()来删除源目录,此时源目录应该是空的。该任务必须在postVisitDirectory()方法中完成。
  • 当您复制文件或目录时,您需要决定是否要使用REPLACE_EXISTINGCOPY_ATTRIBUTES选项。此外,当你移动一个文件或目录时,你需要决定是否需要ATOMIC_MOVE
  • 如果您想保留源目录的属性,您需要在文件被移动之后,在postVisitDirectory()方法中这样做。一些属性,如lastModifiedTime,应该在preVisitDirectory()方法中提取并存储,直到它们在postVisitDirectory()中被设置。原因是从源目录移动文件后,目录内容发生了变化,初始的最后修改时间被新日期覆盖。
  • 如果一个文件不能被访问,visitFileFailed()方法应该返回FileVisitResult.CONTINUETERMINATE,这取决于您的决定。
  • 如果指定了FOLLOW_LINKS选项,移动过程可以遵循符号链接。请记住,移动符号链接会移动链接本身,而不是链接的目标。

下面的代码片段将C:\rafaelnadal目录的内容移动到C:\ATP\players\rafaelnafal目录中(在测试之前,您必须手动创建文件夹C:\ATP\players\)。在这种情况下,使用Files.copy()Files.delete()移动目录和子目录,使用Files.move()移动文件。

import java.io.IOException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; `import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.EnumSet;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;

class MoveTree implements FileVisitor {

private final Path moveFrom;
   private final Path moveTo;
   static FileTime time = null;

public MoveTree(Path moveFrom, Path moveTo) {
        this.moveFrom = moveFrom;
        this.moveTo = moveTo;
   }

static void moveSubTree(Path moveFrom, Path moveTo) throws IOException {
        try {
            Files.move(moveFrom, moveTo, REPLACE_EXISTING, ATOMIC_MOVE);
        } catch (IOException e) {
            System.err.println("Unable to move " + moveFrom + " [" + e + "]");
        }

}

@Override
   public FileVisitResult postVisitDirectory(Object dir, IOException exc)
                                                                     throws IOException {
        Path newdir = moveTo.resolve(moveFrom.relativize((Path) dir));
        try {
            Files.setLastModifiedTime(newdir, time);
            Files.delete((Path) dir);
        } catch (IOException e) {
            System.err.println("Unable to copy all attributes to: " + newdir+" [" + e + "]");
        }

return FileVisitResult.CONTINUE;
   }

@Override
   public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs)
                                                                        throws IOException {
        System.out.println("Move directory: " + (Path) dir);
        Path newdir = moveTo.resolve(moveFrom.relativize((Path) dir));
        try {
            Files.copy((Path) dir, newdir, REPLACE_EXISTING, COPY_ATTRIBUTES);
            time = Files.getLastModifiedTime((Path) dir);
        } catch (IOException e) {             System.err.println("Unable to move " + newdir + " [" + e + "]");
            return FileVisitResult.SKIP_SUBTREE;
        }

return FileVisitResult.CONTINUE;
   }

@Override
   public FileVisitResult visitFile(Object file, BasicFileAttributes attrs)
                                                                        throws IOException {
        System.out.println("Move file: " + (Path) file);
        moveSubTree((Path) file, moveTo.resolve(moveFrom.relativize((Path) file)));
        return FileVisitResult.CONTINUE;
   }

@Override
   public FileVisitResult visitFileFailed(Object file, IOException exc)
                                                                        throws IOException {
        return FileVisitResult.CONTINUE;
   }
}

class Main {

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

Path moveFrom = Paths.get("C:/rafaelnadal");
        Path moveTo = Paths.get("C:/ATP/players/rafaelnadal");

MoveTree walk = new MoveTree(moveFrom, moveTo);
        EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);

Files.walkFileTree(moveFrom, opts, Integer.MAX_VALUE, walk);
    }
}`

您可以不使用Files.move()来完成同样的任务,因为每次移动都只是一对复制和删除操作。例如,您可以重写moveSubTree()方法来使用Files.copy()Files.delete()来移动文件:

static void moveSubTree(Path moveFrom, Path moveTo) throws IOException {
        try {
            Files.copy(moveFrom, moveTo, REPLACE_EXISTING, COPY_ATTRIBUTES);
            Files.delete(moveFrom);
        } catch (IOException e) {
            System.err.println("Unable to move " + moveFrom + " [" + e + "]");
        }
    }

总结

本章着重于开发文件和目录的递归操作。在简单介绍了递归编程技术之后,您了解了FileVisitor接口和SimpleFileVisitor实现。然后,您看到了如何开发一组应用,您可以使用它们来执行涉及遍历文件树的任务,例如查找、复制、删除和移动文件。

六、监视服务 API

监视服务 API 是在 Java 7 (NIO.2)中作为线程安全服务引入的,它能够监视对象的变化和事件。最常见的用途是通过创建、删除和修改等操作来监视目录内容的变化。您可能已经多次看到这种服务的效果。例如,当你在编辑器中打开一个文本文件(如 GridinSoft 记事本、jEdit 等。)并且文件内容在编辑器外被修改,您将看到一条消息,询问您是否要重新加载该文件,因为它已被修改。这意味着编辑器已经通过监视服务检测到文件更改,并相应地报告它。这就是所谓的文件更改通知机制,从 NIO.2 开始,它可以通过 Watch Service API 获得。

Watch Service API 是一个低级 API,可以按原样使用,也可以进行定制。你甚至可以在它上面写一个高级 API。默认情况下,这个 API 使用底层文件系统功能来监视文件系统的变化。它允许您注册一个(或多个)目录,以便针对您在注册期间指定的不同类型的通知事件进行监控。当观察器服务检测到一个或多个已注册的通知事件时,观察器服务将通知事件传递给已注册的进程,以便通过单独的线程或线程池来处理它们。

Image 注意从 NIO.2 开始,您不再需要轮询文件系统的变化或者使用其他内部解决方案来监控文件系统的变化。在以前的 Java 版本中,您必须实现一个在单独线程中运行的代理,该代理跟踪被监视目录的所有内容,不断轮询文件系统以查看是否发生了任何重要的事情。现在,无论您运行的是 Mac OS X、Linux、Unix、Windows 还是其他操作系统,您都可以保证底层操作系统和文件系统提供所需的功能,允许 Java 注册以接收文件系统更改的通知。

在本章中,您将看到如何基于提供的 Watch Service API 开发应用。实现一个功能应用并不容易,所以我们将从最简单的情况开始,应用监视单个目录的变化。之后,您将看到如何递归地监视您已经注册要被监视的目录树。此外,我们将开发另外两个不太通用的应用,它们封装了现实生活中的案例。为了帮助您入门,本章提供了编写基于 Watch Service API 的应用所涉及的主要类的概述。

监视服务 API 类

java.nio.file.WatchService接口是这个 API 的起点。对于不同的文件系统和操作系统,它有多种实现。您可以使用这个接口和三个类来开发一个具有文件系统监视功能的系统。下面的项目符号概述了这些类:

  • 可观察对象:如果一个对象代表了一个实现了java.nio.file.Watchable接口的类的实例,那么这个对象就是“可观察的”。在我们的例子中,这是 NIO 2 最重要的类,也就是众所周知的Path类。
  • 事件类型:这是我们感兴趣监控的事件列表。只有在 register 调用中指定了事件时,事件才会触发通知。标准支持的事件由java.nio.file.StandardWatchEventKinds类表示,包括创建、删除和修改。这个类实现了WatchEvent.Kind<T>接口。
  • 事件修饰符:这限定了Watchable如何向WatchService注册。在撰写本文时,NIO.2 还没有定义任何标准修饰符。
  • 守望者:守望者观看监视!在我们的例子中,观察者是WatchService,它监视文件系统的变化(文件系统是一个FileSystem实例)。正如您将看到的,WatchService将通过FileSystem类创建。它会在后台静静地看着注册的Path

实现监视服务

实现监视服务是一项需要完成一系列步骤的任务。在本节中,您将看到开发监视服务的主要步骤,该服务监视给定目录的三个通知事件:删除、创建和修改。每一步都有一段代码支持,演示了如何实际完成该步骤。最后,我们将把这些块粘合在一起,形成一个完整的监视服务功能示例。

创建监视服务

我们从创建一个用于监控文件系统的WatchService开始我们的旅程。为此我们称之为FileSystem.newWatchService()方法:

WatchService watchService = FileSystems.getDefault().newWatchService();

我们现在有一个监视服务供我们使用。

向监视服务注册对象

应该被监视的每个对象都必须向监视服务显式注册。我们可以注册任何实现了Watchable接口的对象。对于我们的例子,我们将注册作为Path类实例的目录。除了被监视的对象之外,注册过程还需要服务应该监视和通知的事件的标识。受支持的事件类型在StandardWatchEventKinds类下映射为Kind<Path>类型的常量:

  • StandardWatchEventKinds.ENTRY_CREATE:创建一个目录条目。当文件被重命名或移动到这个目录中时,也会触发一个ENTRY_CREATE事件。
  • StandardWatchEventKinds.ENTRY_DELETE:删除一个目录条目。当文件被重命名或移出该目录时,也会触发一个ENTRY_DELETE事件。
  • StandardWatchEventKinds.ENTRY_MODIFY:修改一个目录条目。哪些事件构成修改在某种程度上是特定于平台的,但是实际上修改文件的内容总是会触发一个修改事件。在某些平台上,更改文件属性也会触发此事件。
  • StandardWatchEventKinds.OVERFLOW:表示事件可能已经丢失或被丢弃。您不必注册参加OVERFLOW活动就能收到它。

因为Path类实现了Watchable接口,所以它提供了Watchable.register()方法。有两种这样的方法专用于向 watch 服务注册对象。其中一个接收两个参数,表示该对象要注册到的监视服务和该对象应该注册到的事件。第二个 register 方法也接收这两个参数,第三个参数指定限定目录注册方式的修饰符。在撰写本文时,NIO.2 没有提供任何标准的修饰符。

下面的代码片段向观察服务注册了Path C:\rafaelnadal(被监视的事件将被创建、删除和修改):

import static java.nio.file.StandardWatchEventKinds.*;
…
final Path path = Paths.get("C:/rafaelnadal");
WatchService watchService = FileSystems.getDefault().newWatchService();
…
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
              StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
…
watchService.close();
…

您注册的每个目录都会收到一个WatchKey实例;这是一个用WatchService表示可观察对象注册的令牌。您可以选择是否保留这个引用,因为当事件被触发时,WatchService会将相关的WatchKey返回给您。下一节将提供更多关于监视钥匙的详细信息。

等待到来的事件

等待到来的事件需要一个无限循环。当一个事件发生时,watch 服务负责发送相应的 watch 键信号,并将其放入 watcher 的队列中,我们可以从队列中检索它——我们说 watch 键被排队。因此,我们的无限循环可能是以下类型:

while(true){
//retrieve and process the incoming events
…
}

或者它可以是以下类型:

for(;;){
//retrieve and process the incoming events
…
}

拿到监视钥匙

检索排队的键可以通过调用WatchService类的以下三个方法之一来完成。这三种方法都检索下一个键,并将其从队列中删除。如果没有可用的密钥,它们的响应会有所不同,如下所述:

  • poll():如果没有可用的键,则立即返回一个null值。
  • poll(long, TimeUnit):如果没有可用的按键,它会等待指定的时间,然后再次尝试。如果仍然没有可用的密钥,那么它返回null。时间段用一个long数字表示,而TimeUnit参数决定指定的时间是分钟、秒、毫秒还是其他时间单位。
  • take():如果没有可用的密钥,它将一直等待,直到一个密钥被排队或者无限循环由于任何不同的原因而停止。

以下三段代码向您展示了在无限循环中调用的每一种方法:

`//poll method, without arguments
while (true) {
       //retrieve and remove the next watch key
       final WatchKey key = watchService.poll();
       //the thread flow gets here immediately with an available key or a null value

}

//poll method, with arguments
while (true) {
       //retrieve and remove the next watch key
       final WatchKey key = watchService.poll(10, TimeUnit.SECONDS);
       //the thread flow gets here immediately if a key is available, or after 10 seconds
       //with an available key or null value

} //take method
while (true) {
       //retrieve and remove the next watch key
       final WatchKey key = watchService.take();
       //the thread flow gets here immediately if a key is available, or it will wait until a    
       //key is available, or the loop breaks

}`

请记住,键总是有一个状态,可以是就绪、有信号或无效:

  • 就绪:刚创建的时候,一个按键处于就绪状态,这意味着它已经准备好接受事件了。
  • 有信号:当一个键处于有信号状态时,意味着至少有一个事件已经发生,并且这个键已经被排队,所以它可以被poll()take()方法检索。(这类似于钓鱼:关键是浮子,事件是鱼。当你有鱼上钩时,浮标(钥匙)会向你发出信号,让你把鱼线拉出水面。)一旦发出信号,键就保持在这个状态,直到调用它的reset()方法将键返回到就绪状态。如果其他事件在该键被发出信号时发生,它们会被排队,而不会对该键本身重新排队(钓鱼时不会发生这种情况)。
  • 无效:当一个按键处于无效状态时,意味着它不再有效。一个键保持有效,直到通过显式调用cancel()方法取消它,目录变得不可访问,或者 watch 服务被关闭。您可以通过调用WatchKey.isValid()方法来测试一个键是否有效,该方法将返回一个相应的布尔值。

Image 注意监视键对于多个并发线程来说是安全的。

检索关键字的未决事件

当键被发出信号时,我们有一个或多个未决事件等待我们采取行动。我们可以通过调用WatchKey.pollEvents()方法来检索和移除特定监视键的所有未决事件。它不获取任何参数,并返回一个包含检索到的未决事件的List。我们可以迭代这个List来单独提取和处理每个未决事件。List类型为WatchEvent<T>,代表用WatchService注册的对象的事件(或重复事件):

public List<WatchEvent<?>> pollEvents()

Image 注意如果没有未决事件,pollEvents()方法不会等待,这有时可能会导致空的List

下面的代码片段迭代我们的键的未决事件:

…
while (true) {
      //retrieve and remove the next watch key
      final WatchKey key = watchService.take();

      //get list of pending events for the watch key
      for (WatchEvent<?> watchEvent : key.pollEvents()) {
…
      }
      …
}
…

Image 注意观察事件是不可变的和线程安全的。

检索事件类型和计数

WatchEvent<T>接口映射事件属性,如类型计数。事件的类型可以通过调用WatchEvent.kind()方法获得,该方法将事件类型作为Kind<T>对象返回。

Image 注意如果忽略注册的事件类型,有可能会收到一个OVERFLOW事件。这种事件可以被忽略,也可以被处理,选择哪个取决于你。

下面的代码片段将列出由pollEvents()方法提供的每个事件的类型:

…
//get list of pending events for the watch key
for (WatchEvent<?> watchEvent : key.pollEvents()) {

     //get the kind of event (create, modify, delete)
     final Kind<?> kind = watchEvent.kind();

     //handle OVERFLOW event
     if (kind == StandardWatchEventKinds.OVERFLOW) {
            continue;
     }

     System.out.println(kind);
}
…

除了事件类型,我们还可以得到事件被观察到的次数(重复事件)。如果我们调用返回一个intWatchEvent.count()方法,这是可能的:

System.out.println(watchEvent.count());

检索与事件相关的文件名

当文件发生删除、创建或修改事件时,我们可以通过获取事件上下文来找出它的名称(文件名作为事件的上下文存储)。这个任务可以通过调用WatchEvent.context()方法来完成:

…
final WatchEvent<Path> watchEventPath = (WatchEvent<Path>) watchEvent;
final Path filename = watchEventPath.context();

System.out.println(filename);
…

将钥匙放回就绪状态

一旦发出信号,键就保持在这个状态,直到调用它的reset()方法将键返回到就绪状态。然后,它继续等待事件。如果监视键有效并已被重置,则reset()方法返回true,如果监视键因不再有效而无法重置,则返回false。在某些情况下,如果密钥不再有效,无限循环应该被打破;例如,如果我们有一个单键,就没有理由停留在无限循环中。

下面是在密钥不再有效时用于中断循环的代码:

…
while(true){
   …
  //reset the key
  boolean valid = key.reset();

  //exit loop if the key is not valid (if the directory was deleted, for example)
  if (!valid) {
          break;
  }
}
…

Image 注意如果您忘记或未能调用reset()方法,该键将不会收到任何进一步的事件!

关闭监视服务

当线程退出或服务关闭时,监视服务也将退出。应该通过显式调用WatchService.close()方法或通过将创建代码放入 try-with-resources 块来关闭它,如下所示:

try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
…
}

当 watch 服务关闭时,任何当前操作都将被取消并失效。在一个观察服务被关闭后,任何对它调用操作的进一步尝试都将抛出ClosedWatchServiceException。如果这个观察服务已经关闭,那么调用这个方法没有任何作用。

把它们粘在一起

在这一节中,我们将前面的所有代码块(包括导入和意大利面条式代码)粘合到一个应用中,该应用监视路径C:\rafaelnadal的创建、删除和修改事件,并报告事件的类型和发生事件的文件。出于测试目的,请尝试手动添加、删除或修改该路径下的文件或目录。请记住,只有一个级别被监控(只有C:\rafaelnadal目录),没有目录下的整个目录树。

应用代码如下:

`package watch_01;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;

class WatchRafaelNadal {

public void watchRNDir(Path path) throws IOException, InterruptedException {
        try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
            path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
                  StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);

//start an infinite loop
            while (true) {

//retrieve and remove the next watch key
                final WatchKey key = watchService.take();

//get list of pending events for the watch key
                for (WatchEvent watchEvent : key.pollEvents()) {` `//get the kind of event (create, modify, delete)                     final Kind kind = watchEvent.kind();

//handle OVERFLOW event
                    if (kind == StandardWatchEventKinds.OVERFLOW) {
                        continue;
                    }

//get the filename for the event
                    final WatchEvent watchEventPath = (WatchEvent) watchEvent;
                    final Path filename = watchEventPath.context();

//print it out
                    System.out.println(kind + " -> " + filename);
                }

//reset the key
                boolean valid = key.reset();

//exit loop if the key is not valid (if the directory was deleted, for example)
                if (!valid) {
                    break;
                }
            }
        }
    }
}

public class Main {

public static void main(String[] args) {

final Path path = Paths.get("C:/rafaelnadal");
        WatchRafaelNadal watch = new WatchRafaelNadal();

try {
            watch.watchRNDir(path);
        } catch (IOException | InterruptedException ex) {
            System.err.println(ex);
        }

}
}`

由于该应用包含一个无限循环,请小心手动停止该应用,或者实现一个停止机制。该应用是作为 NetBeans 项目提供的,因此您可以从“输出”窗口轻松地停止它,不需要任何补充代码。

使用监视服务的其他例子

在本节中,我们将“玩”前面的应用,编写一些场景来探索监视服务的可能性。我们将在此基础上构建新的应用,以完成涉及监视服务的更复杂的任务。与上一节一样,在每个步骤的描述之后,提供了支持该步骤的代码块。在完整描述了这些步骤之后,我们将把所有的东西整合到一个完整的应用中。

Image 注意为了保持代码尽可能的干净,我们将跳过变量的声明(它们的名称与前面的应用中的相同)和应该重复的代码。

看目录树

首先,我们将开发一个应用,扩展前面的例子来观察整个C:\rafaelnadal目录树。此外,如果一个CREATE事件在这棵树的某个地方创建了一个新目录,它将立即被注册,就好像它从一开始就在那里一样。

首先,创建一个观察服务:

private WatchService watchService = FileSystems.getDefault().newWatchService();

接下来,我们需要为创建、删除和修改事件注册目录树。这比在原始应用中更棘手,因为我们需要注册C:\rafaelnadal的每个子目录,而不仅仅是这个目录。因此,我们需要一次遍历(参见第五章)来遍历每个子目录,并在监视服务中单独注册。这种情况非常适合通过扩展SimpleFileVisitor类来实现遍历,因为我们只需要在目录被预先访问时参与进来(此外,您可能希望覆盖visitFileFailed()方法来显式处理意外的遍历错误)。为此,我们将创建一个名为registerTree()的方法,如下所示:

private void registerTree(Path start) throws IOException {

  Files.walkFileTree(start, new SimpleFileVisitor<Path>() {

     @Override
     public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                                                                          throws IOException {
        System.out.println("Registering:" + dir);
        registerPath(dir);
        return FileVisitResult.CONTINUE;
        }
   });
}

如您所见,这里没有注册。对于每个遍历的目录,该代码调用另一个名为registerPath()的方法,该方法将接收到的路径注册到 watch 服务,如下所示:

private void registerPath(Path path) throws IOException {

  //register the received path
  WatchKey key = path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
                  StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
    }

此时,初始的C:\rafaelnadal目录和所有子目录被注册用于创建、删除和修改事件。

接下来,我们将关注将“捕获”这些事件的无限循环。当一个事件发生时,我们特别感兴趣的是它是否是一个CREATE事件,因为它可能表示已经创建了一个新的子目录,在这种情况下,我们有责任通过调用具有相应路径的registerTree()方法将这个子目录添加到观察服务进程中。这里我们需要解决的问题是,我们不知道哪个键已经排队,所以我们不知道应该通过哪个路径进行注册。解决方案可能是将键和相应的路径保存在一个HashMap中,该路径在每次注册时在registerPath()方法中更新,如下所示,在此之后,当事件发生时,我们可以从哈希映射中提取相关的键:

private final Map<WatchKey, Path> directories = new HashMap<>();
…
private void registerPath(Path path) throws IOException {
  //register the received path
  WatchKey key = path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,  
                  StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);

  //store the key and path
  directories.put(key, path);
}

现在,在无限循环中,我们可以注册任何新的子目录,如下所示:

…
while (true) {
  …
  if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
      final Path directory_path = directories.get(key);
      final Path child = directory_path.resolve(filename);

      if (Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) {
        registerTree(child);
      }
  }
…
}
…

HashMap也可用于在没有有效密钥时停止无限循环。为了实现这一点,当一个键无效时,它被从HashMap中移除,当HashMap为空时,循环被中断:

…
while (true) {
  …
  //reset the key
  boolean valid = key.reset();

  //remove the key if it is not valid
  if (!valid) {
      directories.remove(key);

      if (directories.isEmpty()) {
         break;
      }
  }
}
…

就这样!现在,让我们把所有的东西放在一个镜头里:

`import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;

class WatchRecursiveRafaelNadal {

private WatchService watchService;
    private final Map<WatchKey, Path> directories = new HashMap<>();

private void registerPath(Path path) throws IOException {
        //register the received path
        WatchKey key = path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
                  StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);

//store the key and path
        directories.put(key, path);
    } private void registerTree(Path start) throws IOException {

Files.walkFileTree(start, new SimpleFileVisitor() {

@Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                    throws IOException {
                System.out.println("Registering:" + dir);
                registerPath(dir);
                return FileVisitResult.CONTINUE;
            }
        });

}

public void watchRNDir(Path start) throws IOException, InterruptedException {

watchService = FileSystems.getDefault().newWatchService();

registerTree(start);

//start an infinite loop
        while (true) {

//retrieve and remove the next watch key
            final WatchKey key = watchService.take();

//get list of events for the watch key
            for (WatchEvent<?> watchEvent : key.pollEvents()) {

//get the kind of event (create, modify, delete)
                final Kind<?> kind = watchEvent.kind();

//get the filename for the event
                final WatchEvent watchEventPath = (WatchEvent) watchEvent;
                final Path filename = watchEventPath.context();

//handle OVERFLOW event
                if (kind == StandardWatchEventKinds.OVERFLOW) {
                    continue;
                }

//handle CREATE event
                if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
                    final Path directory_path = directories.get(key);
                    final Path child = directory_path.resolve(filename);

if (Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) {
                        registerTree(child);
                    }
                } //print it out
                System.out.println(kind + " -> " + filename);
            }

//reset the key
            boolean valid = key.reset();

//remove the key if it is not valid
            if (!valid) {
                directories.remove(key);

//there are no more keys registered
                if (directories.isEmpty()) {
                    break;
                }
            }
        }
        watchService.close();
    }
}

public class Main {

public static void main(String[] args) {

final Path path = Paths.get("C:/rafaelnadal");
        WatchRecursiveRafaelNadal watch = new WatchRecursiveRafaelNadal();

try {
            watch.watchRNDir(path);
        } catch (IOException | InterruptedException ex) {
            System.err.println(ex);
        }

}
}`

出于测试目的,尝试创建新的子目录和文件,修改它们,然后删除它们。同时,注意控制台输出,看看事件是如何报告的。以下是将名为rafa_champ.jpg的新图片添加到C:\rafaelnadal\photos目录并在几秒钟后将其删除的示例输出:


Registering:C:\rafaelnadal

Registering:C:\rafaelnadal\equipment

Registering:C:\rafaelnadal\grandslam

Registering:C:\rafaelnadal\grandslam\AustralianOpen


Registering:C:\rafaelnadal\grandslam\RolandGarros

Registering:C:\rafaelnadal\grandslam\USOpen

…

Registering:C:\rafaelnadal\wiki

ENTRY_CREATE -> rafa_champ.jpg

ENTRY_MODIFY -> rafa_champ.jpg

ENTRY_MODIFY -> photos

ENTRY_MODIFY -> rafa_champ.jpg

ENTRY_DELETE -> rafa_champ.jpg

ENTRY_MODIFY -> photos

看摄像机

对于这个场景,假设我们有一个监控摄像机,它每 10 秒钟至少捕捉一个图像,并以 JPG 格式将其发送到计算机目录。在幕后,控制器负责检查相机是否按时以正确的 JPG 格式发送图像捕捉。如果摄像机工作不正常,它会显示一条警告消息。

由于 Watch Service API,这个场景可以很容易地在代码行中重现。我们对编写监视摄像机的控制器特别感兴趣。由于摄像机将捕获的图像发送到一个目录,我们的控制器可以观察这个目录中的CREATE事件。本例中的目录是C:\security(您应该手动创建它),它被path变量映射为一个Path:

…
final Path path = Paths.get("C:/security");
…
WatchService watchService = FileSystems.getDefault().newWatchService();
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
…

接下来,我们知道摄像机每 10 秒发送一次图像,这意味着poll(long, TimeUnit)方法应该非常适合监控这一点(请记住,如果在指定的时间段内发生了事件,此方法将退出,返回相关的WatchKey)。我们将其设置为正好等待 11 秒,如果在这段时间内没有创建新的捕获,我们将通过消息报告这一情况并停止系统:

while (true) {      final WatchKey key = watchService.poll(11, TimeUnit.SECONDS);    if (key == null) {     System.out.println("The video camera is jammed - security watch system is canceled!");     break;    } else {    …    } } …

最后,如果我们有一个新的捕获可用,那么我们需要做的就是检查它是否是 JPG 图像格式。为此,我们可以使用来自Files类的助手方法,名为probeContentType(),它探测文件的内容类型。我们传递文件,它以 MIME 的形式返回null或内容类型。对于 JPG 图像,该方法应该返回image/jpeg

…
OUTERMOST:
while (true) {
  …
  if (kind == StandardWatchEventKinds.ENTRY_CREATE) {

    //get the filename for the event
    final WatchEvent<Path> watchEventPath = (WatchEvent<Path>) watchEvent;
    final Path filename = watchEventPath.context();
    final Path child = path.resolve(filename);

    if (Files.probeContentType(child).equals("image/jpeg")) {

       //print out the video capture time
       SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MMM-dd HH:mm:ss");
       System.out.println("Video capture successfully at: " + dateFormat.format(new Date()));
    } else {
       System.out.println("The video camera capture format failed! This could be a virus!");
       break OUTERMOST;
    }
  }
}
…

我们已经完成了编写控制器的主要任务,所以现在我们需要做的就是填充缺失的代码(导入、声明、主函数等)。)向我们提供完整的申请,如下所示:

import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchEvent.Kind; import java.nio.file.WatchKey; import java.nio.file.WatchService; `import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

class SecurityWatch {

WatchService watchService;

private void register(Path path, Kind kind) throws IOException {
        //register the directory with the watchService for Kind event    
        path.register(watchService, kind);
    }

public void watchVideoCamera(Path path) throws IOException, InterruptedException {

watchService = FileSystems.getDefault().newWatchService();
        register(path, StandardWatchEventKinds.ENTRY_CREATE);

//start an infinite loop
        OUTERMOST:
        while (true) {

//retrieve and remove the next watch key
            final WatchKey key = watchService.poll(11, TimeUnit.SECONDS);

if (key == null) {
                System.out.println("The video camera is jammed - security watch system is
                                                                                  canceled!");
                break;
            } else {

//get list of events for the watch key
                for (WatchEvent<?> watchEvent : key.pollEvents()) {

//get the kind of event (create, modify, delete)
                    final Kind<?> kind = watchEvent.kind();

//handle OVERFLOW event
                    if (kind == StandardWatchEventKinds.OVERFLOW) {
                        continue;
                    }

if (kind == StandardWatchEventKinds.ENTRY_CREATE) {

//get the filename for the event
                        final WatchEvent watchEventPath = (WatchEvent) watchEvent;
                        final Path filename = watchEventPath.context();
                        final Path child = path.resolve(filename);

if (Files.probeContentType(child).equals("image/jpeg")) {

//print out the video capture time                             SimpleDateFormat dateFormat = new
                                             SimpleDateFormat("yyyy-MMM-dd HH:mm:ss");
                            System.out.println("Video capture successfully at: " +
                                             dateFormat.format(new Date()));
                        } else {
                            System.out.println("The video camera capture format failed!
                                                                     This could be a virus!");
                            break OUTERMOST;
                        }
                    }
                }

//reset the key
                boolean valid = key.reset();

//exit loop if the key is not valid
                if (!valid) {
                    break;
                }
            }
        }

watchService.close();
    }
}

public class Main {

public static void main(String[] args) {

final Path path = Paths.get("C:/security");
        SecurityWatch watch = new SecurityWatch();

try {
            watch.watchVideoCamera(path);
        } catch (IOException | InterruptedException ex) {
            System.err.println(ex);
        }

}
}`

出于测试的目的,您可能需要编写一个测试器类,或者更简单地扮演摄像机的角色。只需启动应用,在关键时刻到来之前复制并粘贴C:\security中的 JPG 图像。尝试不同的情况,例如使用错误的文件格式,在复制另一个图像之前等待超过 11 秒,等等。

观察打印机托盘系统

在本节中,我们将开发一个应用来监控大规模的打印机托盘。假设我们有一个多线程基类,它接收要打印的文档,并根据一种旨在优化打印机使用的算法将它们分派给一套网络打印机——打印线程在相应的文档打印完毕后终止。该类的实现方式如下:

import java.nio.file.Path;
import java.util.Random;

class Print implements Runnable {

    private Path doc;

    Print(Path doc) {
        this.doc = doc;
    }

    @Override
    public void run() {
        try {
            //sleep a random number of seconds for simulating dispatching and printing            
            Thread.sleep(20000 + new Random().nextInt(30000));
            System.out.println("Printing: " + doc);
        } catch (InterruptedException ex) {
            System.err.println(ex);
        }
    }
}

Image 注意 Java 7 推荐使用新的ThreadLocalRandom类在多线程情况下生成随机数。但是我更喜欢老的Random类,因为新类好像有 bug 它在多个线程上生成相同的数字。如果在你读这本书的时候这个错误已经被解决了,那么你可能想用下面这句话来代替:ThreadLocalRandom.current().nextInt(20000, 50000);

现在,打印机从一个由目录(C:\printertray,您需要手动创建)表示的公共托盘中“进给”。我们的工作是实现一个监视服务来管理这个托盘。当一个新文档到达托盘时,我们必须将它传递给Print类,在一个文档被打印后,我们必须将它从托盘中删除。

我们首先通过传统方法获得一个监视服务,并为CREATEDELETE事件注册C:\printertray目录:

…
final Path path = Paths.get("C:/printertray");
…
WatchService watchService = FileSystems.getDefault().newWatchService();
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
                                                        StandardWatchEventKinds.ENTRY_DELETE);
…

接下来,当一个新文档到达托盘时,我们必须创建一个新的Print线程,并存储该线程和文档路径,以便进一步跟踪线程状态。这将有助于我们了解文档何时已经打印,因此应该从托盘中删除并移除以进行存储(我们使用HashMap来完成此任务)。下面的代码片段包含当一个新文档到达托盘时执行的代码块(一个CREATE事件被排队):

private final Map<Thread, Path> threads = new HashMap<>();
…
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {

     System.out.println("Sending the document to print -> " + filename);

     Runnable task = new Print(path.resolve(filename));
     Thread worker = new Thread(task);

     //we can set the name of the thread
     worker.setName(path.resolve(filename).toString());

     //store the thread and the path
     threads.put(worker, path.resolve(filename));

     //start the thread, never call method run() direct
     worker.start();
}
…

从托盘中删除一个文档后(一个DELETE事件被排队),我们只打印一条消息:

…
if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
    System.out.println("Document " + filename + " was successfully printed!");
}
…

但是文档是什么时候删除的呢?为了解决这个任务,我们使用了一个小技巧。我们不使用take()方法来等待键排队,而是使用poll(long, TimeUnit)方法,这将在指定的时间间隔内给我们无限循环中的控制权——当我们拥有控制权时(不管是否有键排队),我们可以循环线程的HashMap,以查看是否有任何打印作业已经终止(相关的线程状态是TERMINATED)。每一个TERMINATED状态之后都将删除相关联的路径并移除HashMap条目。当路径被删除时,一个DELETE事件将被排队。以下代码向您展示了如何实现这一点:

…
if (!threads.isEmpty()) {
    for (Iterator<Map.Entry<Thread, Path>> it = threads.entrySet().iterator(); it.hasNext();)
         Map.Entry<Thread, Path> entry = it.next();                        
         if (entry.getKey().getState() == Thread.State.TERMINATED) {
             Files.deleteIfExists(entry.getValue());
             it.remove();
          }
    }
}
…

现在,将所有东西放在一起,获得完整的应用:

`import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;

class Print implements Runnable {

private Path doc;

Print(Path doc) {
        this.doc = doc;
    }

@Override
    public void run() {
        try {
            //sleep a random number of seconds for simulating dispatching and printing            
            Thread.sleep(20000 + new Random().nextInt(30000));
            System.out.println("Printing: " + doc);
        } catch (InterruptedException ex) {
            System.err.println(ex);
        }
    }
}

class WatchPrinterTray {

private final Map<Thread, Path> threads = new HashMap<>();

public void watchTray(Path path) throws IOException, InterruptedException {
        try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
            path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
                                                        StandardWatchEventKinds.ENTRY_DELETE);

//start an infinite loop
            while (true) {

//retrieve and remove the next watch key
                final WatchKey key = watchService.poll(10, TimeUnit.SECONDS); //get list of events for the watch key
                if (key != null) {
                    for (WatchEvent<?> watchEvent : key.pollEvents()) {

//get the filename for the event
                        final WatchEvent watchEventPath = (WatchEvent) watchEvent;
                        final Path filename = watchEventPath.context();

//get the kind of event (create, modify, delete)
                        final Kind<?> kind = watchEvent.kind();

//handle OVERFLOW event
                        if (kind == StandardWatchEventKinds.OVERFLOW) {
                            continue;
                        }

if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
                            System.out.println("Sending the document to print ->" + filename);

Runnable task = new Print(path.resolve(filename));
                            Thread worker = new Thread(task);

//we can set the name of the thread
                            worker.setName(path.resolve(filename).toString());

//store the thread and the path
                            threads.put(worker, path.resolve(filename));

//start the thread, never call method run() direct
                            worker.start();
                        }

if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
                            System.out.println(filename + " was successfully printed!");
                        }
                    }

//reset the key
                    boolean valid = key.reset();

//exit loop if the key is not valid
                    if (!valid) {
                        threads.clear();
                        break;
                    }
                }

if (!threads.isEmpty()) {
                    for (Iterator<Map.Entry<Thread, Path>> it = threads.entrySet().iterator();  
                                                                              it.hasNext()😉 {
                        Map.Entry<Thread, Path> entry = it.next();                         if (entry.getKey().getState() == Thread.State.TERMINATED) {
                            Files.deleteIfExists(entry.getValue());
                            it.remove();
                        }
                    }
                }
            }
        }
    }
}

public class Main {

public static void main(String[] args) {

final Path path = Paths.get("C:/printertray");
        WatchPrinterTray watch = new WatchPrinterTray();

try {
            watch.watchTray(path);
        } catch (IOException | InterruptedException ex) {
            System.err.println(ex);
        }

}
}`

出于测试目的,启动应用并将一组文件复制到C:\printertray目录中。例如,以下是使用一组文件进行测试的输出:


Sending the document to print -> rafa_1.jpg

Sending the document to print -> AEGON.txt

Sending the document to print -> BNP.txt

Printing: C:\printertray\rafa_1.jpg

Printing: C:\printertray\AEGON.txt

rafa_1.jpg was successfully printed!

AEGON.txt was successfully printed!

Printing: C:\printertray\BNP.txt

Sending the document to print -> rafa_winner.jpg

BNP.txt was successfully printed!


Printing: C:\printertray\rafa_winner.jpg

rafa_winner.jpg was successfully printed

总结

在这一章中,你已经探索了 NIO.2 的一个很好的工具——监视服务 API。您学习了如何监视目录或目录树中的事件,如创建、删除和修改。在概述了这个 API 和一个介绍性的应用之后,您看到了如何将这个 API 与 NIO.2 walks 结合起来,如何模拟摄像机监控,以及如何观察大规模的打印机托盘。这些例子只是为了激发您的好奇心,进一步探索这个 API 的精彩世界。由于它非常通用,因此可以应用于许多其他场景。例如,您可以使用它来更新 GUI 显示中的文件列表,或者检测可以重新加载的配置文件的修改。

七、随机访问文件

在前面的章节中,我们已经按顺序探索了文件。可以顺序浏览的文件称为顺序文件。在本章中,你将看到使用非顺序(随机)访问文件内容的优点。允许随机访问其内容的文件被称为随机访问文件(RAFs )。顺序文件更常用,因为它们易于创建,但是 RAF 更灵活,并且可以更快地找到它们的数据。

使用 RAF,您可以打开文件,查找特定位置,并读取或写入该文件。在你打开 RAF 之后,你可以通过使用一个记录号以随机的方式读取或写入它,或者你可以添加到文件的开头或结尾,因为你知道文件中有多少记录。RAF 允许您读取单个字符、读取一个字节块或一行、替换文件的一部分、添加行、删除行等等,并允许您以随机的方式执行所有这些操作。

Java 7 (NIO.2)引入了一个全新的接口来使用 RAFs。它的名字叫SeekableByteChannel,在java.nio.channels包中有售。它扩展了旧的ByteChannel接口,并代表一个字节通道,该通道保持当前位置并允许修改该位置。此外,Java 7 通过实现这个接口并一次性提供 RAF 和FileChannel功能,改进了众所周知的FileChannel类。通过简单的造型,我们可以将一个SeekableByteChannel转换成一个FileChannel

本章广泛使用了java.nio.ByteBuffer类,所以我们将从它的一个简短概述开始。我们将继续详细介绍SeekableByteChannel与应用的接口,这些应用将随机读写文件以完成不同类型的常见任务。然后,您将看到如何获得具有 RAF 功能的FileChannel,并探索FileChannel提供的主要功能,例如将文件的一个区域直接映射到内存中以实现更快的访问,锁定文件的一个区域,以及在不影响通道当前位置的情况下从绝对位置读取和写入字节。本章结尾是一个基准测试应用,它将帮助您确定使用FileChannel功能复制文件的最快方法,而不是其他常见的方法,如Files.copy(),缓冲流等等。

byte buffer 概述

缓冲区本质上是一个数组(通常是字节数组,但也可以使用其他类型的数组——Buffer接口提供了ByteBufferCharBufferIntBufferShortBufferLongBufferFloatBufferDoubleBuffer,用于保存一些要写入或刚刚读取的数据。

NIO 中缓冲区的两个最重要的组件是属性和祖先方法,下面将依次讨论。

ByteBuffer 属性

以下是缓冲区的基本属性:

  • Limit :当从一个缓冲区写入时,Limit 指定还有多少数据要获取。当你读入一个缓冲区时,这个限制指定了还有多少空间可以存放数据。
  • Position:Position 记录你读了或写了多少数据。它指定下一个字节将进入或来自哪个数组元素。缓冲区的位置永远不会是负的,也永远不会大于它的限制。
  • 容量:容量指定一个缓冲区可以存储的最大数据量。限制永远不能大于容量。

Image 注意作为不变量,这三个性质尊重以下关系:0 ≤位置≤极限≤容量。

例如,假设一个缓冲器有 6 字节的容量,如图 7-1 所示。

Image

图 7-1。 Java 缓冲区表示(一)

在起点,极限和容量相等(极限不能大于容量,但反过来是完全正常的),并被设置为一个虚拟槽(在我们的例子中,槽号为 7),如图图 7-2 所示。

Image

图 7-2 Java 缓冲区表示(b)

Image 注意在某些情况下,初始限制可能是 0,也可能是其他值,这取决于缓冲区的类型及其构造方式。

同样,在起始点,位置被设置为 0(槽 1,如图 7-3 中的所示)——一个读或写字节将访问位置 0。

Image

图 7-3 Java 缓冲区表示(c)

接下来,假设我们将 2 个字节的数据读入缓冲区。这 2 个字节的数据从位置 0 开始进入缓冲区。因此,前两个字节被填充,位置转到第三个字节,如图图 7-4 所示。

Image

图 7-4 Java 缓冲区表示(d)

继续第二次读取,另外 3 个字节进入缓冲区。位置增加到 5(槽 6),如图图 7-5 所示。

Image

图 7-5 Java 缓冲区表示(e)

此时,假设我们不再读入缓冲区,而是想从缓冲区写入。为此,我们首先需要在写入任何字节之前调用flip()方法。这会将限制设置到当前位置,并将位置设置为 0。翻转后,缓冲器出现如图图 7-6 所示。

Image

图 7-6 Java 缓冲区表示(f)

假设我们从缓冲区写入 3 个字节。由于位置为 0,前 3 个字节被写入,位置移动到 3(槽 4),如图图 7-7 所示。限制和容量保持不变。

Image

图 7-7 Java 缓冲区表示(g)

接下来我们再写 2 个字节,位置前移至槽 6,如图图 7-8;限制和容量保持不变。

Image

图 7-8 Java 缓冲区表示(h)

我们可能还想完成另外两个操作。继续参照图 7-8 ,我们可能想要倒带缓冲区或清除缓冲区。倒带缓冲区(调用rewind()方法)将为重新读取缓冲区已经包含的数据做准备——限制保持不变,位置设置为 0。清空缓冲区(调用clear()方法)将重置缓冲区以接收更多字节(数据不会被删除)—限制设置为容量,位置设置为 0。图 7-9 显示了clear()方法的效果,图 7-10 显示了rewind()方法的效果。

Image

图 7-9 Java 缓冲区表示(一)

Image

图 7-10 Java 缓冲区表示(j)

此外,一个缓冲区保存一个标记。这是调用reset()方法时其位置将被重置的索引。标记并不总是确定的,但它永远不会是负数,也永远不会大于位置。如果标记被定义,那么当位置或限制被调整到小于标记的值时,它被丢弃。如果标记没有定义,那么调用reset()方法会导致抛出一个InvalidMarkException

Image 将标记插入关系中得到以下结果:0 ≤标记≤位置≤极限≤容量。

ByteBuffer 祖先方法

ByteBuffer提供了一套访问数据的get()put()方法。由于它们非常直观,我将在此简单列出。更多详情,请查阅[download.oracle.com/javase/7/docs/api/index.html](http://download.oracle.com/javase/7/docs/api/index.html)[download.oracle.com/javase/7/docs/index.html](http://download.oracle.com/javase/7/docs/index.html)的官方文件。

public abstract byte get()
public ByteBuffer get(byte[] dst)
public ByteBuffer get(byte[] dst, int offset, int length)
public abstract byte get(int index)

public abstract ByteBuffer put(byte b)
public final ByteBuffer put(byte[] src)
public ByteBuffer put(byte[] src, int offset, int length)
public ByteBuffer put(ByteBuffer src)
public abstract ByteBuffer put(int index, byte b)

除了get()put()方法,ByteBuffer还有额外的方法来读取和写入不同类型的值,如下所示:

public abstract char getChar()
public abstract char getChar(int index)
public abstract double getDouble()
public abstract double getDouble(int index)
public abstract float getFloat()
public abstract float getFloat(int index)
public abstract int getInt()
public abstract int getInt(int index)
public abstract long getLong()
public abstract long getLong(int index)
public abstract short getShort()
public abstract short getShort(int index)

public abstract ByteBuffer putChar(char value)
public abstract ByteBuffer putChar(int index, char value)
public abstract ByteBuffer putDouble(double value)
public abstract ByteBuffer putDouble(int index, double value)
public abstract ByteBuffer putFloat(float value)
public abstract ByteBuffer putFloat(int index, float value)
public abstract ByteBuffer putInt(int value)
public abstract ByteBuffer putInt(int index, int value)
public abstract ByteBuffer putLong(int index, long value)
public abstract ByteBuffer putLong(long value)
public abstract ByteBuffer putShort(int index, short value)
public abstract ByteBuffer putShort(short value)

一个字节缓冲区可以是直接的,也可以是非直接的。JVM 将在直接缓冲区上执行本机 I/O 操作。直接缓冲区通过使用allocateDirect()方法创建,而非直接缓冲区通过使用allocate()方法创建。

此时你已经有了足够的关于ByteBuffer的信息来理解下面的应用。(要深入了解ByteBuffer,请访问网上的专门教程。)因此,我们暂时抛开ByteBuffer,进入本章的主题SeekableByteChannel界面。下一节将向您介绍通道,并将它们与缓冲区联系起来。

渠道概述

面向流的 I/O 系统中,输入流产生 1 字节的数据,输出流消耗 1 字节的数据——这样的系统通常相当慢。相比之下,在一个面向块的 I/O 系统中,输入/输出流一步就产生或消耗一个数据块。

通道类似于流,但有一些不同:

  • 虽然流通常是单向的(读或写),但通道支持读和写。
  • 通道可以异步读写。
  • 通道总是读取或写入缓冲区。发送到通道的所有数据必须首先放在缓冲区中。从通道读取的任何数据都被读入缓冲区。

使用 SeekableByteChannel 接口对文件进行随机访问

新的SeekableByteChannel接口通过实现通道上位置的概念来提供对 RAF 的支持。我们可以从通道中读取或向通道中写入一个ByteBuffer,获取或设置当前位置,并将连接到通道的实体截断到指定的维度。以下方法与这些功能相关(更多详情可在[download.oracle.com/javase/7/docs/api/index.html](http://download.oracle.com/javase/7/docs/api/index.html)的官方文件中获得):

  • position():返回通道的当前位置(非负)。
  • position(long):将通道的位置设置为指定的long(非负)。将位置设置为大于当前大小的值是合法的,但不会改变实体的大小。
  • truncate(long):将连接到通道的实体截断到指定的长度。
  • read(ByteBuffer):从通道读取字节到缓冲区。
  • write(ByteBuffer):将字节从缓冲区写入通道。
  • size():返回该通道连接的实体的当前大小。

获取SeekableByteChannel的实例可以通过Files类的两个方法完成,名为newByteChannel()。第一个(最简单的)newByteChannel()方法接收要打开或创建的文件的路径和一组指定如何打开文件的选项。StandardOpenOption枚举常量在第四章章节“使用标准打开选项”中有所描述,但为了便于参考,在此重复这些常量:

Image

第二个newByteChannel()方法接收要打开或创建的文件的路径,一组指定如何打开文件的选项,以及可选的,创建文件时自动设置的文件属性列表。

这两种方法都打开或创建一个文件,返回SeekableByteChannel来访问该文件。

使用 SeekableByteChannel 读取文件

关注第一个newByteChannel()方法,我们得到一个用于读取路径C:\rafaelnadal\grandslam\RolandGarros\story.txtSeekableByteChannel(文件必须存在):

… Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt"); … try (SeekableByteChannel seekableByteChannel = Files.newByteChannel(path,                                                          EnumSet.of(StandardOpenOption.READ))) { … } catch (IOException ex) {    System.err.println(ex); }

例如,下面的应用将使用一个ByteBuffer读取并显示story.txt的内容(该文件必须存在)。我选择了 12 字节的缓冲区,但也可以随意使用其他大小的缓冲区。

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
import java.nio.file.StandardOpenOption;

public class Main {

 public static void main(String[] args) {

  Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");

  //read a file using SeekableByteChannel
  try (SeekableByteChannel seekableByteChannel = Files.newByteChannel(path,
                                                       EnumSet.of(StandardOpenOption.READ))) {

   ByteBuffer buffer = ByteBuffer.allocate(12);            
   String encoding = System.getProperty("file.encoding");
   buffer.clear();

   while (seekableByteChannel.read(buffer) > 0) {
         buffer.flip();
         System.out.print(Charset.forName(encoding).decode(buffer));
         buffer.clear();                
   }
  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}

输出应该类似于以下内容:


Rafa Nadal produced another masterclass of clay-court tennis to win his fifth French Open
title ...

用 SeekableByteChannel 写文件

SeekableByteChannel写文件需要使用WRITE选项。此外,如果我们想在写作前清理现有的内容,我们可以添加如下的TRUNCATE_EXISTING选项。在这里,我们截断了story.txt,并准备写它(?? 文件必须存在)。

…
Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");
…
try (SeekableByteChannel seekableByteChannel = Files.newByteChannel(path,
                EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING))) {
…
} catch (IOException ex) {
   System.err.println(ex);
}

例如,下面的应用将使用一个ByteBuffer截断并在story.txt中写入一些文本(在这种情况下,文件已经存在;如果它不存在,那么我们将添加CREATECREATE_NEWWRITE选项,并去掉TRUNCATE_EXISTING选项,因为文件反正是空的)。

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;

public class Main {

 public static void main(String[] args) {

   Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");

   //write a file using SeekableByteChannel
   try (SeekableByteChannel seekableByteChannel = Files.newByteChannel(path,
                EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING))) {

    ByteBuffer buffer = ByteBuffer.wrap("Rafa Nadal produced another masterclass of clay-court
                                   tennis to win his fifth French Open title ...".getBytes());

    int write = seekableByteChannel.write(buffer);
    System.out.println("Number of written bytes: " + write);

    buffer.clear();
   } catch (IOException ex) {
     System.err.println(ex);
   }
 }
}

当你写一个文件时,有一些常见的情况涉及到组合打开选项:

  • 要写入一个存在的文件,在开始时,使用WRITE
  • 要写入一个存在的文件,在最后,使用WRITEAPPEND
  • 要写入一个存在的文件并在写入前清理其内容,使用WRITETRUNCATE_EXISTING
  • 要写入一个不存在的文件,使用CREATE(或CREATE_NEW)和WRITE

可查找的字节通道和文件属性

以下代码片段(为 Unix 和其他 POSIX 文件系统编写)创建了一个具有一组特定文件权限的文件。这段代码在home\rafaelnadal\email目录中创建文件email.txt,如果它已经存在,则追加到该文件中。创建的email.txt文件对所有者具有读写权限,对组具有只读权限。

`import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.EnumSet;
import java.util.Set;

public class Main {

public static void main(String[] args) {

Path path = Paths.get("home/rafaelnadal/email", "email.txt");
 ByteBuffer buffer = ByteBuffer.wrap("Hi Rafa, I want to congratulate you for the amazing
                                                      match that you played ... ".getBytes());

//create the custom permissions attribute for the email.txt file
 Set perms = PosixFilePermissions.fromString("rw-r------");
 FileAttribute<Set> attr = PosixFilePermissions.asFileAttribute(perms);

//write a file using SeekableByteChannel
 try (SeekableByteChannel seekableByteChannel = Files.newByteChannel(path,
                    EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.APPEND), attr)) {

int write = seekableByteChannel.write(buffer);
  System.out.println("Number of written bytes: " + write);

} catch (IOException ex) {
   System.err.println(ex);  }

buffer.clear();
 }
}`

使用旧的 ReadableByteChannel 接口读取文件

新的SeekableByteChannel接口基于旧的接口ReadableByteChannel(代表读取字节的通道;一次只能有一个线程读取)和WritableByteChannel(代表一个写字节的通道;一次只能有一个线程可以写),这是从 JDK 1.4 开始在 NIO 中提供的。这两个接口是SeekableByteChannel的超级接口。由于它们之间的这种关系,我们可以使用旧的ReadableByteChannel接口和新的Files.newByteChannel()方法,如下所示,其中我们读取现有的story.txt文件的内容:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Main {

 public static void main(String[] args) {

  Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");

  //read a file using ReadableByteChannel
  try (ReadableByteChannel readableByteChannel = Files.newByteChannel(path)) {

   ByteBuffer buffer = ByteBuffer.allocate(12);
   buffer.clear();

   String encoding = System.getProperty("file.encoding");

   while (readableByteChannel.read(buffer) > 0) {
          buffer.flip();                
          System.out.print(Charset.forName(encoding).decode(buffer));
          buffer.clear();                
   }
  } catch (IOException ex) {
     System.err.println(ex);
  }
 }
}

如您所见,不需要指定READ选项。

用旧的 WritableByteChannel 接口写文件

我们也可以将旧的WritableByteChannel接口与新的Files.newByteChannel()方法结合起来,如下所示,其中我们将一些文本添加到story.txt中:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;
public class Main {

public static void main(String[] args) {

 Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");

 //write a file using WritableByteChannel
 try (WritableByteChannel writableByteChannel = Files.newByteChannel(path,
                           EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.APPEND))) {

  ByteBuffer buffer = ByteBuffer.wrap("Vamos Rafa!".getBytes());

  int write = writableByteChannel.write(buffer);
  System.out.println("Number of written bytes: " + write);

  buffer.clear();

  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}

即使我们使用了一个WritableByteChannel,我们仍然需要显式地指定WRITE选项。APPEND选项是可选的,特定于前面的例子。

玩弄 SeekableByteChannel 的立场

现在您已经知道如何使用SeekableByteChannel读写整个文件,您已经准备好发现如何在指定的通道(实体)位置执行相同的操作。为此,我们将在一组四个例子中利用position()position(long)方法,意在让你熟悉 RAF 概念。请记住,不带参数的position()方法返回当前的通道(实体)位置,而position(long)方法通过计算从通道(实体)开始的字节数来设置通道(实体)的当前位置。第一个位置是 0,最后一个有效位置是通道(实体)大小。

例 1:从不同位置读一个字符

我们从一个简单的例子开始,它从文本文件的第一、中间和最后位置读取一个字符。文件是MovistarOpen.txt,它位于C:\rafaelnadal\tournaments\2009目录中。

`import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;

public class Main {

public static void main(String[] args) {

Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "MovistarOpen.txt");
  ByteBuffer buffer = ByteBuffer.allocate(1);
  String encoding = System.getProperty("file.encoding");

try (SeekableByteChannel seekableByteChannel = (Files.newByteChannel(path,
                                                      EnumSet.of(StandardOpenOption.READ)))) {

//the initial position should be 0 anyway
   seekableByteChannel.position(0);

System.out.println("Reading one character from position: " +
                                                              seekableByteChannel.position());
   seekableByteChannel.read(buffer);
   buffer.flip();
   System.out.print(Charset.forName(encoding).decode(buffer));
   buffer.rewind();

//get into the middle
   seekableByteChannel.position(seekableByteChannel.size()/2);

System.out.println("\nReading one character from position: " +
                                                              seekableByteChannel.position());
   seekableByteChannel.read(buffer);
   buffer.flip();
   System.out.print(Charset.forName(encoding).decode(buffer));
   buffer.rewind();

//get to the end
   seekableByteChannel.position(seekableByteChannel.size()-1);

System.out.println("\nReading one character from position: " +
                                                              seekableByteChannel.position());
   seekableByteChannel.read(buffer);    buffer.flip();
   System.out.print(Charset.forName(encoding).decode(buffer));
   buffer.clear();

} catch (IOException ex) {
   System.err.println(ex);
  }        
 }
}`

前面的应用将产生以下输出:


Reading one character from position: 0

T

Reading one character from position: 181

n

Reading one character from position: 361

.

例 2:在不同的位置书写字符

接下来,我们将尝试写入特定位置。假设MovistarOpen.txt文件有以下默认内容:

The Movistar Open moved to Santiago from Viña del Mar in 2010\. It is the first clay-court
tournament of the ATP World Tour season and also the opening leg of the four-tournament swing
through Latin America, aptly coined the "Golden Swing" in honour of top Chileans and Olympic
Gold medalists Fernando Gonsales and Nicolas Massu. Gonzalez is a four-time champion.

我们希望完成两项任务:首先,在前面的文本末尾添加一些文本,其次,将“Gonsales”替换为“Gonzalez ”,因为 Fernando 的姓在第一个实例中被拼错了。下面是应用:

`import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;

public class Main { public static void main(String[] args) {

Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "MovistarOpen.txt");
  ByteBuffer buffer_1 = ByteBuffer.wrap("Great players participate in our tournament, like:
              Tommy Robredo, Fernando Gonzalez, Jose Acasuso or Thomaz Bellucci.".getBytes());
  ByteBuffer buffer_2 = ByteBuffer.wrap("Gonzalez".getBytes());

try (SeekableByteChannel seekableByteChannel = (Files.newByteChannel(path,
                                                     EnumSet.of(StandardOpenOption.WRITE)))) {

//append some text at the end
   seekableByteChannel.position(seekableByteChannel.size());

while (buffer_1.hasRemaining()) {
          seekableByteChannel.write(buffer_1);
   }

//replace "Gonsales" with "Gonzalez"
   seekableByteChannel.position(301);

while (buffer_2.hasRemaining()) {
          seekableByteChannel.write(buffer_2);
   }

buffer_1.clear();
   buffer_2.clear();

} catch (IOException ex) {
    System.err.println(ex);
  }
 }
}`

如果一切正常,新的MovistarOpen.txt内容应该如下所示:

The Movistar Open moved to Santiago from Viña del Mar in 2010\. It is the first clay-court
tournament of the ATP World Tour season and also the opening leg of the four-tournament swing
through Latin America, aptly coined the "Golden Swing" in honour of top Chileans and Olympic
Gold medalists Fernando Gonzalez and Nicolas Massu. Gonzalez is a four-time champion. Great
players participate in our tournament, like: Tommy Robredo, Fernando Gonzalez, Jose Acasuso or
Thomaz Bellucci.
例 3:从头到尾复制一个文件的一部分

转到一个新的应用,我们下一步想把一部分文本从一个文件的开头复制到同一个文件的结尾。例如,我们将使用HeinekenOpen.txt文件(位于C:\rafaelnadal\tournaments\2009目录中),该文件包含以下内容:

The Pride Of New Zealand
The Heineken Open is the biggest men's professional sporting event in New Zealand, held in...

我们想把“新西兰的骄傲”这句话抄在最后,就像这样:

The Pride Of New Zealand
The Heineken Open is the biggest men's professional sporting event in New Zealand, held in...
The Pride Of New Zealand

以下应用完成了这项任务:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;

public class Main {

 public static void main(String[] args) {

  Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "HeinekenOpen.txt");        

  ByteBuffer copy = ByteBuffer.allocate(25);
  copy.put("\n".getBytes());

  try (SeekableByteChannel seekableByteChannel = (Files.newByteChannel(path,
                            EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE)))) {

   int nbytes;
   do {
      nbytes = seekableByteChannel.read(copy);
   } while (nbytes != -1 && copy.hasRemaining());

   copy.flip();            

   seekableByteChannel.position(seekableByteChannel.size());
   while (copy.hasRemaining()) {
          seekableByteChannel.write(copy);
   }

   copy.clear();

  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}
例 4:用截断功能替换文件部分

在本例中,我们将截断一个文件,并在截断的文本中添加新的文本。我们将使用BrasilOpen.txt文件(在C:\rafaelnadal\tournaments\2009目录中找到),该文件包含以下内容:

Brasil Open At Forefront Of Green Movement
The Brasil Open, the second stop of the four-tournament Latin American swing, is held in an
area renowned for its lush natural beauty and stunning beaches. From this point forward ...

我们希望截断文件内容,以删除“从现在开始...”的文本并在其位置追加新文本。以下是解决方案:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;

public class Main {

 public static void main(String[] args) {

  Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BrasilOpen.txt");

  ByteBuffer buffer = ByteBuffer.wrap("The tournament has taken a lead in environmental
conservation efforts, with highlights including the planting of 500 trees to neutralise carbon
emissions and providing recyclable materials to local children for use in craft
work.".getBytes());

  try (SeekableByteChannel seekableByteChannel = (Files.newByteChannel(path,
                            EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE)))) {

   seekableByteChannel.truncate(200);

   seekableByteChannel.position(seekableByteChannel.size()-1);
   while (buffer.hasRemaining()) {
          seekableByteChannel.write(buffer);
   }

   buffer.clear();

  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}

该应用的效果是对BrasilOpen.txt文件的如下修改:

Brasil Open At Forefront Of Green Movement
The Brasil Open, the second stop of the four-tournament Latin American swing, is held in an
area renowned for its lush natural beauty and stunning beaches. The tournament has taken a
lead in environmental conservation efforts, with highlights including the planting of 500
trees to neutralise carbon emissions and providing recyclable materials to local children for
use in craft work.

这一组例子应该有助于您理解如何随机访问文件内容。接下来,我们将把SeekableByteChannel接口转换为FileChannel,让我们可以访问更高级的特性。

使用文件通道

FileChannel是在 Java 4 中引入的,但最近它被更新以实现新的SeekableByteChannel接口,结合它们的力量以实现更大的能力。SeekableByteChannel提供了随机访问文件功能,而FileChannel提供了强大的高级功能,例如将文件的一个区域直接映射到内存中,以便更快地访问和锁定文件的一个区域。

为一个Path获取一个FileChannel可以通过两个新的FileChannel.open()方法来完成。这两种方法都能够为给定的Path打开或创建一个文件,并返回一个新的通道。第一种(最简单的)方法接收要打开或创建的文件的路径,以及一组指定如何打开文件的选项。第二种方法接收要打开或创建的文件的路径、一组指定如何打开文件的选项,以及(可选)在创建文件时自动设置的文件属性列表。

例如,以下代码为指定路径获取具有读/写功能的文件通道:

Path path = Paths.get("…");
…
try (FileChannel fileChannel = (FileChannel.open(path, EnumSet.of(
                                       StandardOpenOption.READ, StandardOpenOption.WRITE)))) {
…
} catch (IOException ex) {
  System.err.println(ex);
}

SeekableByteChannel显式转换为FileChannel可以替代前面的代码:

Path path = Paths.get("…");
…
try (FileChannel fileChannel = (FileChannel)(Files.newByteChannel(path,
                              EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE)))) {
…
} catch (IOException ex) {
  System.err.println(ex);
}

现在,fileChannel实例可以访问由SeekableByteChannelFileChannel提供的方法。

将通道的文件区域直接映射到内存中

最棒的FileChannel功能之一是能够将通道文件的一个区域直接映射到内存中。这要归功于FileChannel.map()方法,它获得以下三个参数:

  • mode:将一个区域映射到内存有三种方式可以完成:MapMode.READ_ONLY ( 只读映射;写尝试会抛出ReadOnlyBufferExceptionMapMode.READ_WRITE ( 读/写映射;结果缓冲区中的变化可以传播到文件,并且可以从映射相同文件的其他程序中看到),或者MapMode.PRIVATE ( 写时复制映射;结果缓冲区中的更改不能传播到文件,并且在其他程序中不可见)。
  • position:映射的区域从文件中指定的位置开始(非负)。
  • size:表示映射区域的大小(0sizeInteger.MAX_VALUE)。

Image 注意只有打开读的通道才能映射为只读,只有打开读写的通道才能映射为读/写或私有。

map()方法将返回一个实际代表提取区域的MappedByteBuffer。这用以下三个方法扩展了ByteBuffer,更多细节可以在[download.oracle.com/javase/7/docs/api/index.html](http://download.oracle.com/javase/7/docs/api/index.html)的官方文档中找到:

  • force():强制将转换后的缓冲区传播到原始文件
  • load():将缓冲内容加载到物理内存中
  • isLoaded():验证缓冲区内容是否在物理内存中

下一个应用为文件BrasilOpen.txt(位于C:\rafaelnadal\tournaments\2009中)获得一个新的通道,并在READ_ONLY模式下将其全部内容映射到一个字节缓冲区中。为了测试操作是否成功完成,以下是字节缓冲区内容的打印输出:

`import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;

public class Main { public static void main(String[] args) {

Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BrasilOpen.txt");
   MappedByteBuffer buffer = null;

try (FileChannel fileChannel = (FileChannel.open(path,
                                                      EnumSet.of(StandardOpenOption.READ)))) {

buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());

} catch (IOException ex) {
     System.err.println(ex);
   }

if (buffer != null) {
       try {
           Charset charset = Charset.defaultCharset();
           CharsetDecoder decoder = charset.newDecoder();
           CharBuffer charBuffer = decoder.decode(buffer);
           String content = charBuffer.toString();
           System.out.println(content);

buffer.clear();
       } catch (CharacterCodingException ex) {
         System.err.println(ex);
       }
    }
  }
}`

如果一切正常,您应该看到BrasilOpen.txt内容输出到控制台。

锁定频道的文件

文件锁定是一种限制访问文件或其他数据的机制,以确保两个或更多用户不能同时修改同一个文件。这防止了经典的调解更新场景。通常,当第一个用户访问该文件时,该文件被锁定,并保持锁定(可以读取,但不能修改),直到该用户完成该文件。

文件锁定的确切行为取决于平台。在某些平台上,文件锁定是建议性的(如果应用不检查文件锁定,任何应用都可以访问文件),而在其他平台上,它是强制性的(文件锁定阻止任何应用访问文件)。

我们可以通过 NIO API 利用 Java 应用中的文件锁定。但是,不能保证文件锁定机制总是按您预期的那样工作。底层操作系统支持或有时错误的实现可能会影响预期的行为。请记住以下几点:

  • “文件锁代表整个 Java 虚拟机持有。它们不适合控制同一虚拟机内多个线程对文件的访问。(Java 平台 SE 7 官方文档,http://download . Oracle . com/javase/7/docs/API/Java/nio/channels/file lock . html。)
  • Windows 会为您锁定目录和其他结构,因此如果另一个进程打开了文件,删除、重命名或写操作将会失败。因此,在系统锁之上创建 Java 锁将会失败。
  • Linux 内核管理一组被称为咨询锁定机制的功能。此外,您可以使用强制锁在内核级别强制锁定。因此,在使用 Java 锁时,请记住这一点。

FileChannel类提供了四种文件锁定方法:两种lock()方法和两种tryLock()方法。lock()方法阻塞应用,直到可以检索到所需的锁,而tryLock()方法不阻塞应用,如果文件已经被锁定,则返回null或抛出异常。有一个lock() / tryLock()方法用于检索该通道文件上的排他锁,还有一个方法用于检索该通道文件的某个区域上的锁——该方法还允许共享锁。

为了演示文件锁定,我们来看两个应用。第一个在向一个名为vamos.txt(在C:\rafaelnadal\email下)的文件中写入一些文本时锁定该文件 2 分钟。在此期间,第二个应用将尝试写入同一文件。如果文件成功锁定了 2 分钟,那么第二个应用将抛出一个java.io.IO.Exception,并输出如下消息:


The process cannot access the file because another process has locked a portion of the file.

这是第一个应用:

`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;
import java.util.EnumSet;

public class Main {

public static void main(String[] args) {

Path path = Paths.get("C:/rafaelnadal/email", "vamos.txt");
   ByteBuffer buffer = ByteBuffer.wrap("Vamos Rafa!".getBytes());

try (FileChannel fileChannel = (FileChannel.open(path, EnumSet.of(StandardOpenOption.READ,
                                                                StandardOpenOption.WRITE)))) {

// Use the file channel to create a lock on the file.
    // This method blocks until it can retrieve the lock.
    FileLock lock = fileChannel.lock();

// Try acquiring the lock without blocking. This method returns
     // null or throws an exception if the file is already locked.
     //try {
        //    lock = fileChannel.tryLock();      //} catch (OverlappingFileLockException e) {
         // File is already locked in this thread or virtual machine
     //}

if (lock.isValid()) {

System.out.println("Writing to a locked file ...");
         try {
             Thread.sleep(60000);
             } catch (InterruptedException ex) {
               System.err.println(ex);
             }
         fileChannel.position(0);
         fileChannel.write(buffer);
         try {
             Thread.sleep(60000);
         } catch (InterruptedException ex) {
           System.err.println(ex);
         }
      }

// Release the lock
      lock.release();

System.out.println("\nLock released!");

} catch (IOException ex) {
       System.err.println(ex);
     }
  }
}`

运行前面的应用,并在最多 2 分钟内并行启动下面的应用:

`import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;

public class Main {

public static void main(String[] args) {

Path path = Paths.get("C:/rafaelnadal/email", "vamos.txt");
   ByteBuffer buffer = ByteBuffer.wrap("Hai Hanescu !".getBytes());

try (FileChannel fileChannel = (FileChannel.open(path, EnumSet.of(StandardOpenOption.READ,
                                                                StandardOpenOption.WRITE)))) { fileChannel.position(0);
     fileChannel.write(buffer);

} catch (IOException ex) {
     System.err.println(ex);
   }
 }
}`

你应该会发现第二个应用只有在锁被释放后,也就是 2 分钟后才能写入vamos.txt

使用 FileChannel 复制文件

FileChannel提供了几种复制文件的方法。您可以使用直接或非直接ByteBufferFileChannel,使用FileChannel.transferTo()FileChannel.transferFrom(),或使用FileChannel.map()

使用 FileChannel 和直接或非直接字节缓冲区复制文件

要复制带有FileChannel和直接或非直接ByteBuffer的文件,我们需要一个通道用于源文件,一个通道用于目标文件,以及一个直接或非直接ByteBuffer。例如,下面的代码片段将使用 4KB 的直接文件ByteBuffer将文件Rafa Best Shots.mp4(位于C:\rafaelnadal\tournaments\2009\videos目录中)复制到C:\根目录:

`…
final Path copy_from = Paths.get("C:/rafaelnadal/tournaments/2009/
                                                             videos/Rafa Best Shots.mp4");
final Path copy_to = Paths.get("C:/Rafa Best Shots.mp4");
int bufferSizeKB = 4;
int bufferSize = bufferSizeKB * 1024;

System.out.println("Using FileChannel and direct buffer ...");
try (FileChannel fileChannel_from = (FileChannel.open(copy_from,
                      EnumSet.of(StandardOpenOption.READ)));
     FileChannel fileChannel_to = (FileChannel.open(copy_to,
                      EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {

// Allocate a direct ByteBuffer
     ByteBuffer bytebuffer = ByteBuffer.allocateDirect(bufferSize);

// Read data from file into ByteBuffer
     int bytesCount;
     while ((bytesCount = fileChannel_from.read(bytebuffer)) > 0) {
             //flip the buffer which set the limit to current position, and position to 0            
             bytebuffer.flip();
             //write data from ByteBuffer to file
             fileChannel_to.write(bytebuffer);
             //for the next read
             bytebuffer.clear();      }
} catch (IOException ex) {
  System.err.println(ex);
}
…`

要使用非直接ByteBuffer,只需更换线路即可

     ByteBuffer bytebuffer = ByteBuffer.allocateDirect(bufferSize);

用下面一行:

     ByteBuffer bytebuffer = ByteBuffer.allocate(bufferSize);
使用 FileChannel.transferTo()或 FileChannel.transferFrom()复制文件

FileChannel.transferTo()将字节从一个通道的文件传输到给定的可写字节通道。您选择位置、要传输的最大字节数和目标通道,FileChannel.transferTo()返回传输的字节数。下面的例子转移了Rafa Best Shots.mp4的全部内容:

…
System.out.println("Using FileChannel.transferTo method ...");
try (FileChannel fileChannel_from = (FileChannel.open(copy_from,
                      EnumSet.of(StandardOpenOption.READ)));
     FileChannel fileChannel_to = (FileChannel.open(copy_to,
                      EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {

            fileChannel_from.transferTo(0L, fileChannel_from.size(), fileChannel_to);

} catch (IOException ex) {
  System.err.println(ex);
}
…

或者,您可以使用FileChannel.transferFrom()将字节从给定的可读字节通道传输到该通道的文件中。为此,请通过替换以下行来修改前面的代码

          fileChannel_from.transferTo(0L, fileChannel_from.size(), fileChannel_to);

用下面一行:

          fileChannel_to.transferFrom(fileChannel_from, 0L, (int) fileChannel_from.size());
使用 FileChannel.map()复制文件

在本章的前面,您已经看到了如何使用MappedByteBuffer将通道文件的一个区域映射到内存中。在本节中,我们推断这个例子复制了Rafa Best Shots.mp4内容:

… System.out.println("Using FileChannel.map method ..."); try (FileChannel fileChannel_from = (FileChannel.open(copy_from, `                      EnumSet.of(StandardOpenOption.READ)));
     FileChannel fileChannel_to = (FileChannel.open(copy_to,
                      EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {

MappedByteBuffer buffer = fileChannel_from.map(FileChannel.MapMode.READ_ONLY, 0,
                                                                     fileChannel_from.size());

fileChannel_to.write(buffer);
     buffer.clear();

} catch (IOException ex) {
  System.err.println(ex);
}
…`

基准测试文件通道拷贝功能

在前三节中,您看到了使用FileChannel功能复制文件的不同方式。Java 还提供了另一套复制文件的解决方案,包括使用Files.copy()方法或者缓冲/非缓冲流和一个字节数组。你应该选择哪一个?这是一个很难回答的问题,它的答案取决于很多因素。本节重点介绍一个因素,即速度,因为快速完成拷贝任务可以提高工作效率,在某些情况下,这对成功至关重要。因此,本节实现了一个应用,该应用比较以下每种解决方案为每个拷贝花费的时间:

  • FileChannel和非直达ByteBuffer
  • FileChannel和直接ByteBuffer
  • FileChannel.transferTo()
  • FileChannel.transferFrom()
  • FileChannel.map()
  • 使用缓冲流和字节数组
  • 使用无缓冲流和字节数组
  • Files.copy() ( PathPathInputStreamPathPathOutputStream)

测试在以下条件下进行:

  • 复制的文件类型:MP4 视频(文件名为Rafa Best Shots.mp4,初始位置在C:\rafaelnadal\tournaments\2009\videos
  • 复制文件大小:58.3MB
  • 测试的缓冲区大小:4KB、16KB、32KB、64KB、128KB、256KB 和 1024KB
  • 机器:移动 AMD Sempron 处理器 3400 + 1.80 GHz,1.00GB 内存,32 位操作系统,Windows 7 旗舰版
  • 测量类型:使用System.nanoTime()方法
  • 仅在三次被忽略的连续运行后捕获时间;忽略前三次运行以获得趋势。第一次运行总是比后续运行慢。

接下来列出了该应用,可以在本书页面的源代码下载部分获得:

`import java.nio.MappedByteBuffer;
import java.io.OutputStream;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;

public class Main {

public static void deleteCopied(Path path){

try {
      Files.deleteIfExists(path);
  } catch (IOException ex) {
    System.err.println(ex);
  }

}

public static void main(String[] args) {

final Path copy_from = Paths.get("C:/rafaelnadal/tournaments/2009/videos/
                                                                        Rafa Best Shots.mp4");
 final Path copy_to = Paths.get("C:/Rafa Best Shots.mp4");
 long startTime, elapsedTime;
 int bufferSizeKB = 4; //also tested for 16, 32, 64, 128, 256 and 1024
 int bufferSize = bufferSizeKB * 1024;

deleteCopied(copy_to);

//FileChannel and non-direct buffer
 System.out.println("Using FileChannel and non-direct buffer ...");
 try (FileChannel fileChannel_from = (FileChannel.open(copy_from,
                      EnumSet.of(StandardOpenOption.READ))); FileChannel fileChannel_to = (FileChannel.open(copy_to,
                      EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {

startTime = System.nanoTime();

// Allocate a non-direct ByteBuffer
      ByteBuffer bytebuffer = ByteBuffer.allocate(bufferSize);

// Read data from file into ByteBuffer
      int bytesCount;
      while ((bytesCount = fileChannel_from.read(bytebuffer)) > 0) {
       //flip the buffer which set the limit to current position, and position to 0            
       bytebuffer.flip();
       //write data from ByteBuffer to file
       fileChannel_to.write(bytebuffer);
       //for the next read
       bytebuffer.clear();
      }

elapsedTime = System.nanoTime() - startTime;
      System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds");
 } catch (IOException ex) {
   System.err.println(ex);
 }

deleteCopied(copy_to);

//FileChannel and direct buffer
 System.out.println("Using FileChannel and direct buffer ...");
 try (FileChannel fileChannel_from = (FileChannel.open(copy_from,
                      EnumSet.of(StandardOpenOption.READ)));
      FileChannel fileChannel_to = (FileChannel.open(copy_to,
                      EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {

startTime = System.nanoTime();

// Allocate a direct ByteBuffer
      ByteBuffer bytebuffer = ByteBuffer.allocateDirect(bufferSize);

// Read data from file into ByteBuffer
      int bytesCount;
      while ((bytesCount = fileChannel_from.read(bytebuffer)) > 0) {
       //flip the buffer which set the limit to current position, and position to 0            
       bytebuffer.flip();
       //write data from ByteBuffer to file
       fileChannel_to.write(bytebuffer);
       //for the next read
       bytebuffer.clear();
      }

elapsedTime = System.nanoTime() - startTime;
      System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException ex) {
   System.err.println(ex);
 }

deleteCopied(copy_to);

//FileChannel.transferTo()
 System.out.println("Using FileChannel.transferTo method ...");
 try (FileChannel fileChannel_from = (FileChannel.open(copy_from,
                      EnumSet.of(StandardOpenOption.READ)));
      FileChannel fileChannel_to = (FileChannel.open(copy_to,
                      EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {

startTime = System.nanoTime();

fileChannel_from.transferTo(0L, fileChannel_from.size(), fileChannel_to);

elapsedTime = System.nanoTime() - startTime;
      System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds");
 } catch (IOException ex) {
   System.err.println(ex);
 }

deleteCopied(copy_to);

//FileChannel.transferFrom()
 System.out.println("Using FileChannel.transferFrom method ...");
 try (FileChannel fileChannel_from = (FileChannel.open(copy_from,
                      EnumSet.of(StandardOpenOption.READ)));
      FileChannel fileChannel_to = (FileChannel.open(copy_to,
                      EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {

startTime = System.nanoTime();

fileChannel_to.transferFrom(fileChannel_from, 0L, (int) fileChannel_from.size());

elapsedTime = System.nanoTime() - startTime;
      System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds");
 } catch (IOException ex) {
   System.err.println(ex);
 }

deleteCopied(copy_to);

//FileChannel.map
 System.out.println("Using FileChannel.map method ...");
 try (FileChannel fileChannel_from = (FileChannel.open(copy_from,
                      EnumSet.of(StandardOpenOption.READ)));
      FileChannel fileChannel_to = (FileChannel.open(copy_to,
                      EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)))) {

startTime = System.nanoTime(); MappedByteBuffer buffer = fileChannel_from.map(FileChannel.MapMode.READ_ONLY,
                                                                  0, fileChannel_from.size());

fileChannel_to.write(buffer);
      buffer.clear();

elapsedTime = System.nanoTime() - startTime;
      System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds");
} catch (IOException ex) {
  System.err.println(ex);
}

deleteCopied(copy_to);

//Buffered Stream I/O
System.out.println("Using buffered streams and byte array ...");
File inFileStr = copy_from.toFile();
File outFileStr = copy_to.toFile();
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(inFileStr));
     BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(outFileStr))) {

startTime = System.nanoTime();

byte[] byteArray = new byte[bufferSize];
     int bytesCount;
     while ((bytesCount = in.read(byteArray)) != -1) {
             out.write(byteArray, 0, bytesCount);
     }

elapsedTime = System.nanoTime() - startTime;
     System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds");
} catch (IOException ex) {
  System.err.println(ex);
}

deleteCopied(copy_to);

System.out.println("Using un-buffered streams and byte array ...");
try (FileInputStream in = new FileInputStream(inFileStr);
     FileOutputStream out = new FileOutputStream(outFileStr)) {

startTime = System.nanoTime();

byte[] byteArray = new byte[bufferSize];
     int bytesCount;
     while ((bytesCount = in.read(byteArray)) != -1) {
             out.write(byteArray, 0, bytesCount);
     }

elapsedTime = System.nanoTime() - startTime;
     System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds"); } catch (IOException ex) {
  System.err.println(ex);
}

deleteCopied(copy_to);

System.out.println("Using Files.copy (Path to Path) method ...");
try {
    startTime = System.nanoTime();

Files.copy(copy_from, copy_to, NOFOLLOW_LINKS);

elapsedTime = System.nanoTime() - startTime;
    System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds");
} catch (IOException e) {
  System.err.println(e);
}

deleteCopied(copy_to);

System.out.println("Using Files.copy (InputStream to Path) ...");
try (InputStream is = new FileInputStream(copy_from.toFile())) {

startTime = System.nanoTime();

Files.copy(is, copy_to);

elapsedTime = System.nanoTime() - startTime;
    System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds");
} catch (IOException e) {
  System.err.println(e);
}

deleteCopied(copy_to);

System.out.println("Using Files.copy (Path to OutputStream) ...");
try (OutputStream os = new FileOutputStream(copy_to.toFile())) {

startTime = System.nanoTime();

Files.copy(copy_from, os);

elapsedTime = System.nanoTime() - startTime;
     System.out.println("Elapsed Time is " + (elapsedTime / 1000000000.0) + " seconds");
 } catch (IOException e) {
   System.err.println(e);
 }
 }
}`

这个应用的输出很难进行分类,因为涉及的数字太多了,所以我绘制了一些数据,以便让您更清楚地看到几次比较的结果,如下面几节中的图所示。这些图中的 Y 轴是以秒表示的估计时间,X 轴是所用缓冲区的大小(或运行次数,跳过前三次运行后)。

文件通道和非直接缓冲区与文件通道和直接缓冲区

如图 7-11 所示,似乎对于小于 256KB 的缓冲区,非直接缓冲区要快得多,而对于大于 256KB 的缓冲区,直接缓冲区略快(参见图 7-11 )。

Image

图 7-11 文件通道和非直接缓冲区对比文件通道和直接缓冲区

filechannel . transferto()诉 filechannel . transferfrom()诉 FileChannel.map()

如图 7-12 中的所示,看起来transferTo()transferFrom()在连续七次运行后几乎相同,而FileChannel.map()是最慢的解决方案。

Image

图 7-12【filechannel . transfer()诉 filechannel . transferfrom()诉 FileChannel.map()

三种不同的 Files.copy()方法

如图图 7-13 所示,最快的Files.copy()方法是PathPath,其次是PathOutputStream,最后是InputStreamPath

Image

图 7-13 Files.copy()方法

FileChannel 和非直接缓冲区与 FileChannel.transferTo()与路径到路径

作为最终测试,我们从上述三张图中取出最快的结果,并将它们放在图 7-14 中。由于我们没有指定FileChannel.transferTo()PathPath的缓冲区大小,我们将七次运行的平均时间作为参考。如你所见,Files.copy()PathPath似乎是复制文件最快的解决方案。

Image

图 7-14 带非直接缓冲区的文件通道与文件通道. transferTo()与路径到路径的路径

总结

这一章从简单概述ByteBuffer类开始,它通常与SeekableByteChannelFileChannel一起使用。接下来详细介绍了SeekableByteChannel与应用的接口,这些应用将随机读写文件以完成不同类型的常见任务。然后,您看到了如何获得具有 RAF 功能的FileChannel,并发现了FileChannel提供的主要功能,包括将文件的一个区域直接映射到内存中以实现更快的访问,锁定文件的一个区域,以及在不影响通道当前位置的情况下从绝对位置读取和写入字节。本章以一个基准测试应用结束,该应用试图通过将FileChannel的功能与其他常用方法进行比较来确定复制文件的最快方法,例如Files.copy(),使用缓冲流和字节数组,以及使用非缓冲流和字节数组。

八、套接字 API

互联网诞生于 20 世纪 50 年代和 60 年代。几年后,大约在 80 年代,套接字的概念在 BSD(Berkeley Software Distribution——一种 Unix 变体)上被引入,用于使用互联网协议(IP)的进程之间的通信。几年后,在 1996 年,JDK 1.0 将套接字的概念带到了编程世界,作为易于使用和跨平台的网络通信模型。最后,程序员现在可以创建网络应用,而无需对网络通信进行多年的研究。Java 开发人员只需简单了解几个主题,如 IP、IP 地址、端口和 Java 网络,就可以编写一个简单的网络应用。

IP 将所有通信分成从源到目的地分别处理的个数据包(数据块)——没有交付保证。在 IP 之上,我们还有其他常见的协议,如 TCP(传输控制协议)和 UDP(用户数据报协议)(本章的应用利用了这些协议),在这些协议之上,我们还有更多,包括 HTTP、TELNET、DNS 等等。套接字利用 IP 进行机器之间的通信,因此 Java 网络应用可以使用它们预定义的协议与现有的服务器“对话”。

在互联网上,每台机器都可以通过一个数字标签来识别,这个数字标签被称为 IP 地址。每个 Java 开发人员都应该知道我们处理两种类型的 IP 地址:IPv4(用 32 位表示,例如 124.32.45.23)和 IPv6(用 128 位表示,例如 2607:f0d 0:1002:0051:0000:0000:0000:0000:0004)。而且,要知道 IP 地址被组织成 A、B、C、D、e 类是很重要的,由于我们对 D 类 IP 地址特别感兴趣,假设 IPv4s 地址在 224.0.0.1 和 239.255.255.255 之间变化,表示组播组。另外,记住地址 127.0.0.1 是为本地主机地址保留的。

集中在端口上,TCP/UDP 端口的范围在 0 到 65535 之间,它们在 Java 中用整数表示。某些类型的服务器通常位于某些端口上:例如,如果您连接到一台主机的端口 80,您可能会发现一个 HTTP 服务器。在端口 21 上,你可以期待一个 FTP 服务器,在端口 23 上,一个 Telnet 服务器,在端口 119 上,一个 NNTP 服务器,等等。因此,选择端口时要谨慎;确保不要干扰其他进程,并且保持在适当的范围内。

每个概念都有专门的书籍,但是对于创建 Java 客户机/服务器应用来说,这些信息已经足够了。在客户机/服务器模型中,服务器运行在主机上,监听端口,以接收来自网络上的客户机甚至同一台机器的连接请求。客户端使用 IP 地址(主机名)和端口来定位服务器,而服务器根据每个客户端的请求为其提供服务。在连接过程中,客户端通过一个本地端口号向服务器标识自己,该端口号可以由内核显式设置或分配—一个套接字绑定到这个本地端口号,以便在连接过程中使用(我们说客户端绑定到一个本地端口号)。接受之后,服务器获得一个绑定到新的本地端口号的新套接字,并将其远程端点设置为客户端的地址和端口——它需要一个新的端口号,以便可以继续侦听原始端口上的连接请求。一旦通信建立,数据可以在套接字之间来回传递,直到通信被故意关闭或意外中断。

我们可以得出结论,对于 Java 来说,套接字是服务器程序和它的客户端程序之间的双向软件端点,或者更一般地说,是在网络上运行的参与双向通信的两个程序之间的双向软件端点。一个端点是 IP 地址和端口号的组合。

Java 在 JDK 1.0 中引入了对套接字的支持,但是随着时间的推移,版本与版本之间的事情已经发生了变化。跳到 Java 7,NIO.2 通过用新方法更新现有的类和添加新的接口/类来编写基于 TCP/UDP 的应用,改进了这种支持。首先,NIO.2 引入了一个名为NetworkChannel的接口,它为所有网络通道类提供了方法公共——任何实现这个接口的通道都是网络套接字的通道。专用于同步套接字通道的主要类ServerSocketChannelSocketChannelDatagramChannel实现了这个接口,它提供了绑定和返回本地地址的方法,以及通过新的SocketOption<T>接口和StandardSocketOptions类设置和获取套接字选项的方法。该接口的方法和直接添加到类中的方法(用于检查连接状态、获取远程地址和关闭)将使您不必调用socket()方法。

NIO.2 还引入了MulticastChannel接口作为NetworkChannel.的子接口,顾名思义,MulticastChannel接口映射了一个支持 IP 组播的网络通道。请记住,MulticastChannel仅由数据报通道DatagramChannel实现。当加入一个组播组时,你会得到一个成员密钥,这是一个代表组播组成员的令牌。通过成员资格密钥,您可以阻止/解除阻止来自不同地址的数据报、删除成员资格、获取为其创建成员资格密钥的频道和/或多播组,等等。

Image 关于 Java 通道的简要概述,请看看第七章的的“通道简要概述”一节。此外,为了理解 Java 缓冲区是如何工作的,可以考虑“字节缓冲区概述”一节。

网络渠道概述

在本节中,我们将对NetworkChannel方法进行一个简短的概述。这个接口代表了一个网络套接字的通道,并附带了一组用于所有套接字的五个通用方法。我们在这里介绍它们,因为它们在接下来的部分会非常有用。

我们将从bind()方法开始,它将通道的套接字绑定到一个本地地址。更准确地说,该方法将在套接字和本地地址之间建立关联,本地地址通常被显式指定为一个InetSocketAddress实例(该类用 IP(或主机名)和端口表示套接字地址,并扩展了抽象的SocketAddress类)。如果我们将 null 传递给bind()方法,也可以自动分配本地地址。此方法用于将服务器套接字通道、套接字通道和数据报套接字通道与本地机器绑定。它将返回当前频道:

NetworkChannel bind(SocketAddress local) throws IOException

NetworkChannel可以通过调用getLocalAddress()方法提取绑定地址。如果通道的套接字未绑定,则它返回 null:

SocketAddress getLocalAddress() throws IOException

插座选项

NetworkChannel剩下的三个方法处理当前通道支持的套接字选项。与套接字关联的套接字选项由SocketOption<T>接口表示。目前,NIO.2 通过 StandardSocketOptions 类中的一组标准选项来实现这个接口。它们在这里:

  • IP_MULTICAST_IF:该选项用于指定面向数据报套接字发送多播数据报所使用的网络接口(NetworkInterface);如果是null,那么操作系统将选择输出接口(如果有的话)。默认为null,但是可以在 socket 绑定后设置该选项的值。当我们讨论发送数据报时,您将看到如何找出您的机器上有哪些多播接口。
  • IP_MULTICAST_LOOP:这个选项的值是一个布尔值,它控制多播数据报的回送(这取决于操作系统)。作为应用编写人员,您必须决定是否希望您发送的数据返回到您的主机。默认情况下,这是TRUE,但是该选项的值可以在套接字绑定后设置。
  • IP_MULTICAST_TTL:该选项的值是一个 0 到 255 之间的整数,代表面向数据报套接字发出的组播包的生存时间。如果没有另外指定,多播数据报以默认值 1 发送,以防止它们被转发到本地网络之外。通过这个选项,我们可以控制多播数据报的范围。默认情况下,该值设置为 1,但是该选项的值可以在套接字绑定后设置。
  • IP_TOS:该选项的值是一个整数,表示套接字发送的 IP 数据包中服务类型(ToS)八位字节的值——该值的解释因网络而异。目前,这仅适用于 IPv4,默认情况下,其值通常为 0。套接字绑定后,可以随时设置选项的值。
  • SO_BROADCAST:该选项的值为布尔值,表示是否允许发送广播数据报(特定于发送到 IPv4 广播地址的面向数据报的套接字)。默认为FALSE,但该选项的值可以随时设置。
  • SO_KEEPALIVE:该选项的值是一个布尔值,表示连接是否应该保持活动状态。默认设置为FALSE,但该选项的值可以随时设置。
  • SO_LINGER:该选项的值是一个整数,以秒为单位表示超时(逗留时间间隔)。当试图通过close()方法关闭阻塞模式套接字时,它将在传输未发送的数据(没有为非阻塞模式定义)之前等待逗留间隔的持续时间。默认情况下,它是一个负值,这意味着此选项被禁用。该选项的值可以随时设置,最大值取决于操作系统。
  • SO_RCVBUF:该选项的值是一个整数,表示套接字接收缓冲区——网络实现使用的输入缓冲区——的字节大小。默认情况下,该值取决于操作系统,但可以在套接字绑定或连接之前设置。根据操作系统的不同,该值可以在套接字绑定后更改。不允许负值。
  • SO_SNDBUF:该选项的值是一个整数,表示套接字发送缓冲区的字节大小——网络实现使用的输出缓冲区。默认情况下,该值取决于操作系统,但可以在套接字绑定或连接之前设置。根据操作系统的不同,该值可以在套接字绑定后更改。不允许负值。
  • SO_REUSEADDR:该选项的值是一个整数,表示地址是否可以重用。当我们希望多个程序绑定到同一个地址时,这在数据报多播中非常有用。在面向流的套接字的情况下,当先前的连接处于TIME_WAIT状态时,套接字可以被绑定到一个地址—TIME_WAIT意味着操作系统已经接收到关闭套接字的请求,但是等待来自客户端的可能的延迟通信。默认情况下,该选项的值依赖于操作系统,但是可以在套接字绑定或连接之前进行设置。
  • TCP_NODELAY:该选项的值是一个启用/禁用 Nagle 算法的整数(有关 Nagle 算法的更多信息,请参见[en.wikipedia.org/wiki/Nagle%27s_algorithm](http://en.wikipedia.org/wiki/Nagle%27s_algorithm))。默认为FALSE,但可以随时设置。

现在,设置和获取选项可以通过NetworkChannel.getOption()NetworkChannel.setOption()方法来完成:

<T> T getOption(SocketOption<T> name) throws IOException
<T> NetworkChannel setOption(SocketOption<T> name, T value) throws IOException

检索特定通道(网络套接字)的支持选项可以通过调用该通道上的NetworkChannel.supportedOptions()方法来完成:

Set<SocketOption<?>> supportedOptions()

编写 TCP 服务器/客户端应用

编写 TCP 教程远不是我们的目标,因为这是一个文档丰富的大主题,并且涉及许多技术概念和方面,但是我们将给出一个快速的概述。TCP 类似于电话连接,它通过套接字在两个端点之间建立连接,并且套接字在整个通信期间保持打开。TCP 的主要功能是提供点对点通信机制。一台机器上的一个进程与另一台机器上或同一台机器内的另一个进程通信。唯一的 TCP 连接由五个元素标识:服务器的 IP 地址和端口、客户端的 IP 地址和端口以及协议(TCP/IP、UDP 等。).服务器监听一个端口,并且可以同时与许多客户端通话。TCP 提供了许多涉及数据分组的优点(例如,优于 UDP)。TCP 负责许多重要的任务,包括将数据分成数据包、缓冲数据、跟踪丢失的数据包(用于重新发送丢失或乱序的数据包)以及根据应用处理能力控制数据传输的速度。而且,TCP 支持以字节数组或使用流的形式发送数据,这在 Java 中非常流行。

阻塞与非阻塞机制

当您决定编写一个 Java TCP 服务器/客户端应用时,您必须考虑您是需要编写一个阻塞应用还是非阻塞应用。这个决定很重要,因为实现是不同的,而且复杂性可能也很关键。

阻塞机制的主要特征是假定在完全接收到 I/O 之前,给定的线程不能再做任何事情,这在某些情况下可能需要一段时间——应用的流被阻塞,因为方法不会立即返回。另一方面,非阻塞机制会立即对 I/O 请求进行排队,并将控制权返回给应用流(方法会立即返回)。请求将由内核稍后处理。

从 Java 开发人员的角度来看,您还必须考虑这些机制所涉及的复杂程度。非阻塞机制实现起来比阻塞机制复杂得多,但是它们允许您获得更高的性能和可伸缩性。

Image 注意非阻塞机制不同于异步机制(尽管这经常被争论,取决于你问谁)。例如,在非阻塞环境中,如果不能快速返回答案,API 会立即返回一个错误,并且不做任何其他事情,而在异步环境中,API 总是立即返回,并开始在后台努力为您的请求提供服务。换句话说,使用非阻塞机制,函数不会在堆栈上等待,而使用异步机制,在调用离开堆栈后,工作可以代表函数调用继续进行。异步更熟悉并行(如线程),而非阻塞通常指轮询。

自 NIO 以来,阻塞和非阻塞模式都已经实现,但是我们将尝试用新的 NIO.2 特性来增加代码的趣味。

在接下来的部分中,我们将开发这两种类型的应用。让我们从使用阻塞机制的简单方法开始。

编写阻塞的 TCP 服务器

为了更好地理解如何完成这项任务,最简单的方法是遵循一组简单的步骤,并在讨论结束时将代码块粘在一起。我们想开发一个单线程阻塞的 TCP 服务器,它将把从它那里得到的所有信息反馈给客户端。实现这一点的许多步骤也可以转移到其他阻塞 TCP 服务器。

创建新的服务器套接字通道

第一步包括为面向流的监听套接字创建一个可选择的通道,这要感谢java.nio.channels.ServerSocketChannel类,它可以安全地供多个并发线程使用。更准确地说,这个任务是通过ServerSocketChannel.open()方法完成的,如下所示:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

请记住,新创建的服务器套接字通道没有绑定或连接。绑定和连接将在接下来的步骤中完成。

您可以通过调用ServerSocketChannel.isOpen()方法来检查服务器套接字是否已经打开或者已经成功打开,该方法返回相应的Boolean值:

if (serverSocketChannel.isOpen()) {
    ...
}
配置阻塞机制

如果服务器套接字通道已经成功打开,就该指定阻塞机制了。为此,我们调用接收一个boolean值的ServerSocketChannel.configureBlocking()方法。如果我们传递 true,那么将使用阻塞机制;如果我们通过false,那么非阻塞机制将被使用:

serverSocketChannel.configureBlocking(true);

注意,这个方法返回一个SelectableChannel对象,它代表一个可以通过Selector复用的通道。这在我们处于非阻塞模式时很有用;因此我们暂时忽略它。

设置服务器套接字通道选项

这是一个可选步骤。没有必需的选项(您可以使用缺省值),但是我们将显式地设置几个选项来向您展示如何做到这一点。更准确地说,一个服务器套接字通道支持两个选项:SO_RCVBUF and SO_REUSEADDR.我们将对它们都进行设置,如下所示:

serverSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
serverSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);

您可以通过调用继承的方法supportedOptions()找到服务器套接字通道支持的选项:

Set<SocketOption<?>> options = serverSocketChannel.supportedOptions();
for(SocketOption<?> option : options) System.out.println(option);
绑定服务器套接字通道

此时,我们可以将通道的套接字绑定到本地地址,并配置套接字来侦听连接。为此,我们称之为新的ServerSocketChannel.bind()方法(该方法在前面的“网络通道概述”一节中介绍过)。我们的服务器将在本地主机(127.0.0.1),端口 5555(任意选择)上等待传入的连接:

final int DEFAULT_PORT = 5555;
final String IP = "127.0.0.1";
serverSocketChannel.bind(new InetSocketAddress(IP, DEFAULT_PORT));

另一种常见的方法是创建一个InetSocketAddress对象,不指定 IP 地址,只指定端口(有一个构造函数)。在这种情况下,IP 地址是通配符地址,端口号是指定值。通配符地址是一个特殊的本地 IP 地址,只能在绑定操作中使用,通常表示“任何”:

*serverSocketChannel.bind(new InetSocketAddress(DEFAULT_PORT));

Image 警告当您使用 IP 通配符地址时,请注意避免任何不必要的复杂情况,如果您有多个具有独立 IP 地址的网络接口,可能会出现这种情况。在这种情况下,如果您不确定如何顺利完成,建议将套接字绑定到特定的网络地址,而不是使用通配符。

此外,还有一个bind()方法接收地址以绑定套接字和最大数量的挂起连接:

public abstract ServerSocketChannel bind(SocketAddress local,int pc) throws IOException

如果我们将 null 传递给bind()方法,也可以自动分配本地地址。还可以通过调用从NetworkChannel接口继承的ServerSocketChannel.getLocalAddress()方法找出绑定的本地地址。如果服务器套接字通道尚未绑定,则返回 null。

System.out.println(serverSocketChannel.getLocalAddress());
接受连接

在打开和绑定之后,我们最终到达验收里程碑。由于我们处于阻塞模式,接受连接将阻塞应用,直到有新的连接可用或发生 I/O 错误。我们通过调用ServerSocketChannel.accept()方法来表示对接受新连接的不耐烦。当新连接可用时,该方法返回新连接的客户端套接字通道(或简称为套接字通道)。这是SocketChannel类的一个实例,代表面向流的连接套接字的可选通道。

SocketChannel socketChannel = serverSocketChannel.accept();

Image 注意试图为一个未绑定的服务器套接字通道调用accept()方法会抛出一个NotYetBoundException异常。

一旦我们接受了一个新的连接,我们就可以通过调用SocketChannel.getRemoteAddress()方法找到远程地址。这个方法是 Java 7 (NIO.2)中的新方法,它返回这个通道的套接字所连接的远程地址:

System.out.println("Incoming connection from: " + socketChannel.getRemoteAddress());
通过连接传输数据

此时,服务器和客户端可以通过连接传输数据。它们可以发送和接收映射为字节数组的不同种类的数据包,或者使用流和标准的 Java 文件 I/O 机制。实现传输(发送/接收)是一个灵活且特定于实现的过程,因为它涉及许多方面。例如,对于我们的服务器,我们选择使用ByteBuffers,并且我们记住这是一个 echo 服务器——它从客户端读取的就是它写回的。下面是传输代码片段:

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
...
while (socketChannel.read(buffer) != -1) {

       buffer.flip();

       socketChannel.write(buffer);

       if (buffer.hasRemaining()) {
           buffer.compact();
           } else {
                  buffer.clear();
           }
}

SocketChannel类为ByteBuffers.提供了一组read()/write()方法,因为它们非常直观,我们将只列出它们:

  • 从这个通道读取一个字节序列到给定的缓冲区。这些方法返回读取的字节数(可以是零),或者如果通道已经到达流尾,则返回–1:public abstract int read(ByteBuffer dst) throws IOException public final long read(ByteBuffer[] dsts) throws IOException public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException
  • 从给定的缓冲区向该通道写入一个字节序列。这些方法返回写入的字节数;可以为零:public abstract int write(ByteBuffer src) throws IOException public final long write(ByteBuffer[] srcs) throws IOException public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException
使用流代替缓冲区

如你所知,通道和缓冲区是非常好的朋友,但是如果你决定用 streams 代替(InputStream and OutputStream),那么你需要使用下面的代码;一旦获得了 I/O 流,就可以进一步探索标准的 Java 文件 I/O 机制。

InputStream in = socketChannel.socket().getInputStream();
OutputStream out = socketChannel.socket().getOutputStream();
关闭 I/O 连接

通过调用新的 NIO.2 SocketChannel.shutdownInput()SocketChannel.shutdownOutput()方法,可以在不关闭通道的情况下关闭 I/O 连接。关闭输入(或读取)连接将通过返回流结束指示符–1 来拒绝任何进一步的读取尝试。关闭输出(或写)连接将通过抛出一个ClosedChannelException异常来拒绝任何写尝试。

//shut down connection for reading
socketChannel.shutdownInput();

//shut down connection for writing
socketChannel.shutdownOutput();

如果您想在不关闭通道的情况下拒绝读/写尝试,这些方法非常有用。使用下面的代码可以检查某个连接当前是否因 I/O 而关闭:

boolean inputdown = socketChannel.socket().isInputShutdown();
boolean outputdown = socketChannel.socket().isOutputShutdown();
关闭频道

当一个通道变得无用时,它必须被关闭。为此,您可以调用SocketChannel.close()方法(这不会关闭服务器来监听传入的连接,它只是关闭一个客户端的通道)和/或ServerSocketChannel.close()方法(这将关闭服务器来监听传入的连接;其他客户端将无法再定位该服务器)。

serverSocketChannel.close();
socketChannel.close();

或者,我们可以通过将代码放入 Java 7 的 try-with-resources 特性来关闭这些资源——这是可能的,因为ServerSocketChannelSocketChannel类实现了AutoCloseable接口。使用此功能将确保资源自动关闭。如果您不熟悉 try-with-resources 特性,请查看[download.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html](http://download.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html)

将所有这些放入 Echo 服务器

现在我们已经拥有了创建 echo 服务器所需的一切。将前面的块放在一起,添加必要的导入和意大利面条代码,等等,将为我们提供下面的 echo 服务器:

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

public class Main {

 public static void main(String[] args) {

  final int DEFAULT_PORT = 5555;
  final String IP = "127.0.0.1";

  ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

  //create a new server socket channel
  try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {

       //continue if it was successfully created
       if (serverSocketChannel.isOpen()) {

           //set the blocking mode
           serverSocketChannel.configureBlocking(true);
           //set some options
           serverSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
           serverSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
           //bind the server socket channel to local address
           serverSocketChannel.bind(new InetSocketAddress(IP, DEFAULT_PORT));

           //display a waiting message while ... waiting clients
           System.out.println("Waiting for connections ...");

           //wait for incoming connections
           while(true){
            try (SocketChannel socketChannel = serverSocketChannel.accept()) {
                System.out.println("Incoming connection from: " +
                                                            socketChannel.getRemoteAddress());

                //transmitting data
                while (socketChannel.read(buffer) != -1) {

                       buffer.flip();

                       socketChannel.write(buffer);

                       if (buffer.hasRemaining()) {
                           buffer.compact();
                       } else {
                           buffer.clear();
                       }
                }
            } catch (IOException ex) {                    
            }
           }

       } else {
         System.out.println("The server socket channel cannot be opened!");
       }
  } catch (IOException ex) {
    System.err.println(ex);
  }
}
}

编写阻塞 TCP 客户端

没有客户端的服务器有什么用?我们不想找出这个问题的答案,所以让我们为我们的 echo 服务器开发一个客户机。假设以下场景:客户机连接到我们的服务器,发送一个“Hello!”消息,然后继续发送 0 到 100 之间的随机数,直到生成数字 50。当生成数字 50 时,客户端停止发送并关闭通道。服务器将回显(写回)它从客户端读取的所有内容。现在我们有了一个场景,让我们看看实现它的步骤。

创建新的套接字通道

第一步是为面向流的连接套接字创建一个可选通道。这是通过java.nio.channels.SocketChannel类完成的,它对于多个并发线程来说是安全的。更准确地说,这个任务是通过SocketChannel.open()方法完成的,如下所示:

SocketChannel socketChannel = SocketChannel.open();

请记住,新创建的套接字通道没有连接。在单个镜头中创建和连接一个套接字通道需要调用SocketChannel.open(SocketAddress)方法。正如我们将要讨论的,也可以分两步完成。

您可以通过调用SocketChannel.isOpen()方法来检查服务器套接字是否已经打开或者已经成功打开,该方法返回相应的Boolean值:

if (socketChannel.isOpen()) {
    ...
}
配置阻塞机制

如果套接字通道已经成功打开,就该指定阻塞机制了。我们将传递真值,因为我们想要激活阻塞机制:

socketChannel.configureBlocking(true);
设置套接字通道选项

一个插座通道支持以下选项:SO_RCVBUFSO_LINGERIP_TOSSO_OOBINLINESO_REUSEADDRTCP_NODELAYSO_KEEPALIVESO_SNDBUF。其中一些如下所示:

socketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 128 * 1024);
socketChannel.setOption(StandardSocketOptions.SO_SNDBUF, 128 * 1024);
socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
socketChannel.setOption(StandardSocketOptions.SO_LINGER, 5);

您可以通过调用继承的方法supportedOptions()找到服务器套接字通道支持的选项:

Set<SocketOption<?>> options = socketChannel.supportedOptions();
for(SocketOption<?> option : options) System.out.println(option);
连接通道的插座

打开套接字通道(并可选地绑定它)后,您应该连接到远程地址(服务器端地址)。由于我们处于阻塞模式,连接到远程地址将阻塞应用,直到新的连接可用或发生 I/O 错误。通过调用SocketChannel.connect()方法并向其传递远程地址作为InetSocketAddress的一个实例来表明连接的意图,如下所示(记住我们的 echo 服务器运行在 127.0.0.1,端口 5555 上):

final int DEFAULT_PORT = 5555;
final String IP = "127.0.0.1";
socketChannel.connect(new InetSocketAddress(IP, DEFAULT_PORT));

该方法返回一个代表成功连接尝试的boolean值。您可以使用这个布尔值来检查连接的可用性,直到通过这个连接发送/接收数据包。此外,同样的检查可以通过调用SocketChannel.isConnected()方法来完成,如下所示:

if (socketChannel.isConnected()) {
    ...
}

Image 注意显然,在现实世界中,在应用中硬编码 IP 地址被认为是一种不好的做法。在这种情况下,客户端将只能与服务器在同一台机器上运行,这在某种程度上违背了远程通信的目的。在您的情况下,客户端可能使用服务器的主机名,而不是 IP 地址(可能通过 DNS 配置)。IP 地址经常变化,有时甚至通过 DHCP 动态分配。

通过连接传输数据

连接已经建立,所以我们可以开始传输数据包。下面的代码发送“Hello!”消息,然后发送随机数,直到生成数字 50。我们使用了ByteBufferCharBufferSocketChannel类的read()/write()方法(我们之前在开发服务器端代码时已经列出了这些方法,所以您应该已经熟悉了):

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
ByteBuffer helloBuffer = ByteBuffer.wrap("Hello !".getBytes());
ByteBuffer randomBuffer;
CharBuffer charBuffer;
Charset charset = Charset.defaultCharset();
CharsetDecoder decoder = charset.newDecoder();
...
socketChannel.write(helloBuffer);

while (socketChannel.read(buffer) != -1) {

       buffer.flip();

       charBuffer = decoder.decode(buffer);
       System.out.println(charBuffer.toString());

       if (buffer.hasRemaining()) {
           buffer.compact();
       } else {
           buffer.clear();
       }

       int r = new Random().nextInt(100);
       if (r == 50) {
           System.out.println("50 was generated! Close the socket channel!");
           break;
       } else {
           randomBuffer = ByteBuffer.wrap("Random number:"
                                           .concat(String.valueOf(r)).getBytes());
           socketChannel.write(randomBuffer);
       }
}
关闭频道

当一个通道变得无用时,它必须被关闭。为此,您可以调用SocketChannel.close(),客户端将与服务器断开连接:

socketChannel.close();

同样,Java 7 try-with-resources 特性可用于自动关闭。

将所有这些放入客户端

现在我们拥有了创建客户所需的一切。将所有需要的元素放在一起将为我们提供以下客户:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Random;

public class Main {

 public static void main(String[] args) {

  final int DEFAULT_PORT = 5555;
  final String IP = "127.0.0.1";

  ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
  ByteBuffer helloBuffer = ByteBuffer.wrap("Hello !".getBytes());
  ByteBuffer randomBuffer;
  CharBuffer charBuffer;
  Charset charset = Charset.defaultCharset();
  CharsetDecoder decoder = charset.newDecoder();

  //create a new socket channel
  try (SocketChannel socketChannel = SocketChannel.open()) {

       //continue if it was successfully created
       if (socketChannel.isOpen()) {                

           //set the blocking mode
           socketChannel.configureBlocking(true);
           //set some options
           socketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 128 * 1024);
           socketChannel.setOption(StandardSocketOptions.SO_SNDBUF, 128 * 1024);
           socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
           socketChannel.setOption(StandardSocketOptions.SO_LINGER, 5);
           //connect this channel's socket
           socketChannel.connect(new InetSocketAddress(IP, DEFAULT_PORT));

           //check if the connection was successfully accomplished
           if (socketChannel.isConnected()) {

               //transmitting data
               socketChannel.write(helloBuffer);

               while (socketChannel.read(buffer) != -1) {

                      buffer.flip();

                      charBuffer = decoder.decode(buffer);
                      System.out.println(charBuffer.toString());

                      if (buffer.hasRemaining()) {
                          buffer.compact();
                      } else {
                          buffer.clear();
                      }

                      int r = new Random().nextInt(100);
                      if (r == 50) {
                          System.out.println("50 was generated! Close the socket channel!");
                          break;
                      } else {
                          randomBuffer = ByteBuffer.wrap("Random number:".
                                                        concat(String.valueOf(r)).getBytes());
                          socketChannel.write(randomBuffer);
                      }
                }

           } else {
             System.out.println("The connection cannot be established!");
           }
       } else {
         System.out.println("The socket channel cannot be opened!");
       }
  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}

测试阻塞回显应用

测试应用是一项简单的任务。首先,启动服务器,等待直到您看到消息“正在等待连接...”。继续启动客户端并检查输出。以下是一些可能的服务器输出:


Waiting for connections ...

Incoming connection from: /127.0.0.1:49911

下面是一些可能的客户端输出:


Hello !

Random number:71

Random number:60

Random number:22

Random number:4

Random number:60

Random number:13

...

50 was generated! Close the socket channel!

编写一个非阻塞的 TCP 客户机/服务器应用

在我们开始开发之前,让我们对非阻塞 API 做一个简短的概述,它从 NIO 开始就可用了,所以它对您来说不应该是全新的。记住这一点,我们不会对你可能已经知道的事情进行过多的描述。

非阻塞套接字模式就是允许在通道上进行 I/O 操作,而不阻塞使用它的进程。故事从阻塞应用开始:服务器端是开放的,绑定到一个本地地址,并从客户端接收请求,客户端显然是开放的,连接到远程地址,并向服务器发送请求。

当所有非阻塞技术的主要实体——java.nio.channels.Selector类出现时,事情开始变得疯狂。一个Selector是通过一个无参数的open()方法创建的(Selector在 Java 7 中没有被修改)。基本上,这个类能够识别一个或多个通道何时可用于数据传输,并序列化请求以帮助服务器满足其客户端(它监视每个记录的套接字通道)。

此外,Selector在一个线程中处理多个套接字的 I/O 读/写操作,这要归功于一个被称为多路复用的概念——这解决了为每个套接字连接分配一个线程的问题。在 API 术语中,Selectorjava.nio.channels.SelectableChannels多路复用器,它可以通过register()方法注册(在ServerSocketChannelSocketChannel类中可用,它们是SelectableChannel)的间接子类,通过解除Selector分配给通道的资源来取消注册)。

使用 SelectionKey 类

如果你还在正轨上,那我们再深入一点!每次用Selector注册通道时,它都通过java.nio.channels.SelectionKey类的一个实例来表示,这些实例被称为选择键— Java 7 不修改这个类。可以把键想象成选择器用来对客户机请求进行排序的助手——每个助手(键)代表一个客户机子请求,包含识别客户机和请求类型(连接、读、写等)的信息。).注册时,我们指出了选择器,通常还指出了结果键的兴趣集(兴趣集标识了由Selector监控的键通道的操作)。密钥有四种可能的类型:

  • SelectionKey.OP_ACCEPT ( 可接受):关联的客户端请求连接(通常在服务器端创建,用于指示客户端需要连接)。
  • SelectionKey.OP_CONNECT ( 可连接):服务器接受连接(通常在客户端创建)。
  • SelectionKey.OP_READ ( 可读):表示读操作。
  • SelectionKey.OP_WRITE ( 可写):表示写操作。

选择器负责维护三组选择键:

  • key-set:包含代表该选择器当前频道注册的按键
  • selected-key:包含一组键,使得每个键的通道被检测到准备好用于在先前的选择操作期间在该键的兴趣组中识别的至少一个操作
  • cancelled-key:包含已取消但频道尚未注销的按键集合

Image 注意在新创建的选择器中,三个集合都是空的。选择器本身对于多个并发线程来说是安全的,但是它们的键集却不安全。

当战场上发生一些事情时,选择器醒来并创建相应的键(SelectionKey类的实例)。每个键保存关于发出请求的应用和请求类型(尝试/接受连接和读/写操作)的信息。

选择器等待进入无限循环的连接(等待选择器上记录的事件)。通常Selector.select()方法是循环中的第一行,它阻塞应用,直到至少选择了一个通道,调用了选择器的Selector.wakeup()方法,或者中断了当前线程——无论哪一个先发生。(此外,还有一个“select()超时”方法,以及一个名为selectNow().的非阻塞方法)

Selector等待客户机尝试连接,当这种情况发生时,服务器应用获得选择器创建的密钥。对于每个键,它检查类型(通过显式调用键上迭代器的remove()方法,从集合中删除每个处理过的键——这将防止相同的键再次出现)。这里搜索可接受的键,当SelectionKey.isAcceptable()方法返回 true 时,服务器通过调用accept()方法定位客户端套接字通道,将其设置为非阻塞,并使用OP_READ and/or OP_WRITE选项将其注册到选择器。

此时,客户端套接字通道注册到选择器以进行读/写操作。为了保持这种趋势,当客户端在套接字通道上写入数据时,选择器将告诉服务器有一些数据要读取——为此,SelectionKey.isReadable()方法返回 true。如果客户端试图从服务器读取数据,过程是类似的,但是服务器改为写入数据,并且SelectionKey.isWritable()方法返回true

图 8-1 显示了一个无阻塞流程图。

Image

图 8-1。选择器基础无阻塞流动。

所以,服务器已经准备好了!

Image 注意在非阻塞模式下,I/O 操作传输的字节可能比请求的少(部分读或写),或者可能根本没有字节。

使用选择器的方法

接下来,我们将回顾本节中调用的方法,以及接下来概述的其他一些方法(以下大部分描述摘自官方的 Java 7 Javadoc)。

  • Selector.open():创建一个新的选择器。
  • Selector.select():通过执行阻止选择操作来选择一组按键。
  • Selector.select(t):与选择相同,但仅在指定的毫秒内执行阻塞。如果时间到了,但没有可供选择的内容,则返回 0。
  • Selector.selectNow():与 select 相同,但具有非阻塞选择操作。如果没有可供选择的内容,它将返回 0。
  • Selector.selectedKeys():返回该选择器选择的按键集合为 Set < SelectionKey >。
  • Selector.keys():返回该选择器的按键设置为 Set < SelectionKey >。
  • Selector.wakeup():使尚未返回的第一次选择操作立即返回。
  • SelectionKey.isValid():检查密钥是否有效。如果键被取消、其通道被关闭或其选择器被关闭,则该键无效。
  • SelectionKey.isReadable():测试该键的通道是否可以读取。
  • SelectionKey.isWritable():测试该键的通道是否准备好写入。
  • SelectionKey.isAcceptable():测试这个键的通道是否准备好接受新的套接字连接。
  • SelectionKey.isConnectable():测试该按键的通道是否已经完成或者未能完成其套接字连接操作。
  • SelectionKey.cancel():请求取消该键的通道与其选择器的注册。
  • SelectionKey.interestOps():检索该键的兴趣集。
  • SelectionKey.interestOps(t):将该键的兴趣集设置为给定值。
  • SelectionKey.readyOps():检索该键的就绪操作设置。

此外,ServerSocketChannelSocketChannel包含register()方法,用于向给定的选择器注册当前频道并返回选择键。它获得选择器、结果键的兴趣集和结果键的附件(可能为空)。

public final SelectionKey register(Selector s,int p,Object a) throws ClosedChannelException
编写服务器

基于这些方法和前面的讨论,我们编写了下面的非阻塞 echo 服务器(对每个步骤都进行了注释,以帮助您更好地理解):

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
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.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class Main {

 private Map<SocketChannel, List<byte[]>> keepDataTrack = new HashMap<>();
 private ByteBuffer buffer = ByteBuffer.allocate(2 * 1024);

 private void startEchoServer() {

  final int DEFAULT_PORT = 5555;

  //open Selector and ServerSocketChannel by calling the open() method
  try (Selector selector = Selector.open();
       ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {

       //check that both of them were successfully opened
       if ((serverSocketChannel.isOpen()) && (selector.isOpen())) {

            //configure non-blocking mode
            serverSocketChannel.configureBlocking(false);

            //set some options
            serverSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 256 * 1024);
            serverSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);

            //bind the server socket channel to port
            serverSocketChannel.bind(new InetSocketAddress(DEFAULT_PORT));

            //register the current channel with the given selector  
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            //display a waiting message while ... waiting!
            System.out.println("Waiting for connections ...");

            while (true) {
                   //wait for incomming events
                   selector.select();

                   //there is something to process on selected keys
                   Iterator keys = selector.selectedKeys().iterator();

                   while (keys.hasNext()) {
                          SelectionKey key = (SelectionKey) keys.next();

                          //prevent the same key from coming up again
                          keys.remove();

                          if (!key.isValid()) {
                              continue;
                          }

                          if (key.isAcceptable()) {
                              acceptOP(key, selector);
                          } else if (key.isReadable()) {
                              this.readOP(key);
                          } else if (key.isWritable()) {
                              this.writeOP(key);
                          }
                   }
            }
       } else {
         System.out.println("The server socket channel or selector cannot be opened!");
       }

  } catch (IOException ex) {
    System.err.println(ex);
  }
 }

 //isAcceptable returned true
 private void acceptOP(SelectionKey key, Selector selector) throws IOException {

  ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
  SocketChannel socketChannel = serverChannel.accept();
  socketChannel.configureBlocking(false);

  System.out.println("Incoming connection from: " + socketChannel.getRemoteAddress());

  //write a welcome message
  socketChannel.write(ByteBuffer.wrap("Hello!\n".getBytes("UTF-8")));

  //register channel with selector for further I/O
  keepDataTrack.put(socketChannel, new ArrayList<byte[]>());
  socketChannel.register(selector, SelectionKey.OP_READ);
  }

  //isReadable returned true
  private void readOP(SelectionKey key) {

   try {
       SocketChannel socketChannel = (SocketChannel) key.channel();

       buffer.clear();

       int numRead = -1;
       try {
           numRead = socketChannel.read(buffer);
       } catch (IOException e) {
         System.err.println("Cannot read error!");
       }

       if (numRead == -1) {
           this.keepDataTrack.remove(socketChannel);
           System.out.println("Connection closed by: " + socketChannel.getRemoteAddress());
           socketChannel.close();
           key.cancel();
           return;
       }

       byte[] data = new byte[numRead];
       System.arraycopy(buffer.array(), 0, data, 0, numRead);
       System.out.println(new String(data, "UTF-8") + " from " +      
                                                        socketChannel.getRemoteAddress());

       // write back to client
       doEchoJob(key, data);

   } catch (IOException ex) {
     System.err.println(ex);
   }
 }

 //isWritable returned true
 private void writeOP(SelectionKey key) throws IOException {

  SocketChannel socketChannel = (SocketChannel) key.channel();

  List<byte[]> channelData = keepDataTrack.get(socketChannel);
  Iterator<byte[]> its = channelData.iterator();

  while (its.hasNext()) {
         byte[] it = its.next();
         its.remove();
         socketChannel.write(ByteBuffer.wrap(it));
  }

  key.interestOps(SelectionKey.OP_READ);
 }

 private void doEchoJob(SelectionKey key, byte[] data) {

  SocketChannel socketChannel = (SocketChannel) key.channel();
  List<byte[]> channelData = keepDataTrack.get(socketChannel);
  channelData.add(data);

  key.interestOps(SelectionKey.OP_WRITE);
 }

 public static void main(String[] args) {
  Main main = new Main();
  main.startEchoServer();
 }
}
编写客户端

关注客户端,结构几乎是相同的,只有一些不同:

  • 首先,用SelectionKey.OP_CONNECT选项注册客户机套接字通道,因为客户机希望在服务器接受连接时得到选择器的通知。
  • 第二,客户端不会无限尝试连接,因为服务器可能不是活动的;因此,带有超时的Selector.select()方法对它来说是合适的(500 到 1000 毫秒的超时将完成这项工作)。
  • 第三,客户端必须检查密钥是否可连接(即SelectionKey.isConnectable()方法是否返回true)。如果这个键是可连接的,它会在一个条件语句中混合使用套接字通道isConnectionPending()finishConnect()方法来关闭挂起的连接。当您需要判断这个通道上是否正在进行连接操作时,调用SocketChannel.isConnectionPending()方法,该方法返回一个Boolean值。此外,通过SocketChannel.finishConnect()方法可以完成连接插座通道的过程。

最后,客户机为 I/O 操作做好了准备。我们重现了阻塞客户机/服务器应用中的相同场景:客户机连接到我们的服务器并发送一个“Hello!”消息,然后继续发送 0 到 100 之间的随机数,直到生成数字 50。生成 50 时,客户端停止发送并关闭通道。服务器将回显(写回)它从客户端读取的所有内容。

import java.io.IOException;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;

public class Main {

 public static void main(String[] args) {

  final int DEFAULT_PORT = 5555;
  final String IP = "127.0.0.1";

  ByteBuffer buffer = ByteBuffer.allocateDirect(2 * 1024);
  ByteBuffer randomBuffer;
  CharBuffer charBuffer;

  Charset charset = Charset.defaultCharset();
  CharsetDecoder decoder = charset.newDecoder();

  //open Selector and ServerSocketChannel by calling the open() method
  try (Selector selector = Selector.open();
       SocketChannel socketChannel = SocketChannel.open()) {

       //check that both of them were successfully opened
       if ((socketChannel.isOpen()) && (selector.isOpen())) {                

            //configure non-blocking mode
            socketChannel.configureBlocking(false);
            //set some options

            socketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 128 * 1024);
            socketChannel.setOption(StandardSocketOptions.SO_SNDBUF, 128 * 1024);
            socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);

            //register the current channel with the given selector
            socketChannel.register(selector, SelectionKey.OP_CONNECT);

            //connect to remote host
            socketChannel.connect(new java.net.InetSocketAddress(IP, DEFAULT_PORT));

            System.out.println("Localhost: " + socketChannel.getLocalAddress());

            //waiting for the connection
            while (selector.select(1000) > 0) {

                   //get keys
                   Set keys = selector.selectedKeys();
                   Iterator its = keys.iterator();

                   //process each key
                   while (its.hasNext()) {
                          SelectionKey key = (SelectionKey) its.next();

                          //remove the current key
                          its.remove();

                          //get the socket channel for this key
                          try (SocketChannel keySocketChannel=(SocketChannel) key.channel()) {

                               //attempt a connection
                               if (key.isConnectable()) {

                                   //signal connection success
                                   System.out.println("I am connected!");

                                   //close pending connections
                                   if (keySocketChannel.isConnectionPending()) {
                                       keySocketChannel.finishConnect();
                                   }

                                   //read/write from/to server
                                   while (keySocketChannel.read(buffer) != -1) {

                                          buffer.flip();

                                          charBuffer = decoder.decode(buffer);
                                          System.out.println(charBuffer.toString());

                                          if (buffer.hasRemaining()) {
                                              buffer.compact();
                                          } else {
                                              buffer.clear();

                                          }

                                          int r = new Random().nextInt(100);
                                          if (r == 50) {
                                              System.out.println("50 was generated! Close
                                                                    the socket channel!");
                                              break;
                                          } else {
                                            randomBuffer = ByteBuffer.wrap("Random number:"  
                                             .concat(String.valueOf(r)).getBytes("UTF-8"));
                                            keySocketChannel.write(randomBuffer);
                                            try {
                                                Thread.sleep(1500);
                                            } catch (InterruptedException ex) {                                            
                                            }
                                          }
                                   }
                               }
                          } catch (IOException ex) {
                            System.err.println(ex);
                          }
                   }
            }
       } else {
         System.out.println("The socket channel or selector cannot be opened!");
       }
  } catch (IOException ex) {
    System.err.println(ex);
  }

  }
}
测试无阻塞回显应用

测试应用是一项简单的任务。首先,启动服务器,等待直到您看到消息“正在等待连接…”继续启动一组客户机并检查输出。图 8-2 显示了一个运行服务器和三个客户端实例的例子。

Image

图 8-2。非阻塞服务器回显应用输出。

图 8-3 显示了客户端 2 的输出。

Image

图 8-3。非阻塞客户端回显应用输出

请记住,即使它看起来像一个多线程应用,这也是一个基于多路复用技术的单线程应用。

编写 UDP 服务器/客户端应用

既然 TCP 已经有了它的辉煌时刻,现在是 UDP 引起我们注意的时候了。UDP 建立在 IP 之上,有几个重要的特征。首先,包的大小被限制在单个 IP 包所能容纳的数量内——最多 65507 字节;这是 65535 字节的 IP 数据包大小减去 20 字节的最小 IP 报头,再减去 8 字节的 UDP 报头。此外,每个数据包都是独立的,被单独处理(没有数据包知道其他数据包)。此外,数据包可以以任何顺序到达,其中一些可能会在发送者不知情的情况下丢失,或者它们到达的速度比处理速度快或慢——不能保证以特定的顺序发送/接收数据,也不能保证发送的数据会被接收到。

由于发送方无法跟踪数据包的路由,每个数据包都封装了远程 IP 地址和端口。如果说 TCP 像一部电话,那么 UDP 就像一封信。发送方将接收方地址(远程 IP 和端口)和发送方地址(本地 IP 和端口)写在信封(UDP 包)上,将信件(要发送的数据)放入信封,然后发送信件。他不知道这封信是否会到达收信人那里。此外,较新的信可能比旧的信到达得快,而信可能根本就不会到达——这些信彼此并不知道。请记住,TCP 用于高可靠性的数据传输,而 UDP 用于低开销的传输。通常,在可靠性不重要但速度重要的应用中使用 UDP。当顺序不重要并且您不需要将所有消息发送到另一台机器时,UDP 非常适合从一个系统向另一个系统发送消息。

在下一节中,我们将编写一个基于 UDP 的单线程阻塞客户机/服务器应用。我们将从服务器端开始。

编写 UDP 服务器

为了帮助您理解,我们将把开发过程分成几个独立的步骤,并将 NIO.2 旨在提高性能和简化开发的特性放在前面。同样,我们将编写一个 echo 服务器和一个客户机,向它发送一些文本并接收它的返回。

创建面向服务器数据报的套接字通道

编写客户机/服务器 UDP 应用的整个过程涉及到java.nio.channels.DatagramChannel类,它代表面向数据报套接字的线程安全可选通道。因此,我们将通过创建一个新的DatagramChannel来启动我们的服务器,这可以通过调用 NIO.2 DatagramChannel.open()方法来完成。这个方法得到一个称为协议 参数的参数,它实际上是一个java.net.ProtocolFamily对象。这个接口是 NIO.2 中的新特性,它代表了一系列通信协议——目前它有一个名为java.net.StandardProtocolFamily的实现,并定义了两个枚举常量:

  • StandardProtocolFamily.INETIP 版本 4 (IPv4)
  • StandardProtocolFamily.INET6IP 版本 6 (IPv6)

因此,我们可以为 IPv4 创建一个面向服务器数据报的套接字,如下所示:

DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET);

旧的 NIO 无参数DatagramChannel.open()方法仍然可用,并且可以使用,因为它没有被废弃。但是在这种情况下,通道套接字的ProtocolFamily依赖于平台(配置),因此是未指定的。

您可以通过调用DatagramChannel.isOpen()方法来检查面向数据报的套接字通道是否已经打开或者已经成功打开,该方法返回相应的Boolean值:

if (datagramChannel.isOpen()) {
    ...
}

可以用同样的方式创建和检查面向客户端数据报的套接字通道。

设置面向数据报的套接字通道选项

面向数据报的套接字通道支持以下选项(尽管大多数情况下可以使用默认值):SO_REUSEADDRSO_BROADCASTIP_MULTICAST_LOOPSO_SNDBUFIP_MULTICAST_TTLIP_TOSIP_MULTICAST_IFSO_RCVBUF。例如,我们可以将网络实施使用的输入和输出缓冲区设置如下:

datagramChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
datagramChannel.setOption(StandardSocketOptions.SO_SNDBUF, 4 * 1024);

注意,您可以通过调用继承的方法supportedOptions()找到面向数据报的套接字通道的支持选项:

Set<SocketOption<?>> options = datagramChannel.supportedOptions();
for(SocketOption<?> option : options) System.out.println(option);
绑定面向数据报的套接字通道

此时,我们可以将通道的套接字绑定到本地地址,并配置套接字来侦听连接。为此,我们称之为新的DatagramChannel.bind()方法(该方法在前面的“网络通道概述”一节中介绍过)。我们的服务器将在本地主机(127.0.0.1),端口 5555(任意选择)上等待传入的连接:

final int LOCAL_PORT = 5555;
final String LOCAL_IP = "127.0.0.1";
datagramChannel.bind(new InetSocketAddress(LOCAL_IP, LOCAL_PORT));

也可以使用通配符地址:

datagramChannel.bind(new InetSocketAddress(LOCAL_PORT));

如果我们将 null 传递给bind()方法,也可以自动分配本地地址。还可以通过调用从NetworkChannel接口继承的ServerSocketChannel.getLocalAddress()方法找出绑定的本地地址。如果服务器套接字通道尚未绑定,则返回 null。

System.out.println(serverSocketChannel.getLocalAddress());
传输数据包

此时,我们的服务器已经准备好接收和发送数据包。由于 UDP 是一种无连接的网络协议,您不能像从其他通道那样默认读写一个DatagramChannel——稍后,您将看到如何通过 UDP 建立连接。相反,你使用DatagramChannel.send()DatagramChannel.receive()方法发送和接收数据包。

当您发送一个包时,您向send()方法传递一个ByteBuffer,它包含珍贵的数据和远程地址(服务器或客户机的,取决于谁在发送)。根据官方文档,这是如何工作的(参见[download.oracle.com/javase/7/docs/api/)](http://download.oracle.com/javase/7/docs/api/)):

如果该通道处于非阻塞模式,并且底层输出缓冲区中有足够的空间,或者如果该通道处于阻塞模式,并且有足够的空间可用,那么给定缓冲区中的剩余字节将作为单个数据报传输到给定的目标地址。这个方法可以在任何时候调用。但是,如果另一个线程已经在这个通道上启动了写操作,那么这个方法的调用将被阻塞,直到第一个操作完成。如果这个通道的套接字没有绑定,那么这个方法将首先使套接字绑定到一个自动分配的地址,就像通过调用带有参数nullbind()方法一样。

该方法将返回发送的字节数。

当您接收到一个包时,您向receive()方法传递数据报将被传输到的缓冲区(ByteBuffer)。同样,根据文档,它是这样工作的(参见[download.oracle.com/javase/7/docs/api/](http://download.oracle.com/javase/7/docs/api/)):

如果一个数据报立即可用,或者如果该通道处于阻塞模式并且最终有一个数据报可用,那么该数据报被复制到给定的字节缓冲区,并且其源地址被返回。如果该通道处于非阻塞模式,并且数据报不立即可用,则该方法立即返回 null。这个方法可以在任何时候调用。但是,如果另一个线程已经在这个通道上启动了一个读操作,那么这个方法的调用将被阻塞,直到第一个操作完成。如果这个通道的套接字没有绑定,那么这个方法将首先使套接字绑定到一个自动分配的地址,就像通过调用带有参数nullbind()方法一样。

该方法将返回数据报的源地址,或者如果该通道处于非阻塞模式并且没有数据报立即可用,则返回null。远程地址可用于找出向何处发送应答包。

另外,可以通过调用DatagramChannel.getRemoteAddress()方法找出远程地址。这个方法是 Java 7 (NIO.2)中的新方法,它返回这个通道的套接字所连接的远程地址——记住,对于 UDP 无连接的情况,这个方法返回null:

System.out.println("Connected to: " + datagramChannel.getRemoteAddress());

我们的数据报回显服务器将以阻塞模式(默认)在无限循环中监听传入的数据包,当数据包到达时,它将从中提取远程地址和数据。数据根据远程地址发送回来:

final int MAX_PACKET_SIZE = 65507;
ByteBuffer echoText = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);
...
while (true) {

       SocketAddress clientAddress = datagramChannel.receive(echoText);

       echoText.flip();
       System.out.println("I have received " + echoText.limit() + " bytes from " +
                                        clientAddress.toString() + "! Sending them back ...");
       datagramChannel.send(echoText, clientAddress);
       echoText.clear();
}
关闭数据报通道

当数据报通道变得无用时,必须将其关闭。为此,您可以调用DatagramChannel.close()方法:

datagramChannel.close();

同样,Java 7 的 try-with-resources 特性可以用于自动关闭。

将所有内容放入服务器

现在我们已经拥有了创建服务器所需的一切。将前面的所有信息放在一起,我们将得到以下服务器:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.StandardProtocolFamily;
import java.nio.channels.DatagramChannel;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;

public class Main {

public static void main(String[] args) {
  final int LOCAL_PORT = 5555;
  final String LOCAL_IP = "127.0.0.1";  //modify this to your local IP      
  final int MAX_PACKET_SIZE = 65507;

  ByteBuffer echoText = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);

  //create a new datagram channel
  try (DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET)) {

       //check if the channel was successfully opened
       if (datagramChannel.isOpen()) {

           System.out.println("Echo server was successfully opened!");
           //set some options
           datagramChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
           datagramChannel.setOption(StandardSocketOptions.SO_SNDBUF, 4 * 1024);
           //bind the channel to local address
           datagramChannel.bind(new InetSocketAddress(LOCAL_IP, LOCAL_PORT));
           System.out.println("Echo server was binded on:"+datagramChannel.getLocalAddress());
           System.out.println("Echo server is ready to echo ...");

           //transmitting data packets
           while (true) {

                  SocketAddress clientAddress = datagramChannel.receive(echoText);

                  echoText.flip();
                  System.out.println("I have received " + echoText.limit() + " bytes from " +
                                        clientAddress.toString() + "! Sending them back ...");
                  datagramChannel.send(echoText, clientAddress);
                  echoText.clear();
           }
       } else {
         System.out.println("The channel cannot be opened!");
       }
  } catch (Exception ex) {
           if (ex instanceof ClosedChannelException) {
               System.err.println("The channel was unexpected closed ...");
           }
           if (ex instanceof SecurityException) {
               System.err.println("A security exception occured ...");
           }
           if (ex instanceof IOException) {
               System.err.println("An I/O error occured ...");
           }

           System.err.println("\n" + ex);
  }
 }
}

编写无连接 UDP 客户端

编写无连接的 UDP 客户端类似于编写 UDP 服务器。在以与前面相同的方式创建了一个新的DatagramChannel并设置了您需要的任何选项之后,您就可以开始发送和接收数据包了。面向客户端数据报的套接字通道不必绑定到本地地址,因为服务器将从每个收到的数据包中提取 IP 地址和端口,换句话说,它知道客户端住在哪里。此外,如果这个通道的套接字没有绑定,那么send()receive()方法将首先使套接字(客户端或服务器)绑定到一个自动分配的地址,就像通过调用带有参数nullbind()方法一样。但是请记住,如果服务器端是自动绑定的(不是显式绑定的),那么客户端应该知道所选择的地址(或者更准确地说,知道所选择的 IP 地址和端口)。如果服务器发送第一个数据分组,反之亦然。

我们的客户知道服务器的地址是 127.0.0.1,端口是 5555;因此,它发送第一个数据包,并从中接收答案。这是代码:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardProtocolFamily;
import java.nio.channels.DatagramChannel;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

public class Main {

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

  final int REMOTE_PORT = 5555;
  final String REMOTE_IP = "127.0.0.1"; //modify this accordingly if you want to test remote
  final int MAX_PACKET_SIZE = 65507;

  CharBuffer charBuffer = null;
  Charset charset = Charset.defaultCharset();
  CharsetDecoder decoder = charset.newDecoder();
  ByteBuffer textToEcho = ByteBuffer.wrap("Echo this: I'm a big and ugly server!".getBytes());
  ByteBuffer echoedText = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);

  //create a new datagram channel
  try (DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET)) {

       //check if the channel was successfully opened
       if (datagramChannel.isOpen()) {

           //set some options
           datagramChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
           datagramChannel.setOption(StandardSocketOptions.SO_SNDBUF, 4 * 1024);

           //transmitting data packets
           int sent = datagramChannel.send(textToEcho,
                                           new InetSocketAddress(REMOTE_IP, REMOTE_PORT));
           System.out.println("I have successfully sent "+sent+ " bytes to the Echo Server!");

           datagramChannel.receive(echoedText);

           echoedText.flip();
           charBuffer = decoder.decode(echoedText);
           System.out.println(charBuffer.toString());
           echoedText.clear();

       } else {
         System.out.println("The channel cannot be opened!");

       }
  } catch (Exception ex) {
    if (ex instanceof ClosedChannelException) {
        System.err.println("The channel was unexpected closed ...");
    }
    if (ex instanceof SecurityException) {
        System.err.println("A security exception occured ...");
    }
    if (ex instanceof IOException) {
        System.err.println("An I/O error occured ...");
    }

    System.err.println("\n" + ex);
  }
 }
}

测试 UDP 无连接回显应用

测试应用是一项简单的任务。首先,启动服务器并等待,直到您看到以下消息:

Echo server was successfully opened!
Echo server was binded on: /127.0.0.1:5555
Echo server is ready to echo ...

然后启动客户机并检查输出。以下是 UDP 服务器的一些可能输出:


Echo server was successfully opened!

Echo server was binded on: /127.0.0.1:5555

Echo server is ready to echo ...

I have received 37 bytes from /127.0.0.1:49155! Sending them back ...

下面是一些可能的 UDP 客户端输出:


I have successfully sent 37 bytes to the Echo Server!

Echo this: I'm a big and ugly server!

Image 注意完成测试后不要忘记手动停止 UDP 服务器!

编写连接的 UDP 客户端

如果你想使用DatagramChannel.read()DatagramChannel.write()方法(基于ByteBuffer s),而不是send()receive(),你需要写一个连接的 UDP 客户端。在连接客户端的场景中,通道的套接字被配置为只从/向给定的远程对等地址接收/发送数据报。连接建立后,数据包可能无法从任何其他地址接收/发送到任何其他地址。面向数据报的套接字保持连接,直到它被显式断开或关闭。

这种类型的客户端必须显式调用DatagramChannel.connect()方法,并向其传递服务器端远程地址,如下所示:

final int REMOTE_PORT = 5555;
final String REMOTE_IP = "127.0.0.1";
datagramChannel.connect(new InetSocketAddress(REMOTE_IP, REMOTE_PORT));

注意,与SocketChannel.connect()方法不同,这种方法实际上并不通过网络发送/接收任何数据包,因为 UDP 是一种无连接协议——这种方法返回非常快,并且不会阻塞应用。这里不需要一个finishConnect()或者isConnectionPending()方法。此方法可以在任何时候调用,因为它不会影响调用时已经在进行的读/写操作。如果这个通道的套接字没有被绑定,那么这个方法将首先导致套接字被绑定到一个自动分配的地址,就像调用带有参数nullbind()方法一样。

您可以通过调用DatagramChannel.isConnected()方法来检查连接状态。将返回一个相应的boolean值(如果该通道的套接字打开并连接,则返回true):

if (datagramChannel.isConnected()) {
    ...
}

以下应用是我们的 UDP echo 服务器的 UDP 连接客户端。它连接到远程地址,并使用read() / write()方法传输数据:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardProtocolFamily;
import java.nio.channels.DatagramChannel;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

public class Main {

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

  final int REMOTE_PORT = 5555;
  final String REMOTE_IP = "127.0.0.1"; //modify this accordingly if you want to test remote
  final int MAX_PACKET_SIZE = 65507;

  CharBuffer charBuffer = null;
  Charset charset = Charset.defaultCharset();

  CharsetDecoder decoder = charset.newDecoder();
  ByteBuffer textToEcho = ByteBuffer.wrap("Echo this: I'm a big and ugly server!".getBytes());
  ByteBuffer echoedText = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);

  //create a new datagram channel
  try (DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET)) {

       //set some options
       datagramChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
       datagramChannel.setOption(StandardSocketOptions.SO_SNDBUF, 4 * 1024);

       //check if the channel was successfully opened
       if (datagramChannel.isOpen()) {

           //connect to remote address
           datagramChannel.connect(new InetSocketAddress(REMOTE_IP, REMOTE_PORT));

           //check if the channel was successfully connected
           if (datagramChannel.isConnected()) {

               //transmitting data packets
               int sent = datagramChannel.write(textToEcho);
               System.out.println("I have successfully sent "+sent
                                                             +" bytes to the Echo Server!");

               datagramChannel.read(echoedText);

               echoedText.flip();
               charBuffer = decoder.decode(echoedText);
               System.out.println(charBuffer.toString());
               echoedText.clear();

           } else {
             System.out.println("The channel cannot be connected!");
           }
       } else {
         System.out.println("The channel cannot be opened!");
       }
  } catch (Exception ex) {
    if (ex instanceof ClosedChannelException) {
        System.err.println("The channel was unexpected closed ...");
    }
    if (ex instanceof SecurityException) {
        System.err.println("A security exception occured ...");
    }
    if (ex instanceof IOException) {
        System.err.println("An I/O error occured ...");
    }

    System.err.println("\n" + ex);
  }
 }
}

The well-known read()/write() methods are available in DatagramChannel:
  • 将字节序列从该通道读入给定的缓冲区。这些方法返回读取的字节数(可以是零),如果通道已经到达流的末尾,则返回–1:public abstract int read(ByteBuffer dst) throws IOException public final long read(ByteBuffer[] dsts) throws IOException public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException
  • 从给定的缓冲区向该通道写入一个字节序列。这些方法返回写入的字节数;可以为零:public abstract int write(ByteBuffer src) throws IOException public final long write(ByteBuffer[] srcs) throws IOException public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException

测试 UDP 连接回显应用

测试应用是一项简单的任务。首先,启动服务器,等待看到以下消息:


Echo server was successfully opened!

Echo server was binded on: /127.0.0.1:5555

Echo server is ready to echo ...

然后启动客户机并检查输出。UDP 服务器输出如下所示:


Echo server was successfully opened!

Echo server was binded on: /127.0.0.1:5555

Echo server is ready to echo ...

I have received 37 bytes from /127.0.0.1:57374! Sending them back ...

下面是 UDP 客户端的输出:


I have successfully sent 37 bytes to the Echo Server!

Echo this: I'm a big and ugly server!

组播

你可能已经熟悉了术语多播。但是,如果你不是,让我们对这个概念有一个简短的概述。没有学术上的描述和定义,可以把多播看作是广播的互联网版本。例如,电视台从一个信号源广播信号,但信号可以到达信号区域内的每个人,只有那些没有合适设备或拒绝接收信号的人才能接收不到信号。

在计算机世界中,电视台可以被翻译成一个主节点或机器,它将数据报传播到一组目的主机。这要归功于多播传输服务,它在一次呼叫中将数据报从一个源发送到多个接收者——这与单播传输服务相反,后者专用于基于点对点连接的高级网络协议,需要复制单播来将相同的数据发送到多个点(实际上,它将数据的副本发送到每个点)。

多播引入了代表数据报接收者的组的概念。组由 D 类 IP 地址标识(多播组 IPv4 地址在 224.0.0.1 和 239.255.255.255 之间)。当一个新的接收者(客户端)想要加入一个多播组时,它需要通过相应的 IP 地址连接到该组,并监听传入的数据报。

许多现实生活中的案例可以基于多播进行编程,例如在线会议、新闻发布、广告、电子邮件组和数据共享管理。

接下来,我们将讨论 NIO.2 对多播的贡献。

组播频道概述

NIO.2 附带了一个新的接口,用于映射支持 IP 多播的网络通道。这是java.nio.channels.MulticastChannel界面。在 API 层,这是本章前面介绍的NetworkChannel接口的子接口,它由一个类实现:?? 类。

基本上,它定义了两个join()方法和一个close()方法。聚焦于join()方法,这里有一个简短的概述:

  • 第一个join()方法由想要加入多播组以接收传入数据报的客户端调用。我们需要传递该组的 IP 地址和加入该组的网络接口(您将很快看到如何检查您的机器是否有支持多播的网络接口)。如果指示的组被成功加入,该方法返回一个MembershipKey实例。这是 NIO.2 中的新特性,它是一个代表 ip 多播组成员资格的令牌(见下一节)。MembershipKey join(InetAddress g, NetworkInterface i) throws IOException
  • 第二种join()方法也用于加入多播组。然而,在这种情况下,我们指出了一个源地址,组成员可以从该源地址开始接收数据报。成员资格是累积的,这意味着该方法可以用同一个组和接口再次调用,以接收由其他源地址发送到该组的数据报。MembershipKey join(InetAddress g, NetworkInterface i, InetAddress s) throws IOException

Image 注意一个组播通道可以加入几个组播组,包括多个接口上的同一个组。

close()方法用于删除成员资格(如果加入了任何组)并关闭通道。

会员密钥概述

当您加入一个多播组时,您会得到一个成员密钥,该密钥可用于在该组内执行不同种类的操作。最常见的如下所示:

  • 阻塞/解除阻塞:您可以通过调用block()方法并传递源地址来阻塞从特定来源发送的数据报。而且,您可以通过使用相同的地址调用unblock()方法来解锁被阻塞的源。
  • Get group :通过调用无参数的group()方法,可以获得为其创建成员密钥的组播组的源地址。这个方法返回一个InetAddress对象。
  • 获取通道:您可以通过调用无参数方法channel()来获取为其创建这个成员键的通道。这个方法返回一个MulticastChannel对象。
  • 获取源地址:如果成员键是特定于源的(只接收来自特定源地址的数据报),您可以通过调用无参数sourceAddress()方法来获取源地址。这个方法返回一个InetAddress对象。
  • 获取网络接口:您可以通过调用无参数networkInterface()方法来获取为其创建该成员密钥的网络接口。这个方法返回一个NetworkInterface对象。
  • 检查有效性:您可以通过调用isValid()方法来检查成员资格是否有效。这个方法返回一个boolean值。
  • Drop :您可以通过调用无参数drop()方法来删除成员资格(通道将不再接收发送到该组的任何数据报)。

成员资格密钥在创建时有效,并且在使用drop()方法删除成员资格或关闭通道之前一直有效。

网络接口概述

NetworkInterface类代表一个网络接口,它由一个名称和分配给该接口的 IP 地址列表组成。它用于标识多播组加入的本地接口。例如,以下代码将返回在您的计算机上找到的所有网络接口的信息:

import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;

public class Main {

public static void main(String argv[]) throws Exception {

  Enumeration enumInterfaces = NetworkInterface.getNetworkInterfaces();
  while (enumInterfaces.hasMoreElements()) {
     NetworkInterface net = (NetworkInterface) enumInterfaces.nextElement();
     System.out.println("Network Interface Display Name: " + net.getDisplayName());
     System.out.println(net.getDisplayName() + " is up and running ?" + net.isUp());
     System.out.println(net.getDisplayName()+" Supports Multicast: "+net.supportsMulticast());
     System.out.println(net.getDisplayName() + " Name: " + net.getName());
     System.out.println(net.getDisplayName() + " Is Virtual:  " + net.isVirtual());
     System.out.println("IP addresses:");
     Enumeration enumIP = net.getInetAddresses();
     while (enumIP.hasMoreElements()) {
        InetAddress ip = (InetAddress) enumIP.nextElement();
        System.out.println("IP address:" + ip);
     }
  }
 }
}

该应用将返回在您的机器上找到的所有网络接口,并为每个接口呈现其显示名称(描述网络设备的人类可读的String)和名称(用于标识网络接口的真实名称)。此外,还会检查每个网络接口,看它是否支持多播,是否是虚拟的(子接口),以及是否已启动并正在运行。

图 8-4 显示了我的机器上的输出片段。帧接口是用于测试多播应用的接口,其名称为 eth3,稍后将在客户端/服务器多播应用中用于指示该接口。

Image

图 8-4。找出本地接口。

编写 UDP 组播服务器

在本节中,我们将编写一个 UDP 多播服务器,它向组发送包含服务器上当前日期和时间的数据报。这将每 10 秒钟重复一次。既然我们已经有了一些编写 UDP 客户端/服务器应用的经验,就没有必要一步一步地重复整个过程。我们将只指出将普通的 UDP 客户机/服务器应用转换成 UDP 多播客户机/服务器应用的主要区别。

我们通过调用open()方法创建一个新的DatagramChannel对象来开始开发过程。接下来,我们设置两个重要的选项,IP_MULTICAST_IFSO_REUSEADDR。第一个将指示本例中使用的 IP 多播数据报的网络接口,第二个应在绑定套接字之前启用,这是允许组的多个成员绑定到同一地址所必需的:

NetworkInterface networkInterface = NetworkInterface.getByName("eth3");
...
datagramChannel.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface);
datagramChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);

接下来,我们通过调用bind()方法将通道的套接字绑定到本地地址:

final int DEFAULT_PORT = 5555;
datagramChannel.bind(new InetSocketAddress(DEFAULT_PORT));

最后,我们准备数据报传输代码。因为我们每 10 秒钟向组发送一次服务器日期和时间,所以我们需要一个无限循环,其中包含 10 秒钟的睡眠时间和对send()方法的调用。多播组 IP 地址被任意选择为 225.4.5.6,它由一个InetAddress对象映射:

final int DEFAULT_PORT = 5555;
final String GROUP = "225.4.5.6";
ByteBuffer datetime;
...
while (true) {

       //sleep for 10 seconds
       try {
           Thread.sleep(10000);
       } catch (InterruptedException ex) {}

       System.out.println("Sending data ...");
       datetime = ByteBuffer.wrap(new Date().toString().getBytes());
       datagramChannel.send(datetime, new  
                            InetSocketAddress(InetAddress.getByName(GROUP), DEFAULT_PORT));
       datetime.flip();
}

将所有内容放在一起将产生以下应用:

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.StandardProtocolFamily;
import java.nio.channels.DatagramChannel;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.util.Date;

public class Main {

 public static void main(String[] args) {

  final int DEFAULT_PORT = 5555;
  final String GROUP = "225.4.5.6";
  ByteBuffer datetime;

  //create a new channel
  try (DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET)) {

       //check if the channel was successfully created
       if (datagramChannel.isOpen()) {

           //get the network interface used for multicast
           NetworkInterface networkInterface = NetworkInterface.getByName("eth3");

           //set some options

           datagramChannel.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface);        
           datagramChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);                

           //bind the channel to the local address
           datagramChannel.bind(new InetSocketAddress(DEFAULT_PORT));
           System.out.println("Date-time server is ready ... shortly I'll start sending ...");

           //transmitting datagrams
           while (true) {

                  //sleep for 10 seconds
                  try {
                      Thread.sleep(10000);
                  } catch (InterruptedException ex) {}

                  System.out.println("Sending data ...");
                  datetime = ByteBuffer.wrap(new Date().toString().getBytes());
                  datagramChannel.send(datetime, new
                               InetSocketAddress(InetAddress.getByName(GROUP), DEFAULT_PORT));
                  datetime.flip();
           }
       } else {
         System.out.println("The channel cannot be opened!");
       }
  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}
编写 UDP 组播客户端

UDP 组播客户端的代码与服务器几乎相同,只是有一些不同。首先,您可能想要检查远程地址是否实际上是一个多播地址——这可以通过调用返回一个booleanInetAddress.isMulticastAddress()方法来实现。其次,因为这是一个客户端,它必须通过调用两个join()方法之一来加入这个组。数据报传输代码仅适用于从 UDP 多播服务器接收数据报。以下应用是一种可能的客户端实现:

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.StandardProtocolFamily;
import java.nio.channels.DatagramChannel;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.MembershipKey;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

public class Main {

 public static void main(String[] args) {

  final int DEFAULT_PORT = 5555;
  final int MAX_PACKET_SIZE = 65507;
  final String GROUP = "225.4.5.6";

  CharBuffer charBuffer = null;
  Charset charset = Charset.defaultCharset();
  CharsetDecoder decoder = charset.newDecoder();
  ByteBuffer datetime = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);

  //create a new channel
  try (DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET)) {

       InetAddress group = InetAddress.getByName(GROUP);
       //check if the group address is multicast
       if (group.isMulticastAddress()) {
           //check if the channel was successfully created
           if (datagramChannel.isOpen()) {
               //get the network interface used for multicast
               NetworkInterface networkInterface = NetworkInterface.getByName("eth3");

               //set some options
               datagramChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
               //bind the channel to the local address
               datagramChannel.bind(new InetSocketAddress(DEFAULT_PORT));
               //join the multicast group and get ready to receive datagrams
               MembershipKey key = datagramChannel.join(group, networkInterface);

               //wait for datagrams
               while (true) {

                      if (key.isValid()) {

                          datagramChannel.receive(datetime);
                          datetime.flip();
                          charBuffer = decoder.decode(datetime);
                          System.out.println(charBuffer.toString());
                          datetime.clear();
                      } else {
                        break;
                      }
               }

           } else {
             System.out.println("The channel cannot be opened!");
           }
       } else {
         System.out.println("This is not  multicast address!");

       }

  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}
阻塞和解除阻塞数据报

有时加入多播组会给你带来不想要的数据报(原因与此无关)。您可以通过调用MembershipKey.block()方法并向其传递发送方的InetAddress来阻止从发送方接收数据报。此外,通过调用MembershipKey.unblock()方法并向其传递同一个InetAddress,您可以解除对同一个发送方的阻止,并再次开始从其接收数据报。通常,您会处于以下两种情况之一:

  • 您有一个想要加入的发件人地址列表。假设地址存储在一个List中,你可以循环它并分别连接每个地址,如下所示:`List like = ...;
    DatagramChannel datagramChannel =...;

    if(!like.isEmpty()){
        for(InetAddress source: like){
            datagramChannel.join(group, network_interface, source);
        }
    }`

  • 您有一个不想加入的发件人地址列表。假设地址存储在一个List中,那么你可以循环它,分别阻塞每个地址,如下所示:`List dislike = ...;
    DatagramChannel datagramChannel =...;

    MembershipKey key = datagramChannel.join(group, network_interface);

    if(!dislike.isEmpty()){
       for(InetAddress source: dislike){
           key.block(source);
       }
    }`

测试 UDP 组播应用

测试应用是一项简单的任务。首先,启动多播服务器,等待看到以下消息:


Date-time server is ready ... shortly I'll start sending ..

然后启动客户机并检查输出。以下是 UDP 多播服务器的一些输出示例:


Date-time server is ready ... shortly I'll start sending ...

Sending data ...

Sending data ...

Sending data ...

Sending data ...

Sending data ...

以下是 UDP 客户端输出(客户端在几分钟后启动):


Sat Oct 08 09:40:09 GMT+02:00 2011

Sat Oct 08 09:40:19 GMT+02:00 2011

对这个例子进行一些测试将会揭示一些问题。当服务器启动时,它发送数据报,而不知道是否有任何客户端正在侦听这些数据报。此外,它不知道客户端何时加入或离开组。另一方面,客户端在加入组时开始接收数据报,但不知道服务器是否因为任何原因而停止发送。如果服务器脱机,客户端仍在等待,当服务器再次联机并开始发送时,它将再次接收。如果您的情况需要更多的控制,尝试解决这些问题可能是一个有趣的练习。此外,您可能希望试验线程、阻塞/非阻塞模式和无连接/连接特性,以便为您的多播应用增加更多的灵活性和性能。

总结

本章讲述了用于创建 TCP/UDP 客户端/服务器应用的 NIO.2 特性。如前所述,NIO.2 通过用新方法更新现有的类,并为编写这样的应用添加新的接口/类,改进了这种支持。

这一章从NetworkChannel接口开始,它为所有网络通道类提供了公共方法。它还涵盖了专用于同步套接字通道的主要类:ServerSocketChannelSocketChannelDatagramChannel。它还讨论了MulticastChannel接口——映射支持 IP 多播的网络通道的NetworkChannel子接口。最后,您看到了如何编写单线程阻塞/非阻塞 TCP 客户机/服务器应用、单线程阻塞 UDP 客户机/服务器应用和单线程多播 UDP 客户机/服务器应用。*

九、异步通道 API

我们终于实现了 NIO.2 中引入的最强大的特性,异步通道 API。正如您将在本章中看到的,异步 I/O (AIO) Java 7 之旅始于java.nio.channels.AsynchronousChannel接口,它扩展了一个支持异步 I/O 操作的通道。该接口由三个类实现:AsynchronousFileChannelAsynchronousSocketChannelAsynchronousServerSocketChannel。还有第四个类,AsynchronousDatagramChannel,在 Java 7 beta 版中加入,后在 Java 7 最终版中移除;在撰写本文时,这个类还不可用,但它可能会出现在未来的 Java 7 版本中,所以本章将对它进行足够深入的介绍,让您了解它的用途。这些类在风格上类似于 NIO.2 通道 API。此外,还有一个名为AsynchronousByteChannel的异步通道,可以读写字节,并作为AsynchronousChannel的子接口站立起来(该子接口由AsynchronousSocketChannel类实现)。此外,新的 API 引入了一个名为AsynchronousChannelGroup的类,它提出了一个异步通道组的概念,其中每个异步通道都属于一个通道组(默认的或指定的),该通道组共享一个 Java 线程池。这些线程接收执行 I/O 事件的指令,并将结果发送给完成处理程序。所有的努力都是为了处理已启动的异步 I/O 操作的完成。

在这一章中,你将从 Java 的角度看到异步机制。您将看到 Java 如何实现异步 I/O 的大画面,之后您将开发文件和套接字的相关应用。我们将通过探索AsynchronousFileChannel类从文件的异步 I/O 开始,然后继续 TCP 套接字和 UDP 套接字的异步 I/O。

但是,在我们开始研究 API 的特性之前,应该先简要概述一下同步 I/O 和异步 I/O 之间的区别。

同步输入/输出与异步输入/输出

同步执行和异步执行之间的区别一开始可能看起来有点混乱,所以让我们来澄清一下。基本上,输入/输出(I/O)同步有两种类型:同步 I/O异步 I/O (也称为重叠 I/O )。在同步 I/O 操作中,一个线程开始行动,并等待直到 I/O 请求完成(程序被“卡住”等待进程结束,没有出路)。当相同的动作发生在异步环境中时,线程在更多的内核帮助下执行 I/O 操作。实际上,它会立即将请求传递给内核,并继续处理另一个作业。当操作完成时,内核向线程发送信号,线程通过中断其当前作业并在必要时处理来自 I/O 操作的数据来“尊重”该信号。在 Java 的平台独立性精神中,异步 I/O 可以绑定到多个线程上——基本上,允许在一个单独的线程上处理一些事情。

异步 I/O 和同步 I/O 服务于不同的目的。如果您只想发出请求并接收响应,您可以使用同步 I/O。同步 I/O 限制了性能和可伸缩性,因为它是每个 I/O 连接一个线程,运行数千个线程会显著增加操作系统的开销。异步 I/O 是一种不同的编程模型,因为您不必等待响应,而是提交您的工作以供执行,然后几乎立即或稍后回来等待响应。因此,异步 I/O 似乎比同步 I/O 更好,因为性能和可伸缩性是 I/O 系统的关键词。各种重要的操作系统,如 Windows 和 Linux,基于对发生在 OS 层的 I/O 操作的异步通知的使用,支持快速、可伸缩的 I/O。

总之,预计会花费大量时间的 I/O 处理可以通过使用异步 I/O 来优化。对于相对较快的 I/O 操作,同步 I/O 会更好,因为处理内核 I/O 请求和内核信号的开销可能会使异步 I/O 的好处减少。

异步 I/O 大图

当谈到 Java 中的异步 I/O 时,我们谈论的是异步通道。异步通道是一种连接,它通过单独的线程并行支持多个 I/O 操作(例如,连接、读取和写入),并在操作启动后提供控制操作的机制。

本节讨论所有异步通道共有的几个重要方面。首先,请注意,所有异步通道都会启动 I/O 操作(不会阻止应用执行其他任务),并在 I/O 完成时提供通知。这条规则是异步通道的基础,并由此派生出整个异步通道 API。

为了开始我们对异步 I/O 的讨论,我们先来看看表单。所有异步 I/O 操作都有两种形式中的一种:

  • 待定结果
  • 完整结果

待定结果和未来类别

第一种形式返回一个java.util.concurrent.Future<V>对象,代表异步 I/O 操作的未决结果。通过Future的方法,我们可以检查操作是否完成,等待它的完成(如果它还没有完成),并检索操作的结果。

例如,您可以通过Future.isXXX()方法执行布尔检查:您可以通过调用Future.isDone()方法来确定操作是否完成,或者您可以通过调用Future.isCancelled()方法来检查操作是否被取消。您可以通过调用 Future .cancel()方法显式取消一个操作,该方法将返回一个表示取消成功的布尔值—如果执行该任务的线程应该被中断,则将true传递给该方法;否则,允许正在进行的任务完成。如果任务已经完成、已经取消或者由于其他原因无法取消,则此尝试将会失败。如果成功,并且在调用cancel()时该任务还没有开始,则该任务应该永远不会运行。

Image 注意取消异步 I/O 操作时,所有等待结果的线程都会抛出CancellationException。不能保证底层 I/O 操作会被立即取消,但可以保证不允许进一步尝试启动与被取消的操作“相同”的 I/O 操作(即,通道被置于特定于实现的错误状态)。此外,请记住,如果将cancel()方法参数设置为true,那么 I/O 操作可能会因关闭通道而中断——所有等待 I/O 操作结果的线程都将抛出CancellationException,通道上任何其他未完成的 I/O 操作都将完成,只有AsynchronousCloseException例外。

Image 提示确保当通道保持打开时,取消的读/写操作所涉及的 I/O 缓冲区不会被进一步访问。

操作完成后,只能使用方法Future.get()Future.get(long timeout, TimeUnit unit)检索操作的结果,必要时等待,直到操作就绪或指定的超时过期。在这种情况下,一个TimeoutException就会被抛出。V表示这个Futureget()方法返回的结果类型,也就是说这是操作的结果类型。

完成结果和 CompletionHandler 接口

第二种形式,complete result,让人想起众所周知的回调机制(比如 AJAX 回调)。这是一种替代Future表单的机制。我们向异步 I/O 操作(例如读或写)注册一个回调,当操作完成或失败时,调用一个处理程序(CompletionHandler)来使用操作的结果。

完成处理程序的形式是CompletionHandler<V,A>,其中V是结果值的类型,A是附加到 I/O 操作的对象的类型。处理程序应该覆盖两个方法:当 I/O 操作成功完成时调用的completed()方法,以及当 I/O 操作失败时调用的failed()方法。如果操作成功完成,则将结果作为参数传递给completed()方法,如果操作失败,则将Throwable传递给failed()方法。忽略操作状态,两种方法都接收表示传递给异步操作的对象的附件参数。如果同一个CompletionHandler对象用于多个操作,它可以用来跟踪哪个操作首先完成,但是,当然,您可能会发现它在其他情况下也很有用。这些方法的语法如下所示:

void completed(V result, A attachment)
void failed(Throwable exc, A attachment)

Image 提示根据CompletionHandler的官方 Java 平台 SE 7 文档,“这些方法的实现应该及时完成,以避免阻止调用线程分派给其他完成处理程序。”以下部分将解释原因。

异步信道的类型

在撰写本文时,Java 7 附带了以下三种类型的异步通道。下面的小节依次简要描述了每一个。

  • AsynchronousFileChannel
  • AsynchronousServerSocketChannel
  • AsynchronousSocketChannel
异步文件通道

顾名思义,AsynchronousFileChannel类代表了一个用于读取、写入和操作文件的异步通道。该类提供了基于ByteBuffer s 读写文件的方法。此外,它还提供了锁定文件、截断文件和获取文件大小的方法,但是请记住,与同步FileChannel通道不同,这种类型的通道不维护全局文件位置(当前位置)或偏移量。即使没有全局位置或偏移量可用,每个读取或写入操作也应该指定文件中的读取或写入位置。这允许同时访问文件的不同部分。

当您使用AsynchronousFileChannel通道时,您必须小心考虑以下几个方面:

  • 通过显式调用继承的close()方法(从AsynchronousChannel接口)来关闭异步文件通道会导致通道上所有未完成的异步操作以一个AsynchronousCloseException异常完成。通道关闭后,进一步尝试启动异步 I/O 操作会立即完成,原因为ClosedChannelException
  • 如果通道未打开读取,读取尝试可能会导致NonReadableChannelException异常。如果该通道未打开写入,写入尝试可能会导致NonWritableChannelException异常。
  • 当一个锁已经被这个 Java 虚拟机持有时,或者已经有一个挂起的锁定区域的尝试时,一个锁定尝试将导致一个OverlappingFileLockException异常。
AsynchronousServerSocketChannel

AsynchronousServerSocketChannel类表示面向流的监听套接字的异步通道。打开这样的通道类型允许我们将它绑定到一个具有相关线程池的组,任务被提交到该线程池来处理 I/O 操作(当没有指定时,还有一个默认组)。打开后,通道能够以异步方式接受传入的连接,这意味着我们可以在FutureCompletionHandler之间进行选择来跟踪连接状态。绑定和设置通道选项等重要任务通过实现的NetworkChannel接口提供。

当您使用AsynchronousServerSocketChannel通道时,请注意考虑以下几点:

  • 通过显式调用继承的close()方法(从AsynchronousChannel接口)来关闭异步服务器套接字通道会导致通道上所有未完成的异步操作以一个AsynchronousCloseException异常完成。通道关闭后,进一步尝试启动异步 I/O 操作会立即完成,原因为ClosedChannelException
  • 如果通道组关闭,打开尝试将导致ShutdownChannelGroupException异常。
  • 试图在未绑定的通道上调用accept()方法将导致抛出NotYetBoundException异常。
  • 如果一个线程在前一个接受操作完成之前启动了一个接受操作,那么将抛出一个AcceptPendingException异常。
异步套接字通道

AsynchronousSocketChannel类表示面向流的连接套接字的异步通道。打开这样的通道类型允许我们将它绑定到一个具有相关线程池的组,任务被提交到该线程池以处理 I/O 操作(当没有指定时,还有一个默认组)。打开后,通道能够以异步方式连接到远程地址,这意味着我们可以在FutureCompletionHandler之间进行选择,以跟踪连接状态。为了成功连接,该通道可以通过一组read()write()异步方法读取和写入字节缓冲区(字节序列,ByteBuffer)——同样,我们可以在FutureCompletionHandler之间进行选择,以跟踪读取或写入状态。通过实现的NetworkChannel接口提供了绑定和设置通道选项等重要任务。

当您使用AsynchronousSocketChannel通道时,请注意考虑以下几点:

  • 通过显式调用继承的close()方法(从AsynchronousChannel接口)关闭异步套接字通道会导致通道上所有未完成的异步操作以AsynchronousCloseException异常结束。通过关闭的通道启动异步 I/O 操作的进一步尝试将立即结束,并出现ClosedChannelException异常。
  • 试图在未连接的通道上调用 I/O 操作将导致抛出NotYetConnectedException异常。
  • 如果一个线程在前一个读操作完成之前启动了一个读操作,那么将抛出一个ReadPendingException异常。如果一个线程在前一个写操作完成之前启动一个写操作,那么将抛出一个WritePendingException异常。
  • 如果该通道已经连接,尝试连接到该通道可能会导致AlreadyConnectedException异常。
  • 如果一个连接操作已经在该通道上进行,尝试连接到该通道可能会导致ConnectionPendingException异常。
  • AsynchronousSocketChannel类定义的read()write()方法分别允许在启动读或写操作时指定超时。如果操作完成前超时,那么InterruptedByTimeoutException异常将完成操作。超时可能会使通道或底层连接处于不一致的状态。如果实现不能保证字节没有被从通道读取或写入通道,那么它将通道置于特定于实现的错误状态。随后尝试启动读取或写入操作会导致引发未指定的运行时异常。

团体

正如本章介绍中提到的,异步 API 引入了一个名为AsynchronousChannelGroup的类,它提出了一个异步通道组的概念,其中每个异步通道都属于一个通道组(默认的或指定的),该通道组共享一个 Java 线程池。这些线程接收执行 I/O 事件的指令,并将结果发送给完成处理程序。异步通道组封装了线程池和为通道工作的所有线程共享的资源。此外,频道实际上归该组所有,因此如果该组关闭,频道也将关闭。

异步通道对于多个并发线程来说是安全的。一些通道实现可能支持并发读取和写入,但在任何给定时间可能不允许一个以上的读取和写入操作未完成。

默认组

除了开发人员创建的组之外,JVM 还维护一个系统范围的默认组,它是自动构建的,对于简单的应用非常有用。当没有指定组,或者相反传递了一个null时,异步通道在构造时被绑定到默认组。默认组可以通过两个系统属性进行配置,第一个属性如下:

java.nio.channels.DefaultThreadPool.threadFactory

下面是官方 Java 平台 SE 7 文档中对AsynchronousChannelGroup类的这个属性的描述:

这个属性的值被认为是一个具体的ThreadFactory类的全限定名。该类是使用系统类加载器加载并实例化的。调用工厂的newThread方法为默认组的线程池创建每个线程。如果加载和实例化属性值的过程失败,则会在构造默认组的过程中引发未指定的错误。

换句话说,这个系统属性定义了一个java.util.concurrent.ThreadFactory来代替默认的。

第二个系统属性是

java.nio.channels.DefaultThreadPool.initialSize

官方 Java 平台 SE 文档提供了这样的描述:

默认组的initialSize参数的值。该属性的值被认为是初始大小参数IntegerString表示。如果该值不能被解析为一个Integer,它将导致在构造默认组的过程中抛出一个未指明的错误。

简而言之,这个系统属性指定线程池的初始大小。

自定义群组

如果默认组不能满足您的需要,AsynchronousChannelGroup类提供了三种方法来创建您自己的通道组。对于AsynchronousServerSocketChannelAsynchronousSocketChannelAsynchronousDatagramChannel(在撰写本文时不可用),通道组是通过每个通道的open()方法创建的。AsynchronousFileChannel与其他通道的不同之处在于,为了使用自定义线程池,open()方法采用了ExecutorService而不是AsynchronousChannelGroup。现在,让我们看看每个支持的线程池的优缺点是什么;这些特征将帮助你决定哪一个适合你的情况。

固定线程池

您可以通过调用下面的AsynchronousChannelGroup方法来请求固定线程池:

public static AsynchronousChannelGroup withFixedThreadPool(int nThreads,
ThreadFactory threadFactory) throws IOException

此方法创建一个具有固定线程池的通道组。您必须指定创建新线程时要使用的工厂以及线程数量。

Image 注意固定线程池中的生命周期遵循一个简单的场景:一个线程等待一个 I/O 事件,完成该事件的 I/O,调用一个完成处理程序,然后返回等待更多的 I/O 事件(内核直接将事件分派给这些线程)。当完成处理程序正常终止时,线程返回线程池并等待下一个事件。但是如果完成处理程序没有及时完成,那么就有可能进入无限期阻塞。如果所有线程都在一个完成处理程序中“死锁”,那么应用将被阻塞,直到有一个线程可以再次执行,并且任何新事件都将排队,直到有一个线程可用。在最坏的情况下,没有线程可以获得自由,内核不再执行任何东西。如果在完成处理程序中不使用阻塞或长时间操作,这个问题是可以避免的。此外,您可以使用缓存线程池或超时来避免这个问题。

缓存线程池

您可以通过调用下面的AsynchronousChannelGroup方法来请求缓存线程池:

public static AsynchronousChannelGroup withCachedThreadPool(ExecutorService executor,
 int initialSize) throws IOException

此方法使用给定的线程池创建一个异步通道组,该线程池根据需要创建新线程。您只需要指定线程的初始数量和一个根据需要创建新线程的ExecutorService。当先前构造的线程可用时,它可以重用这些线程。

在这种情况下,异步通道组将向线程池提交事件,线程池只是调用完成处理程序。但是,如果线程池只是调用完成处理程序,那么谁来做艰苦的工作和执行 I/O 操作呢?答案是隐藏线程池。这是一组等待输入 I/O 事件的独立线程。更准确地说,内核 I/O 操作由一个或多个不可见的内部线程处理,这些线程将事件分派到缓存池,缓存池又调用完成处理程序。

隐藏线程池非常重要,因为它大大降低了应用被阻塞的可能性(它解决了固定线程池问题),并保证内核能够完成其 I/O 操作。但我们仍然有一个问题,因为缓存的线程池需要无界排队,这可能会使队列无限增长并导致OutOfMemoryError—所以要监控队列(避免锁定所有线程,避免永远喂队列)。避免在完成处理程序中使用阻塞或长时间操作仍然是一个好主意。

指定线程池

您还可以通过调用下面的AsynchronousChannelGroup方法来请求线程池:

public static AsynchronousChannelGroup withThreadPool(ExecutorService executor)
throws IOException

此方法使用指定的线程池创建异步通道组。线程池是通过一个ExecutorService对象提供的。

ExecutorService执行提交的任务,为组内异步通道发起的操作调度完成结果。使用这种方法需要在配置ExecutorService时格外小心——这里至少要做两件事:为提交任务的直接移交无限队列提供支持,并且绝不允许调用execute()方法的线程直接调用任务。

关闭群组

关闭一个组可以通过调用shutdown()方法或shutdownNow()方法来完成。调用shutdown()方法,通过将组标记为关闭来启动关闭组的过程。进一步尝试构造绑定到组的通道将抛出ShutdownChannelGroupException。一旦它被标记为关闭,该组就开始终止过程,该过程包括等待所有绑定的异步通道被关闭(即,完成处理程序已经运行并且资源已经被释放)。

您可以通过调用带有指定超时的awaitTermination()方法进行阻塞,直到组终止,超时发生,或者当前线程中断,无论哪种情况先发生。您可以通过调用isTerminated()方法来检查一个组是否已经终止,也可以通过调用isShutdown()方法来检查它是否已经关闭。请记住,shutdown()方法不会强制停止或中断正在执行完成处理程序的线程。

此外,强制一个组关闭可以通过调用shutdownNow()方法来完成,这将关闭组中的所有通道,就像AsynchronousChannel.close()方法关闭它们一样。请记住,调用这个方法将会完成这个通道上任何未完成的异步操作,例外是AsynchronousCloseException。通道关闭后,进一步尝试启动异步 I/O 操作会立即完成,原因为ClosedChannelException

当指定了一个ServiceExecutor时,它旨在由产生的异步通道组专用。组的终止导致执行者服务的有序关闭;如果 executor 服务由于其他原因关闭,将会发生未指定的行为。

Image 注意在面向流的连接套接字的异步通道的情况下,也有可能通过调用shutdownInupt()方法(通过返回流结束指示符-1拒绝任何进一步的读取尝试)和通过调用shutdownOutput()方法(通过抛出ClosedChannelException异常)拒绝任何写入尝试)来关闭读取连接。这两种方法都不会关闭通道。

ByteBuffer 注意事项

众所周知,ByteBuffer不是线程安全的。因此,您必须确保不访问当前参与 I/O 操作的字节缓冲区。避免这个问题的一个好办法是使用一个ByteBuffer池。当一个 I/O 操作即将到来时,您从池中获得一个字节缓冲区,执行 I/O 操作,然后将字节缓冲区返回到池中。

修复此问题也会修复另一个关于内存不足的错误。缓冲区的内存需求取决于未完成的 I/O 操作的数量,但是使用池将有助于您重用一组缓冲区并避免内存不足的问题。

介绍 ExecutorService API

前面对组的讨论引用了ExecutorService API。如果您不熟悉这个 API,您应该参考官方文档,可以从

http://download.oracle.com/javase/7/docs/api/java/util/concurrent/ExecutorService.html

这个 API 是 Java 并发和多线程概念的一个重要组成部分,由于它是一个庞大而复杂的 API,所以在这里介绍它超出了我们的目的。我推荐你也在[www.vogella.de/articles/JavaConcurrency/article.html](http://www.vogella.de/articles/JavaConcurrency/article.html)(2011 年 5 月 17 日出版)查阅 Lars Vogel 的《Java 并发/多线程》教程。

简单介绍一下,Executor 框架提供了一种通过java.util.concurrent.Executors类创建定制线程池的便捷方式(该类包含多线程 API 中涉及的不同种类接口的工厂和实用方法,如java.util.concurrent.Executorjava.util.concurrent.ExecutorService)。这个类包含了newFixedThreadPool()newCachedThreadPool()newScheduledThreadPool()等方法。

这些方法中的每一个都创建了一定数量的工作线程线程(由开发人员指定或由默认实现推断)。ExecutorService接口为Executor增加了生命周期方法,可以关闭Executor ( shutdown()方法),等待终止(awaitTermination()方法)。在许多情况下,Executor 框架使用不返回结果的Runnable任务,但是当您希望您的线程返回一个计算结果时,您可以使用java.util.concurrent.Callable接口,它利用泛型来定义返回的对象类型。结果是在Callable.call()方法中计算的,该方法应该相应地被覆盖——如果结果不能被计算,这将抛出一个Exception。每个Callable任务都被提交给Executor(submit()方法),它返回一个代表待定结果的Future;通过调用get()方法检查结果状态并检索结果。

开发异步应用

为了实现异步通道 API 的最佳可伸缩性,需要开发如此多的示例和执行如此多的测试,以至于需要一本专门的书来涵盖所有的细节。因为我们在一章中讨论了这个主题,所以我们将直接切入存根应用,它将为您提供开发其他应用的灵感来源。

我们从用于读取、写入和操作文件的异步文件通道开始这场开发狂欢。您将看到如何对基于FutureCompletionHander表单的文件执行这些 I/O 操作。然后,我们将继续讨论面向流的监听套接字的异步通道和面向流的连接套接字的异步通道。

异步文件通道示例

任何涉及异步文件通道的应用的第一步都是通过调用两个open()方法之一为文件创建一个新的AsynchronousFileChannel实例。最容易使用的将接收要打开或创建的文件的路径,以及一组指定如何打开文件的选项,如下所示。这个open()方法将把通道与一个依赖于系统的默认线程池相关联,该线程池可以与其他通道共享(默认组)。

public static AsynchronousFileChannel open(Path file, OpenOption… options)
throws IOException

Image 注意前面代码中调用的选项集是之前在第四章和第七章中描述的StandardOpenOption枚举常量,因此您应该已经熟悉了这些选项。

文件读取和未来

以下代码片段创建了一个新的异步文件通道,用于读取位于C:\rafaelnadal\grandslam\RolandGaross目录中的文件story.txt(该文件必须存在):

Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");
AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.
                                                  open(path, StandardOpenOption.READ) ;

文件已准备就绪,可以开始阅读了。这个任务由read()方法完成(有两个方法)。由于我们对使用Future模式感兴趣,我们将使用下面的read()方法:

public abstract Future<Integer> read(ByteBuffer dst, long position)

此方法从给定的文件位置开始,将一个字节序列从此通道读入给定的缓冲区,并返回一个表示挂起结果的对象。由于我们处于异步环境中,这个方法只是启动读取,并不阻塞应用。以下代码向您展示了如何使用它来读取前 100 个字节:

ByteBuffer buffer = ByteBuffer.allocate(100);
Future<Integer> result = asynchronousFileChannel.read(buffer, 0);

待定结果允许我们通过Future.isDone()方法跟踪读取过程状态,该方法将返回false直到读取操作完成。将这个调用放在一个循环中允许我们完成其他任务,直到读取完成:

while (!result.isDone()) {
   System.out.println("Do something else while reading …");
}

当读取操作完成时,应用流退出循环,并且可以通过调用get()方法来检索结果,如果需要,该方法会等待操作完成。结果是一个整数,表示读取的字节数,而字节在目标缓冲区中:

System.out.println("Read done: " + result.isDone());
System.out.println("Bytes read: " + result.get());

将所有内容粘合在一起会产生以下应用:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;

public class Main {

 public static void main(String[] args) {

  ByteBuffer buffer = ByteBuffer.allocate(100);
  String encoding = System.getProperty("file.encoding");

  Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");
  try (AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path,
       StandardOpenOption.READ)) {

       Future<Integer> result = asynchronousFileChannel.read(buffer, 0);

        while (!result.isDone()) {
               System.out.println("Do something else while reading …");
        }

        System.out.println("Read done: " + result.isDone());
        System.out.println("Bytes read: " + result.get());

   } catch (Exception ex) {
     System.err.println(ex);
   }

   buffer.flip();
   System.out.print(Charset.forName(encoding).decode(buffer));
   buffer.clear();
 }
}

以下是该应用的可能输出:


…

Do something else while reading …

Do something else while reading …

Do something else while reading …

Do something else while reading …

Read done: true

Bytes read: 100

Rafa Nadal produced another masterclass of clay-court tennis to win his fifth French Open
title …

文件的编写和未来

以下代码片段创建了一个新的异步文件通道,用于将更多字节写入位于C:\rafaelnadal\grandslam\RolandGaross中的文件story.txt(该文件必须存在):

Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");
AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.
                                                  open(path, StandardOpenOption.WRITE) ;

文件已准备好写入,所以我们可以开始写入。这个任务由write()方法完成(有两个方法)。由于我们对使用Future模式感兴趣,我们将使用下面的write()方法:

public abstract Future<Integer> write(ByteBuffer src, long position)

此方法从给定的文件位置开始,将一个字节序列从给定的缓冲区写入此通道,并返回一个表示挂起结果的对象。由于我们处于异步环境中,该方法只是启动写入,并不阻止应用。以下代码向您展示了如何使用它从位置 100 开始写入一些字节:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;

public class Main {

 public static void main(String[] args) {

  ByteBuffer buffer = ByteBuffer.wrap("The win keeps Nadal at the top of the heap in men's
tennis, at least for a few more weeks. The world No2, Novak Djokovic, dumped out here in the
semi-finals by a resurgent Federer, will come hard at them again at Wimbledon but there is
much to come from two rivals who, for seven years, have held all pretenders at
bay.".getBytes());

  Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");
  try (AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path,
       StandardOpenOption.WRITE)) {

       Future<Integer> result = asynchronousFileChannel.write(buffer, 100);

       while (!result.isDone()) {
              System.out.println("Do something else while writing …");
       }

       System.out.println("Written done: " + result.isDone());
       System.out.println("Bytes written: " + result.get());

  } catch (Exception ex) {
    System.err.println(ex);
  }
 }
}

这次,get()方法返回写入的字节数。字节从文件中的位置 100 开始写入。应用输出如下:


…

Do something else while writing …

Do something else while writing …

Do something else while writing …

Written done: true

Bytes written: 319

作为一个练习,尝试将两个应用合并成一个单独的应用来异步读写。

文件读取和未来超时

如前所述,get()方法在必要时会等待操作完成,然后检索结果。这个方法还有一个超时版本,在这个版本中,我们可以精确地指定我们可以等待多长时间。为此,我们向get()方法传递一个超时和单位时间。如果时间到了,这个方法抛出一个TimeoutException,我们可以通过调用带有true参数的cancel()方法来中断线程完成这个任务。下面的应用读取story.txt的内容,超时很短:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class Main {

 public static void main(String[] args) {

  ByteBuffer buffer = ByteBuffer.allocate(100);
  int bytesRead = 0;
  Future<Integer> result = null;

  Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");

  try (AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path,
       StandardOpenOption.READ)) {

       result = asynchronousFileChannel.read(buffer, 0);

       bytesRead = result.get(1, TimeUnit.NANOSECONDS);

       if (result.isDone()) {
           System.out.println("The result is available!");
           System.out.println("Read bytes: " + bytesRead);
       }

  } catch (Exception ex) {
    if (ex instanceof TimeoutException) {
        if (result != null) {
            result.cancel(true);
        }
         System.out.println("The result is not available!");
         System.out.println("The read task was cancelled ? " + result.isCancelled());
         System.out.println("Read bytes: " + bytesRead);
     } else {
        System.err.println(ex);
     }
  }
 }
}

这个应用有两个可能的输出。首先,如果时间到期,I/O 操作没有完成,输出将如下所示:


The result is not available!

The read task was cancelled ? true //(or, false)

Read bytes: 0

如果 I/O 操作在时间到期前完成,输出将如下所示:


The result is available!

Read bytes: 100

文件读取和完成处理程序

现在您已经看到了一些关于Future表单如何工作的例子,是时候看看如何编写一个CompletionHandler来读取story.txt的内容了。在创建了一个用于读取story.txt文件内容的异步文件通道后,我们调用AsynchronousFileChannnel类的第二个read()方法:

public abstract <A> void read(ByteBuffer dst, long position, A attachment,
CompletionHandler<Integer,? super A> handler)

这个方法从给定的文件位置开始,将一个字节序列从这个通道读入给定的缓冲区。除了目标缓冲区和文件位置之外,该方法还获得了附加到 I/O 操作的对象(可以是null)和用于消费结果的完成处理程序。由于我们处于异步环境中,这个方法只是启动读取,并不阻塞应用。下面的代码向您展示了如何使用它来读取前 100 个字节——您可以将CompletionHandler定位为一个匿名内部类:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class Main {

 static Thread current;

 public static void main(String[] args) {

  ByteBuffer buffer = ByteBuffer.allocate(100);
  Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");

  try (AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path,            
       StandardOpenOption.READ)) {

       current = Thread.currentThread();
       asynchronousFileChannel.read(buffer, 0, "Read operation status …", new
       CompletionHandler<Integer, Object>() {

       @Override
       public void completed(Integer result, Object attachment) {
        System.out.println(attachment);
        System.out.print("Read bytes: " + result);
        current.interrupt();
       }

       @Override
       public void failed(Throwable exc, Object attachment) {
        System.out.println(attachment);
        System.out.println("Error:" + exc);
        current.interrupt();
       }
     });

     System.out.println("\nWaiting for reading operation to end …\n");
     try {
         current.join();
     } catch (InterruptedException e) {
     }

     //now the buffer contains the read bytes

     System.out.println("\n\nClose everything and leave! Bye, bye …");

  } catch (Exception ex) {
    System.err.println(ex);
  }
 }
}

使用current线程只是为了发现何时应该停止应用;在某些情况下,流程可能会在完成处理程序使用结果之前结束应用。你可以选择使用Thread.sleep()方法、System.in.read()方法或者任何其他方便的方法。

可能的输出如下:


Waiting for reading operation to end …

Read operation status …

Read bytes: 100

Closing everything and leave! Bye, bye …

在其他情况下,您可能会在CompletionHandler输出之后看到等待消息,这取决于它消耗 I/O 操作结果的速度。
目的地ByteBuffer可能作为 I/O 操作的附件对象“到达”到CompletionHandler(当你没有任何附件时,只需通过null)。以下应用将目的地ByteBuffer的内容解码并显示到CompletionHandlercompleted()方法中:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class Main {

 static Thread current;
 static final Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");

 public static void main(String[] args) {

  CompletionHandler<Integer, ByteBuffer> handler =
                             new CompletionHandler<Integer, ByteBuffer>() {

   String encoding = System.getProperty("file.encoding");

   @Override
   public void completed(Integer result, ByteBuffer attachment) {
    System.out.println("Read bytes: " + result);
    attachment.flip();
    System.out.print(Charset.forName(encoding).decode(attachment));
    attachment.clear();
    current.interrupt();
   }

   @Override
   public void failed(Throwable exc, ByteBuffer attachment) {
    System.out.println(attachment);
    System.out.println("Error:" + exc);
    current.interrupt();
   }
  };

  try (AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path,
       StandardOpenOption.READ)) {

       current = Thread.currentThread();
       ByteBuffer buffer = ByteBuffer.allocate(100);
       asynchronousFileChannel.read(buffer, 0, buffer, handler);

       System.out.println("Waiting for reading operation to end …\n");
       try {
           current.join();
       } catch (InterruptedException e) {
       }

       //the buffer was passed as attachment
       System.out.println("\n\nClosing everything and leave! Bye, bye …");

  } catch (Exception ex) {
    System.err.println(ex);
  }
 }
}

可能的输出如下:


Waiting for reading operation to end …

Read bytes: 100

Rafa Nadal produced another masterclass of clay-court tennis to win his fifth French Open
title …


Closing everything and leave! Bye, bye …

文件锁

有时,您需要在执行另一个 I/O 操作(如读取或写入)之前获取通道文件的排他锁。AsynchronousFileChannelFuture表单提供了一个lock()方法,为CompletionHandler提供了一个lock()方法(两者都有锁定文件区域的签名,更多细节可以在[download.oracle.com/javase/7/docs/api/](http://download.oracle.com/javase/7/docs/api/)的官方文档中找到):

public final Future<FileLock> lock()
public final <A> void lock(A attachment, CompletionHandler<FileLock,? super A> handler)

下面的应用使用带有Future表单的lock()方法来锁定文件。我们将等待通过调用Future.get()方法获得锁,然后,我们将一些字节写入我们的文件。我们再次调用get()方法,该方法将等待新字节被写入,并最终释放锁。使用的文件是CopaClaro.txt,位于C:\rafaelnadal\tournaments\2009(文件必须存在)。

public final Future<FileLock> lock()
public final <A> void lock(A attachment, CompletionHandler<FileLock,? super A> handler)

下面的应用使用带有Future表单的lock()方法来锁定文件。我们将等待通过调用Future.get()方法获得锁,然后,我们将一些字节写入我们的文件。我们再次调用get()方法,该方法将等待新字节被写入,并最终释放锁。使用的文件是CopaClaro.txt,位于C:\rafaelnadal\tournaments\2009(文件必须存在)。

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;

public class Main {

  public static void main(String[] args) {

   ByteBuffer buffer = ByteBuffer.wrap("Argentines At Home In Buenos Aires Cathedral\n The
Copa Claro is the third stop of the four-tournament Latin American swing, and is contested on
clay at the Buenos Aires Lawn Tennis Club, known as the Cathedral of Argentinean tennis. An
Argentine has reached the final in nine of the 11 editions of the ATP World Tour 250
tournament, with champions including Guillermo Coria, Gaston Gaudio, Juan Monaco and David
Nalbandian.".getBytes());

   Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "CopaClaro.txt");
   try (AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path,      
        StandardOpenOption.WRITE)) {

        Future<FileLock> featureLock = asynchronousFileChannel.lock();
        System.out.println("Waiting for the file to be locked …");
        FileLock lock = featureLock.get();
        //or, use shortcut
        //FileLock lock = asynchronousFileChannel.lock().get();

            
        if (lock.isValid()) {
            Future<Integer> featureWrite = asynchronousFileChannel.write(buffer, 0);
            System.out.println("Waiting for the bytes to be written …");
            int written = featureWrite.get();
            //or, use shortcut
            //int written = asynchronousFileChannel.write(buffer,0).get();

            System.out.println("I've written " + written + " bytes into " +
                                              path.getFileName() + " locked file!");

            lock.release();
        }

   } catch (Exception ex) {
     System.err.println(ex);
   }
 }
}

可能的输出如下:


Waiting for the file to be locked …

Waiting for the bytes to be written …

I've written 423 bytes into CopaClaro.txt locked file!

此外,用CompletionHandler实现lock()方法可能如下所示:

import java.io.IOException;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.channels.FileLock;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class Main {

 static Thread current;

  public static void main(String[] args) {

   Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "CopaClaro.txt");

   try (AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path,
        StandardOpenOption.READ, StandardOpenOption.WRITE)) {

        current = Thread.currentThread();

        asynchronousFileChannel.lock("Lock operation status:", new  
                                      CompletionHandler<FileLock, Object>() {

        @Override
        public void completed(FileLock result, Object attachment) {
         System.out.println(attachment + " " + result.isValid());

         if (result.isValid()) {
          //…  processing …            
          System.out.println("Processing the locked file …");
          //…
          try {
              result.release();
          } catch (IOException ex) {
            System.err.println(ex);
          }
         }
         current.interrupt();
        }

        @Override
        public void failed(Throwable exc, Object attachment) {
         System.out.println(attachment);
         System.out.println("Error:" + exc);
         current.interrupt();
        }
        });

        System.out.println("Waiting for file to be locked and process … \n");
        try {
            current.join();
        } catch (InterruptedException e) {
        }
        System.out.println("\n\nClosing everything and leave! Bye, bye …");

   } catch (Exception ex) {
     System.err.println(ex);
   }
 }
}

以下是可能的输出:


Waiting for file to be locked and process …

Lock operation status: true

Processing the locked file …


Closing everything and leave! Bye, bye …

Image AsynchronousFileChannel也提供了众所周知的tryLock()方法,但是它们与FutureCompletionHandler表单没有关联。

异步文件通道和执行服务

到目前为止,您只看到了第一个AsynchronousFileChannel.open()方法,它使用默认的池线程。是时候看看第二个open()方法的工作了,它允许我们通过一个ExecutorService对象来指定一个定制的线程池。此方法的语法如下:

public static AsynchronousFileChannel open(Path file, Set<? extends OpenOption> options,
ExecutorService executor, FileAttribute<?>… attrs) throws IOException

正如您所看到的,这个open()方法获得了要打开或创建的文件的路径、一组指定如何打开文件的选项(可选)、一个作为ExecutorService的线程池(或null)(参见上面的“ExecutorService API 简介”),以及一个在创建文件时自动设置的文件属性列表(可选)。

在我们的场景中,我们希望开发一个应用,用来自story.txt文件随机位置的字节异步填充 50 个ByteBufferByteBuffer s 的容量也会随机。此外,我们希望使用一个自定义组,该组具有五个线程的固定线程池。

我们从通过ExecutorService创建线程池开始:

final int THREADS = 5;
ExecutorService taskExecutor = Executors.newFixedThreadPool(THREADS);

我们继续将线程池传递给文件路径和选项旁边的open()方法:

private static Set withOptions() {
     final Set options = new TreeSet<>();
     options.add(StandardOpenOption.READ);
     return options;
}
AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open
                                                  (path, withOptions(), taskExecutor);

接下来,在一个循环中,我们创建 50 个Callable workers (返回值任务)并覆盖call()方法来创建随机容量的字节缓冲区,并用来自文件中随机位置的字节填充它们——这是我们的计算。我们将每个“工人”提交给执行者,并将它的Future存储到一个ArrayList中。稍后,我们将循环这个列表,并调用get()方法从每个字节缓冲区中检索结果。

List<Future<ByteBuffer>> list = new ArrayList<>();
…
for (int i = 0; i < 50; i++) {
 Callable<ByteBuffer> worker = new Callable<ByteBuffer>() {

  @Override
  public ByteBuffer call() throws Exception {

  ByteBuffer buffer=ByteBuffer.allocateDirect(ThreadLocalRandom.current().nextInt(100, 200));
  asynchronousFileChannel.read(buffer, ThreadLocalRandom.current().nextInt(0, 100));

  return buffer;
  }
 };

 Future<ByteBuffer> future = taskExecutor.submit(worker);

 list.add(future);
}

既然我们将所有必要的任务传递给了 executor,我们就可以关闭它,使它不接受新的任务。它完成队列中所有现有的线程并终止——与此同时,我们可以数一数绵羊:

…
taskExecutor.shutdown();

while (!taskExecutor.isTerminated()) {
  //do something else while the buffers are prepared
  System.out.println("Counting sheep while filling up some buffers!
                      So far I counted: " + (sheeps += 1));
} …

在数了一会儿绵羊之后,isTerminate()方法返回true,结果只是“出炉”迭代Future列表并调用get()方法来检索每个结果:

for (Future<ByteBuffer> future : list) {

 ByteBuffer buffer = future.get();
 …
}

搞定了。将所有内容粘合在一起并添加样板代码和导入会产生以下结果:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import java.util.concurrent.Future;
import java.util.concurrent.ThreadLocalRandom;

public class Main {

private static Set withOptions() {
        final Set options = new TreeSet<>();
        options.add(StandardOpenOption.READ);
        return options;
}

public static void main(String[] args) {

 final int THREADS = 5;
 ExecutorService taskExecutor = Executors.newFixedThreadPool(THREADS);

 String encoding = System.getProperty("file.encoding");
 List<Future<ByteBuffer>> list = new ArrayList<>();
 int sheeps = 0;

 Path path = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "story.txt");

  try (AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path,
       withOptions(), taskExecutor)) {

      for (int i = 0; i < 50; i++) {
           Callable<ByteBuffer> worker = new Callable<ByteBuffer>() {

            @Override
            public ByteBuffer call() throws Exception {
             ByteBuffer buffer = ByteBuffer.allocateDirect
                                (ThreadLocalRandom.current().nextInt(100, 200));
             asynchronousFileChannel.read(buffer, ThreadLocalRandom.current().nextInt(0,100));

             return buffer;
            }
           };

           Future<ByteBuffer> future = taskExecutor.submit(worker);
           list.add(future);
      }

      //this will make the executor accept no new threads
      // and finish all existing threads in the queue
      taskExecutor.shutdown();

      //wait until all threads are finished
      while (!taskExecutor.isTerminated()) {
             //do something else while the buffers are prepared
             System.out.println("Counting sheep while filling up some buffers!
                                 So far I counted: " + (sheeps += 1));
      }

      System.out.println("\nDone! Here are the buffers:\n");
      for (Future<ByteBuffer> future : list) {

           ByteBuffer buffer = future.get();

           System.out.println("\n\n"+ buffer);
           System.out.println("______________________________________________________");
           buffer.flip();
           System.out.print(Charset.forName(encoding).decode(buffer));
           buffer.clear();
      }

  } catch (Exception ex) {
    System.err.println(ex);
  }
 }
}

以下是可能的输出片段:


…

Counting sheep while filling up some buffers! So far I counted: 352

Counting sheep while filling up some buffers! So far I counted: 353

Counting sheep while filling up some buffers! So far I counted: 354

Done! Here are the buffers:

java.nio.HeapByteBuffer[pos=100 lim=100 cap=100]

______________________________________________________

d another masterclass of clay-court tennis to win his fifth French Open title …

java.nio.HeapByteBuffer[pos=189 lim=189 cap=189]

______________________________________________________

nother masterclass of clay-court tennis to win his fifth French Open title …

…

java.nio.HeapByteBuffer[pos=112 lim=112 cap=112]

______________________________________________________

y-court tennis to win his fifth French Open title …

…

异步通道套接字示例

异步通道套接字是 NIO.2 的瑰宝。对于任何专注于网络应用领域的 Java 开发人员来说,开发异步客户机/服务器应用都是一个有趣的项目。为了更好地理解如何完成这项任务,最简单的方法是遵循一组简单的步骤,并在讨论结束时将代码块粘在一起。我们将从基于Future表单的异步服务器开始。

编写异步服务器(基于未来)

我们希望开发一个异步服务器,它将把从它那里得到的一切信息反馈给客户机。在执行过程中,Future模式将负责跟踪接受连接、从客户端读取字节、向客户端写入字节等任务的状态。

创建新的异步服务器套接字通道

第一步包括为面向流的监听套接字创建一个异步通道,这是用java.nio.channels.AsynchronousServerSocketChannel完成的。更准确地说,这个任务是通过AsynchronousServerSocketChannel.open()方法完成的,如此处所示,其中异步服务器套接字通道被绑定到默认组:

AsynchronousServerSocketChannel asynchronousServerSocketChannel=
                                AsynchronousServerSocketChannel.open();

请记住,新创建的异步服务器套接字通道不绑定到本地地址。这将通过以下步骤完成。

您可以通过调用AsynchronousServerSocketChannel.isOpen()方法来检查异步服务器套接字是否已经打开或者已经成功打开,该方法返回相应的 B oolean值:

if (asynchronousServerSocketChannel.isOpen()) {
    …
}
设置异步服务器套接字通道选项

这是一个可选步骤。没有必需的选项(您可以使用缺省值),但是我们将显式地设置几个选项来向您展示如何做到这一点。更准确地说,异步服务器套接字通道支持两个选项:SO_RCVBUFSO_REUSEADDR。我们将设置它们,如下所示:

asynchronousServerSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
asynchronousServerSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);

您可以通过调用继承的方法supportedOptions()来找出异步服务器套接字通道支持哪些选项:

Set<SocketOption<?>> options = asynchronousServerSocketChannel.supportedOptions();
for(SocketOption<?> option : options) System.out.println(option);
绑定异步服务器套接字通道

此时,我们可以将异步服务器套接字通道绑定到本地地址,并将套接字配置为侦听连接。为此我们称之为AsynchronousServerSocketChannel.bind()方法。我们的服务器将在本地主机(127.0.0.1),端口 5555(任意选择)上等待传入的连接:

final int DEFAULT_PORT = 5555;
final String IP = "127.0.0.1";
asynchronousServerSocketChannel.bind(new InetSocketAddress(IP, DEFAULT_PORT));

另一种常见的方法是创建一个InetSocketAddress对象,不指定 IP 地址,只指定端口(有一个构造函数)。在这种情况下,IP 地址是通配符地址,端口号是指定的值。通配符地址是一个特殊的本地 IP 地址,只能使用进行绑定操作。

asynchronousServerSocketChannel.bind(new InetSocketAddress(DEFAULT_PORT));

此外,还有一个bind()方法,除了获取套接字绑定到的地址之外,还获取挂起连接的最大数量:

public abstract AsynchronousServerSocketChannel bind(SocketAddress local,int pc) throws
IOException

如果我们将null传递给bind()方法,也可以自动分配本地地址。也可以通过调用AsynchronousServerSocketChannel.getLocalAddress()方法找出绑定的本地地址,该方法继承自NetworkChannel接口。如果异步服务器套接字通道还没有被绑定,这将返回null

System.out.println(asynchronousServerSocketChannel.getLocalAddress());
接受连接

在打开和绑定之后,我们最终到达验收里程碑。我们通过调用AsynchronousServerSocketChannel.accept()方法来表示对接受新连接的不耐烦,该方法启动一个异步操作来接受对这个通道的套接字进行的连接,并返回一个Future对象来跟踪操作状态。我们称之为Future.get()方法,它在成功完成时返回新的连接。此外,您可能希望使用isDone()方法定期检查操作完成状态。返回的连接是 AsynchronousSocketChannel类的一个实例,它代表面向流的连接套接字的异步通道。

Future<AsynchronousSocketChannel> asynchronousSocketChannelFuture =
                                  asynchronousServerSocketChannel.accept();
AsynchronousSocketChannel asynchronousSocketChannel = asynchronousSocketChannelFuture.get();

Image 注意试图为一个未绑定的服务器套接字通道调用accept()方法会抛出一个NotYetBoundException异常。

一旦我们接受了一个新的连接,我们就可以通过调用AsynchronousSocketChannel.getRemoteAddress()方法找到远程地址:

System.out.println("Incoming connection from: " +  
                    asynchronousSocketChannel.getRemoteAddress());
通过连接传输数据

此时,服务器和客户端可以通过连接传输数据。它们可以发送和接收映射为字节数组的不同种类的数据包。实现传输(发送/接收)是一个灵活而具体的过程,因为它涉及许多选项。例如,对于我们的服务器,我们将使用ByteBuffer s,记住这是一个 echo 服务器——它从客户机读取的就是它写回的。下面是传输代码片段:

final ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
…
while (asynchronousSocketChannel.read(buffer).get() != -1) {

       buffer.flip();

       asynchronousSocketChannel.write(buffer).get();

       if (buffer.hasRemaining()) {
           buffer.compact();
       } else {
           buffer.clear();
       }
 }

前面的read()write()方法获取目的/源ByteBuffer,发起读/写操作,并返回Future<Integer>对象用于跟踪读/写操作状态。调用get()方法强制应用等待操作完成,然后返回读取/写入的字节数。首先,我们等待传入的字节被读取(这是服务器的回应)。第二,我们一直等到写操作结束,以避免更多的字节应该被回显,并且线程在前一个写操作完成之前启动新的写操作,这以WritePendingException异常结束。由于应用是在第一个客户端的读/写操作中被“捕获”的,所以在它完全服务于当前客户端之前,它不准备接受其他连接,这意味着一次只能服务于一个客户端。这是非常初级的,显然对服务器来说不令人满意,但是对我们的第一个异步服务器来说是可以接受的。

关闭频道

当一个通道变得无用时,它必须被关闭。为此,您可以调用AsynchronousSocketChannel.close()方法(这不会关闭服务器来监听传入的连接,它只是关闭一个客户端的通道)和/或AsynchronousServerSocketChannel.close()方法(这将关闭服务器来监听传入的连接;后续客户端将无法再定位该服务器)。

asynchronousServerSocketChannel.close();
asynchronousSocketChannel.close();

或者,我们可以通过将代码放入 Java 7 try-with-resources 特性来关闭这些资源。这是可能的,因为AsynchronousServerSocketChannelAsynchronousSocketChannel类实现了AutoCloseable接口。使用此功能将确保资源自动关闭。

将一切整合到一个 Echo 服务器中

现在我们已经拥有了创建 echo 服务器所需的一切。将前面的代码块放在一起,并添加必要的导入、意大利面条式代码等等,会生成以下 echo sever:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

public class Main {

 public static void main(String[] args) {

  final int DEFAULT_PORT = 5555;
  final String IP = "127.0.0.1";        

  //create an asynchronous server socket channel bound to the default group
  try (AsynchronousServerSocketChannel asynchronousServerSocketChannel =  
       AsynchronousServerSocketChannel.open()) {

       if (asynchronousServerSocketChannel.isOpen()) {

       //set some options
       asynchronousServerSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
       asynchronousServerSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
       //bind the asynchronous server socket channel to local address
       asynchronousServerSocketChannel.bind(new InetSocketAddress(IP, DEFAULT_PORT));

       //display a waiting message while … waiting clients
       System.out.println("Waiting for connections …");
       while (true) {
              Future<AsynchronousSocketChannel> asynchronousSocketChannelFuture =
                                                asynchronousServerSocketChannel.accept();

              try (AsynchronousSocketChannel asynchronousSocketChannel =
                   asynchronousSocketChannelFuture.get()) {

                  System.out.println("Incoming connection from: " +  
                                      asynchronousSocketChannel.getRemoteAddress());

                  final ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

                  //transmitting data                  
                  while (asynchronousSocketChannel.read(buffer).get() != -1) {

                         buffer.flip();

                         asynchronousSocketChannel.write(buffer).get();

                        if (buffer.hasRemaining()) {
                            buffer.compact();
                        } else {
                            buffer.clear();
                        }
                  }

                  System.out.println(asynchronousSocketChannel.getRemoteAddress() +
                                     " was successfully served!");

              } catch (IOException | InterruptedException | ExecutionException ex) {
                System.err.println(ex);
              }
       }
       } else {
         System.out.println("The asynchronous server-socket channel cannot be opened!");
       }

  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}

您可能仍然想知道如何接受多个客户端。一个简单的解决方案是将前面的代码包装到一个ExecutorService中。每当一个新的连接被接受,get()方法将它作为一个AsynchronousSocketChannel通道返回,我们就编写一个“worker”来维持或关闭与客户端的“对话”。之后,将 worker 提交给 executor,并准备接受一个新的连接。如果出现意外错误,那么我们关闭执行器并等待终止。下面的应用修改了前面的应用,以便它可以同时接受多个客户端:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Main {

 public static void main(String[] args) {

  final int DEFAULT_PORT = 5555;
  final String IP = "127.0.0.1";
  ExecutorService taskExecutor=
                  Executors.newCachedThreadPool(Executors.defaultThreadFactory());

  //create asynchronous server socket channel bound to the default group
  try (AsynchronousServerSocketChannel asynchronousServerSocketChannel =
                                      AsynchronousServerSocketChannel.open()) {

      if (asynchronousServerSocketChannel.isOpen()) {

       //set some options
       asynchronousServerSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
       asynchronousServerSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
       //bind the server socket channel to local address
       asynchronousServerSocketChannel.bind(new InetSocketAddress(IP, DEFAULT_PORT));

       //display a waiting message while … waiting clients
       System.out.println("Waiting for connections …");

       while (true) {                                
        Future<AsynchronousSocketChannel> asynchronousSocketChannelFuture =
               asynchronousServerSocketChannel.accept();

        try {                        
            final AsynchronousSocketChannel asynchronousSocketChannel =
                                           asynchronousSocketChannelFuture.get();
            Callable<String> worker = new Callable<String>() {

             @Override
             public String call() throws Exception {

             String host = asynchronousSocketChannel.getRemoteAddress().toString();

             System.out.println("Incoming connection from: " + host);

             final ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

             //transmitting data                  
             while (asynchronousSocketChannel.read(buffer).get() != -1) {

                    buffer.flip();

                    asynchronousSocketChannel.write(buffer).get();

                    if (buffer.hasRemaining()) {
                        buffer.compact();
                    } else {
                        buffer.clear();
                    }
             }

             asynchronousSocketChannel.close();
             System.out.println(host + " was successfully served!");
             return host;
             }
           };

           taskExecutor.submit(worker);                

        } catch (InterruptedException | ExecutionException ex) {
          System.err.println(ex);

          System.err.println("\n Server is shutting down …");

          //this will make the executor accept no new threads
          // and finish all existing threads in the queue
          taskExecutor.shutdown();

          //wait until all threads are finished                        
          while (!taskExecutor.isTerminated()) {
          }

          break;
        }
       }
      } else {
        System.out.println("The asynchronous server-socket channel cannot be opened!");
      }

  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}
编写异步客户端(基于未来)

现在让我们为我们的 echo 服务器开发一个客户机。假设我们有以下场景:客户端连接到我们的服务器,发送一个“Hello!”消息,然后继续发送 0 到 100 之间的随机数,直到生成数字 50。当生成数字 50 时,客户端停止发送并关闭通道。服务器将回显(写回)它从客户端读取的所有内容。接下来将讨论在这种情况下实现客户端的步骤。

创建新的异步套接字通道

第一步是为绑定到默认组的面向流的连接套接字创建一个异步通道。这是通过java.nio.channels.AsynchronousSocketChannel类完成的。更准确地说,这个任务是通过AsynchronousSocketChannel.open()方法完成的,如下所示:

AsynchronousSocketChannel asynchronousSocketChannel = AsynchronousSocketChannel.open();

请记住,新创建的异步套接字通道没有连接。您可以通过调用AsynchronousSocketChannel.isOpen()方法来检查异步服务器套接字是否已经打开或者已经成功打开,该方法返回相应的布尔值:

if (asynchronousSocketChannel.isOpen()) {
    …
}
设置异步套接字通道选项

异步套接字通道支持以下选项:SO_RCVBUFSO_REUSEADDRTCP_NODELAYSO_KEEPALIVESO_SNDBUF。这里显示了其中的一些:

asynchronousSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 128 * 1024);
asynchronousSocketChannel.setOption(StandardSocketOptions.SO_SNDBUF, 128 * 1024);
asynchronousSocketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);

您可以通过调用继承的方法supportedOptions()来发现异步服务器套接字通道支持的选项:

Set<SocketOption<?>> options = asynchronousSocketChannel.supportedOptions();
for(SocketOption<?> option : options) System.out.println(option);
连接异步通道的套接字

打开异步套接字通道(并可选地绑定它)后,您应该连接到远程地址(服务器端地址)。通过调用AsynchronousSocketChannel.connect()方法并向其传递远程地址作为InetSocketAddress的一个实例来表明连接的意图,如下所示(记住我们的 echo 服务器运行在 127.0.0.1,端口 5555 上):

final int DEFAULT_PORT = 5555;
final String IP = "127.0.0.1";
Void connect = asynchronousSocketChannel.connect
                                         (new InetSocketAddress(IP, DEFAULT_PORT)).get();

这个方法启动一个连接到这个通道的操作。该方法返回一个代表待定结果的Future<Void>对象。Futureget()方法在成功完成时返回null

通过连接传输数据

连接已经建立,所以我们可以开始传输数据包。下面的代码发送“Hello!”消息,然后发送随机数,直到生成数字 50。下面的read()write()方法获取一个目的/源ByteBuffer,发起一个读/写操作,并返回一个Future<Integer>对象用于跟踪读/写操作状态。调用get()方法将一直等到操作完成,并返回读取/写入的字节数。将get()方法与write()方法结合使用将避免这样的情况,即更多的字节应该被写入,并且线程在前一个写操作完成之前发起新的写操作,这以WritePendingException异常结束。

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
ByteBuffer helloBuffer = ByteBuffer.wrap("Hello !".getBytes());
ByteBuffer randomBuffer;
CharBuffer charBuffer;
Charset charset = Charset.defaultCharset();
CharsetDecoder decoder = charset.newDecoder();
…
asynchronousSocketChannel.write(helloBuffer).get();

while (asynchronousSocketChannel.read(buffer).get() != -1) {

     buffer.flip();

     charBuffer = decoder.decode(buffer);
     System.out.println(charBuffer.toString());

     if (buffer.hasRemaining()) {
         buffer.compact();
     } else {
        buffer.clear();
     }

     int r = new Random().nextInt(100);
     if (r == 50) {
         System.out.println("50 was generated! Close the asynchronous socket channel!");
         break;
     } else {
     randomBuffer = ByteBuffer.wrap("Random number:".concat(String.valueOf(r)).getBytes());                            
     asynchronousSocketChannel.write(randomBuffer).get();          
     }
}
关闭通道

当一个通道变得无用时,它必须被关闭。为此,您可以调用AsynchronousSocketChannel.close(),客户端将与服务器断开连接:

asynchronousSocketChannel.close();

同样,Java 7 try-with-resources 特性可用于自动关闭。

将一切整合到一个客户端

现在我们有了创建客户所需的一切。将前面的代码块放在一起,并添加必要的导入、意大利面条式代码等,将为我们提供以下客户端:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Random;
import java.util.concurrent.ExecutionException;

public class Main {

 public static void main(String[] args) {

  final int DEFAULT_PORT = 5555;
  final String IP = "127.0.0.1";
  ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
  ByteBuffer helloBuffer = ByteBuffer.wrap("Hello !".getBytes());
  ByteBuffer randomBuffer;
  CharBuffer charBuffer;
  Charset charset = Charset.defaultCharset();
  CharsetDecoder decoder = charset.newDecoder();

  //create an asynchronous socket channel bound to the default group
  try (AsynchronousSocketChannel asynchronousSocketChannel =
                                 AsynchronousSocketChannel.open()) {

       if (asynchronousSocketChannel.isOpen()) {

           //set some options
           asynchronousSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 128 * 1024);
           asynchronousSocketChannel.setOption(StandardSocketOptions.SO_SNDBUF, 128 * 1024);
           asynchronousSocketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
           //connect this channel's socket
           Void connect = asynchronousSocketChannel.connect
                          (new InetSocketAddress(IP, DEFAULT_PORT)).get();

           if (connect == null) {

               System.out.println("Local address: " +  
                                   asynchronousSocketChannel.getLocalAddress());

               //transmitting data
               asynchronousSocketChannel.write(helloBuffer).get();

               while (asynchronousSocketChannel.read(buffer).get() != -1) {

                      buffer.flip();

                      charBuffer = decoder.decode(buffer);
                      System.out.println(charBuffer.toString());

                      if (buffer.hasRemaining()) {
                          buffer.compact();
                      } else {
                          buffer.clear();
                      }

                      int r = new Random().nextInt(100);
                      if (r == 50) {
                          System.out.println("50 was generated! Close the asynchronous
                                                                          socket channel!");
                          break;
                      } else {
                          randomBuffer = ByteBuffer.wrap("Random
                                         number:".concat(String.valueOf(r)).getBytes());                            
                          asynchronousSocketChannel.write(randomBuffer).get();          
                      }
               }

           } else {
             System.out.println("The connection cannot be established!");
           }

       } else {
         System.out.println("The asynchronous socket channel cannot be opened!");
       }

  } catch (IOException | InterruptedException | ExecutionException ex) {
    System.err.println(ex);
  }
 }
}
测试 Echo 应用(基于未来)

测试应用是一项简单的任务。首先,启动服务器并等待,直到您看到消息“正在等待连接…”。继续启动客户端并检查输出。以下是可能的服务输出:


Waiting for connections …

Incoming connection from: /127.0.0.1:49578

Incoming connection from: /127.0.0.1:49579

Incoming connection from: /127.0.0.1:49580

/127.0.0.1:49579 was successfully served!

Incoming connection from: /127.0.0.1:49581

/127.0.0.1:49580 was successfully served!

/127.0.0.1:49578 was successfully served!

/127.0.0.1:49581 was successfully served!

以下是一些可能的客户端输出:


Hello !

Random number:78

Random number:72

Random number:29

Random number:77

Random number:35

Random number:0

…

50 was generated! Close the asynchronous socket channel!

编写异步服务器(基于 CompletionHandler)

接下来,我们想使用CompletionHandler模式而不是Future模式开发相同的 echo 异步服务器。实际上,我们将它们混合在一起,让CompletionHandler模式处理连接的接受操作,让Future模式处理读/写操作。我们打开异步服务器套接字通道,设置它的选项,并以与前面完全相同的方式绑定它。接下来,我们将重点放在表达接受连接的愿望上。为此,我们称之为accept()法:

public abstract <A> void accept(A attachment,
CompletionHandler<AsynchronousSocketChannel,? super A> handler)

该方法获取附加到 I/O 操作的对象(可以是null)和当连接被接受(或者操作失败)时调用的完成处理程序。传递给完成处理器的结果是新连接的AsynchronousSocketChannel

我们将CompletionHandler实现为一个匿名内部类,并覆盖它的方法。现在,完成处理程序的completed()方法负责维护和关闭与连接的客户端的“对话”。为此,我们使用与之前相同的read()write()方法,并使用相同的方法。只有当接受连接的操作失败时,才应该调用完成处理程序的failed()方法——我们只是抛出一个异常,并准备接受另一个连接。

一旦一个连接被接受,我们立即通过从completed()failed()方法中调用accept()方法为新的连接做好准备,如下所示(这是第一行代码):

asynchronousServerSocketChannel.accept(null, this);

最后,还有一个方面需要注意。因为这是一个异步应用,所以流将“遍历”整个应用并快速退出,以至于甚至不能建立或服务一个连接,这是不好的,因为我们希望服务器长时间等待和服务客户端。因此,我们必须添加一些代码来使流“悬在空中”,比如通过添加一个Thread.sleep()方法或一个System.in.read()方法,或者通过加入主线程并等待它死亡或其他方式。对于这个例子,我们将选择System.in.read()方法。

这里是CompletionHandler异步服务器:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;

public class Main {

 public static void main(String[] args) {

  final int DEFAULT_PORT = 5555;
  final String IP = "127.0.0.1";

  //create an asynchronous server socket channel bound to the default group
  try (AsynchronousServerSocketChannel asynchronousServerSocketChannel =
       AsynchronousServerSocketChannel.open()) {

       if (asynchronousServerSocketChannel.isOpen()) {

        //set some options
        asynchronousServerSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF,4 * 1024);
        asynchronousServerSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
        //bind the server socket channel to local address
        asynchronousServerSocketChannel.bind(new InetSocketAddress(IP, DEFAULT_PORT));

        //display a waiting message while … waiting clients
        System.out.println("Waiting for connections …");

        asynchronousServerSocketChannel.accept(null, new
                                      CompletionHandler<AsynchronousSocketChannel, Void>() {

         final ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

         @Override
         public void completed(AsynchronousSocketChannel result, Void attachment) {

          asynchronousServerSocketChannel.accept(null, this);

          try {
              System.out.println("Incoming connection from: " + result.getRemoteAddress());

              //transmitting data                  
              while (result.read(buffer).get() != -1) {

                     buffer.flip();

                     result.write(buffer).get();

                     if (buffer.hasRemaining()) {
                         buffer.compact();
                     } else {
                         buffer.clear();
                     }
              }
          } catch (IOException | InterruptedException | ExecutionException ex) {
            System.err.println(ex);
          } finally {
            try {
                result.close();
            } catch (IOException e) {
              System.err.println(e);
            }
          }
         }

         @Override
         public void failed(Throwable exc, Void attachment) {
          asynchronousServerSocketChannel.accept(null, this);

          throw new UnsupportedOperationException("Cannot accept connections!");
         }
       });

       // Wait
       System.in.read();

       } else {
         System.out.println("The asynchronous server-socket channel cannot be opened!");
       }

  } catch (IOException ex) {
    System.err.println(ex);
  }
 }
}
编写异步客户端(基于 CompletionHandler)

我们的服务器的客户机也可以用一个CompletionHandler来实现,用于处理连接请求操作。为此,我们将调用下面的connect()方法:

public abstract <A> void connect(SocketAddress remote, A attachment,
CompletionHandler<Void,? super A> handler)

该方法获取该通道要连接到的远程地址、附加到 I/O 操作的对象(可以是null)以及当连接成功建立或失败时调用的完成处理程序。

我们将CompletionHandler实现为一个匿名内部类,并覆盖它的方法。现在,完成处理程序的completed()方法负责维护和关闭与服务器的“对话”。为此,我们使用与之前相同的read()write()方法,并使用相同的方法。只有当连接操作失败时,才应该调用完成处理程序的failed()方法——在这种情况下,通道是关闭的。

下面是CompletionHandler异步客户端:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Random;
import java.util.concurrent.ExecutionException;

public class Main {

 public static void main(String[] args) {

   final int DEFAULT_PORT = 5555;
   final String IP = "127.0.0.1";

   //create an asynchronous socket channel bound to the default group
   try (AsynchronousSocketChannel asynchronousSocketChannel =
        AsynchronousSocketChannel.open()) {

        if (asynchronousSocketChannel.isOpen()) {

         //set some options
         asynchronousSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 128 * 1024);
         asynchronousSocketChannel.setOption(StandardSocketOptions.SO_SNDBUF, 128 * 1024);
         asynchronousSocketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);

         //connect this channel's socket
         asynchronousSocketChannel.connect(new InetSocketAddress(IP, DEFAULT_PORT), null,  
                      new CompletionHandler<Void, Void>() {

          final ByteBuffer helloBuffer = ByteBuffer.wrap("Hello !".getBytes());
          final ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
          CharBuffer charBuffer = null;
          ByteBuffer randomBuffer;
          final Charset charset = Charset.defaultCharset();
          final CharsetDecoder decoder = charset.newDecoder();

          @Override
          public void completed(Void result, Void attachment) {
           try {
               System.out.println("Successfully connected at: " +
                      asynchronousSocketChannel.getRemoteAddress());

               //transmitting data
               asynchronousSocketChannel.write(helloBuffer).get();

               while (asynchronousSocketChannel.read(buffer).get() != -1) {

                      buffer.flip();

                      charBuffer = decoder.decode(buffer);
                      System.out.println(charBuffer.toString());

                      if (buffer.hasRemaining()) {
                          buffer.compact();
                      } else {
                          buffer.clear();
                      }

                      int r = new Random().nextInt(100);
                      if (r == 50) {
                          System.out.println("50 was generated! Close the asynchronous
                                                                          socket channel!");
                          break;

                      } else {
                          randomBuffer = ByteBuffer.wrap("Random
                                             number:".concat(String.valueOf(r)).getBytes());
                          asynchronousSocketChannel.write(randomBuffer).get();
                      }
               }
           } catch (IOException | InterruptedException | ExecutionException ex) {
             System.err.println(ex);
           } finally {
             try {
                 asynchronousSocketChannel.close();
             } catch (IOException ex) {
               System.err.println(ex);
             }
           }
          }

          @Override
          public void failed(Throwable exc, Void attachment) {
           throw new UnsupportedOperationException("Connection cannot be established!");
          }
         });

         System.in.read();

         } else {
           System.out.println("The asynchronous socket channel cannot be opened!");
         }

   } catch (IOException ex) {
     System.err.println(ex);
   }
 }
}
测试 Echo 应用(基于 CompletionHandler)

测试应用是一项简单的任务。首先,启动服务器,等待直到您看到消息“正在等待连接…”继续启动客户机并检查输出。以下是可能的服务器输出:


Waiting for connections …

Incoming connection from: /127.0.0.1:50369

Incoming connection from: /127.0.0.1:50370

Incoming connection from: /127.0.0.1:50371


Incoming connection from: /127.0.0.1:50372

下面显示了可能的客户端输出:


Hello !

Random number:19

Random number:54

Random number:28

Random number:59

Random number:34

Random number:60

…

50 was generated! Close the asynchronous socket channel!

使用读/写操作和 CompletionHandler

在前面的示例中,我们已经通过Future模式管理了读/写操作。如果您想将一个CompletionHandler与一个读/写操作相关联,那么您可以使用下一个AsynchronousSocketChannel read()write()方法:

  • 第一个read()方法启动一个操作,从该通道读取一个字节序列到给定缓冲区的一个子序列中(称为异步分散读取)。操作必须在指定的超时时间内结束:public abstract <A> void read(ByteBuffer[] dsts, int offset, int length, long timeout, TimeUnit unit, A attachment, CompletionHandler<Long,? super A> handler)
  • 这个方法启动一个操作,从这个通道读取一个字节序列到给定的缓冲区:public final <A> void read(ByteBuffer dst, A attachment, CompletionHandler<Integer,? super A> handler)
  • 此方法启动一个操作,将一个字节序列从该通道读入给定的缓冲区。操作必须在指定的超时时间内结束:public abstract <A> void read(ByteBuffer dst,long timeout, TimeUnit unit, A attachment, CompletionHandler<Integer,? super A> handler)

类似于这些方法,但是对于写操作,我们有一个方法用于异步采集写:

public abstract <A> void write(ByteBuffer[] srcs, int offset, int length, long timeout,
TimeUnit unit, A attachment, CompletionHandler<Long,? super A> handler)

我们还有两种方法可以将字节序列从给定的缓冲区写入这个通道:

public final <A> void write(ByteBuffer src, A attachment,
CompletionHandler<Integer,? super A> handler)

public abstract <A> void write(ByteBuffer src, long timeout, TimeUnit unit,
A attachment, CompletionHandler<Integer,? super A> handler)
编写一个基于自定义组的异步客户机/服务器

以前的客户机/服务器应用是使用默认组开发的。我们可以指定一个自定义组作为一个传递给AsynchronousServerSocketChannel.open()方法和/或AsynchronousSocketChannel.open()方法的AsynchronousChannelGroup对象。首先,我们创建一个自定义组。此示例创建一个缓存线程池,其初始大小为一个线程:

AsynchronousChannelGroup threadGroup = null;
…
ExecutorService executorService = Executors
                .newCachedThreadPool(Executors.defaultThreadFactory());
try {
    threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);
} catch (IOException ex) {
  System.err.println(ex);
}

下面的示例创建了一个正好有五个线程的固定线程池:

AsynchronousChannelGroup threadGroup = null;
…
try {
    threadGroup = AsynchronousChannelGroup.withFixedThreadPool(5,
                                           Executors.defaultThreadFactory());
    } catch (IOException ex) {
      System.err.println(ex);
}

并且,threadGroup可以被传递给面向流的监听套接字的异步通道——如果该组被关闭并且一个连接被接受,则该连接被关闭,并且操作以一个IOException异常完成并导致ShutdownChannelGroupException:

AsynchronousServerSocketChannel asynchronousServerSocketChannel =
AsynchronousServerSocketChannel.open(threadGroup);

当一个新的连接被接受时,产生的AsynchronousSocketChannel将被绑定到与该通道相同的AsynchronousChannelGroup

ThreadGroup可以被传递给面向流的连接套接字的异步通道——如果该组被关闭并且一个连接是活动的,则该连接被关闭,并且该操作以一个IOException异常完成并导致ShutdownChannelGroupException:

AsynchronousSocketChannel asynchronousSocketChannel =
AsynchronousSocketChannel.open(threadGroup);

现在,您可以修改前面的应用以使用自定义组。

提示

本章介绍的应用适用于教育目的,但不适用于生产环境。如果您需要为生产环境编写应用,那么牢记以下提示是一个好主意。

使用字节缓冲池和节流读取操作

考虑这样一个场景:一个AsynchronousSocketChannel.read()方法从数千个客户端读取数据,并创建数千个ByteBuffer。该方法能够从大量缓慢的客户端读取数据一段时间,但最终会被到达的大量客户端淹没。您可以通过应用一个技巧来避免这种情况:使用字节缓冲池并限制读取操作。此外,如果您的字节缓冲区变得太大,可能会有耗尽内存的危险,因此您必须注意内存消耗(可能需要调整 Java 堆参数,如XmsXmx)。

仅在短读取操作中使用阻塞

对于下一个场景,假设一个AsynchronousSocketChannel.read()方法正在以Future模式从客户端读取,这意味着get()方法将等待读取操作完成,从而阻塞线程。在这种情况下,必须确保没有锁定线程池,尤其是在使用固定线程池的情况下。您可以通过仅对短读取操作使用阻塞来避免这种情况。使用超时也是一种解决方案。

使用 FIFO-Q 并允许写操作阻塞

现在关注写操作,考虑一个场景,其中一个AsynchronousSocketChannel.write()方法将字节无阻塞地写入它的客户端——它启动写操作并继续其他任务。但是,转移到其他任务可能会导致线程再次调用write()方法,并且完成处理程序还没有被之前的写调用调用。坏主意!将抛出一个WritePendingException异常。您可以通过确保在启动新的写操作之前调用完成处理程序complete()方法来解决这个问题。为此,对字节缓冲区使用先进先出队列(FIFO-Q ),仅在前一个write()完成时写入。因此,使用 FIFO-Q 并允许写操作阻塞。

另请参阅本章前面的“字节缓冲器注意事项”一节。

编写异步数据报应用

在撰写本文时,AsynchronousDatagramChannel类不再可用(它存在于 Java 7 草案 ea-b89 中),因此包含此讨论以防将来再次出现。如果是这样,这个类将遵循与AsynchronousServerSocketChannelAsynchronousSocketChannel类相同的趋势:它将提供两个open()方法(一个用于默认组,一个用于自定义组)、一个bind()方法和一个connect()方法。它还将拥有用于读/写操作的专用方法:一组用于无连接情况的send() / receive()方法,以及一组用于连接情况的read() / write()方法。所有读/写操作都将是异步的,并将支持FutureCompletionHandler模式。

异步数据通道

这个类是在早期不稳定的 Java 7 版本中引入的,后来又被删除了。它有可能会出现在以后的版本中,所以这里提供了一些关于它的主要特性的指南。

AsynchronousDatagramChannel类代表面向数据报套接字的异步通道。该通道支持异步打开和读写操作(未连接通道通过send() / receive()方法,连接通道通过read() / write()方法)。这意味着这些操作可以被FutureCompletionHandler机制跟踪。另一方面,这个通道实现NetworkChannel用于绑定和设置/获取套接字选项,实现MulticastChannel用于加入多播组。

如果将来使用异步数据报通道,必须注意考虑以下几个方面:

  • 如果该通道关闭,尝试连接该通道可能会导致ClosedChannelException异常。
  • 试图在未连接的通道上调用 I/O 操作将导致抛出NotYetConnectedException异常。
  • 通过显式调用继承的close()方法(从AsynchronousChannel接口)关闭异步数据报套接字通道会导致通道上所有未完成的异步操作以AsynchronousCloseException异常完成。通道关闭后,进一步尝试启动异步 I/O 操作会立即完成,原因为ClosedChannelException。此外,继承的MulticastChannel.close()方法可用于关闭通道。

下面的代码片段是从 Java 7 DRAFT ea-b89 的官方文档中一字不差地复制过来的,目的是让您大致了解将来可能会有什么样的版本:

final AsynchronousDatagramChannel dc = AsynchronousDatagramChannel.open()
    .bind(new InetSocketAddress(4000));

  // print the source address of all packets that we receive
  dc.receive(buffer, buffer, new CompletionHandler<SocketAddress,ByteBuffer>() {
  public void completed(SocketAddress sa, ByteBuffer buffer) {
   System.out.println(sa);
   buffer.clear();
   dc.receive(buffer, buffer, this);

  }
  public void failed(Throwable exc, ByteBuffer buffer) {
   …
  }
  });

总结

在本章中,您学习了如何使用 NIO.2 异步通道 API。在简要介绍了同步 I/O 和异步 I/O 之间的区别之后,您会对这个 API 结构有一个详细的了解。之后,您看到了理论付诸实践,从java.nio.channels.AsynchronousChannel接口开始,它扩展了一个支持异步 I/O 操作的通道。然后介绍了实现这个接口来对文件和套接字进行异步操作的三个类:AsynchronousFileChannelAsynchronousSocketChannelAsynchronousServerSocketChannel。目前不可用的AsynchronousDatagramChannel职业也在这一章中描述了,以防它在未来再次出现。本章还介绍了AsynchronousChannelGroup,包括异步通道组的概念。本章最后给出了一些关于开发基于异步的应用的技巧。

十、要记住的重要事情

本章的第一部分提供了一些值得了解的信息,或者至少是听说过的信息。这些信息不适合前面的任何章节,你可能不会很快使用它,但它可能有一天会有所帮助。涵盖了以下主题:

  • 重构java.io.File代码
  • 使用 ZIP 文件系统提供程序
  • 关于自定义文件系统提供程序的注意事项

我们用书中介绍和使用的一组 NIO.2 里程碑方法来结束这一章(以及这本书)。每当你需要快速提醒或概述这些方法时,你可以很容易地翻阅这本书的最后几页。

重构 java.io.File 代码

如果你已经开发了几个基于java.io.File的应用,那么你应该熟悉这个类最常见的方法。但是,如果您已经开发了许多基于java.io.File的应用,那么您不仅应该熟悉它的方法,还应该熟悉它的方法的缺点。例如,许多这些方法在失败时不会抛出异常,没有对符号链接的真正支持,元数据访问效率低下,跨平台的文件重命名不一致,一些方法不可伸缩,等等——所有这些对许多高级 Java 开发人员来说应该听起来很熟悉,对初级开发人员来说可能很可怕。

虽然低年级学生将会轻快地跳到 Java 7(它修复了这些缺点,是这个领域的一股新鲜空气),但高年级学生必须花一些宝贵的时间来重构现有代码,以支持 Java 7(或者更准确地说,java.nio.file类)。

重构java.io.File代码的第一个里程碑可以认为是通过java.io.File.toPath()方法将File对象转换为java.nio.file.Path对象:

File file ...;
Path path_from_file = file.toPath();

转换后,您可以利用Path功能。

然而,尽管这是最简单的解决方案,但它可能并不总能满足您的需求。有时,您需要重写文件 I/O 代码并将代码与java.nio.file类对齐,为此,您可以使用两个 API 之间的一一对应关系。表 1-1 显示了这种对应关系。

Image

Image

Image

Image

该表将使您的代码从 Java 5 或 6 到 Java 7 的转换更加容易。

使用 ZIP 文件系统提供商

遵照 NIO.2,文件系统涉及到能够管理和访问文件系统对象的容器的一般概念。文件系统对象通常是文件存储(例如,在 Windows 上我们通常有C:, D:E:文件存储,我们称它们为分区),但它也可以是目录或文件。

基于这种方法,NIO.2 API 在 Java 7 中引入了开发可用于管理文件系统对象的定制文件系统提供者的能力。此外,它提供了一个定制文件系统提供者的实现——ZIP 文件系统提供者(ZFSP)——可以按原样使用,和/或可以作为开发其他定制文件系统提供者的灵感来源。ZFSP 将 ZIP/JAR 文件视为一个文件系统,并提供操作文件内容的能力。ZFSP 为每个 ZIP/JAR 文件创建一个文件系统。

在这一节中,您将看到如何使用 ZFSP 创建一个 ZIP 文件系统(C:\rafaelnadal\tournaments\2009\Tickets.zip归档文件将成为一个 ZIP 文件系统),并从C:\rafaelnadal\tournaments\2009\目录中的新 ZIP 文件系统中复制一个名为AEGONTickets.png的文件作为AEGONTicketsCopy.png.

首先,我们创建一个简单的HashMap,它包含 ZFSP 创建的 ZIP 文件系统的可配置属性。目前,有两种属性可以配置:

  • create:该值可以是truefalse,但类型为java.lang.String。如果该值为 true,ZFSP 将创建一个新的 ZIP 文件(如果该文件不存在)。
  • encoding:该值为一个java.lang.String,表示编码方案(如 UTF-8、US-ASCII、ISO-8859-1 等)。).UTF-8 是默认值。

因此,我们可以指出 ZIP 文件存在,并且需要的编码是 ISO-8859-1,如下所示:

Map<String, String> env = new HashMap<>();
env.put("create", "false");
env.put("encoding", "ISO-8859-1");

为了创建一个新的 ZIP 文件系统或获取对现有系统的引用,我们使用了java.nio.file.FileSystems类的工厂方法。通过指定 ZIP/JAR 文件的路径来创建 ZIP 文件系统。这可以通过使用在java.net.JarURLConnection类中定义的 JAR URL 语法来实现:

URI uri = URI.create("jar:file:/C:/rafaelnadal/tournaments/2009/Tickets.zip");
FileSystem ZipFS = FileSystems.newFileSystem(uri, env);

此外,还有两种方法可以完成这一步:

public static FileSystem newFileSystem(Path path, ClassLoader loader) throws IOException
public static FileSystem newFileSystem(URI uri, Map<String,?> env, ClassLoader loader) throws
IOException

现在我们有了一个 ZIP 文件系统的实例,我们可以调用java.nio.file.FileSystemjava.nio.file.Path类的方法来执行操作,比如复制、移动和重命名文件,以及修改文件属性。我们想从档案中复制出AEGONTickets.png条目。下面的代码将为我们完成这项工作:

`import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class Main {

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

//set zip file system properties
   Map<String, String> env = new HashMap<>();
   env.put("create", "false");
   env.put("encoding", "ISO-8859-1");

//locate file system with java.net.JarURLConnection
   URI uri = URI.create("jar:file:/C:/rafaelnadal/tournaments/2009/Tickets.zip");

try (FileSystem ZipFS = FileSystems.newFileSystem(uri, env)) {
        Path fileInZip = ZipFS.getPath("/AEGONTickets.png");
        Path fileOutZip = Paths.get("C:/rafaelnadal/tournaments/2009/AEGONTicketsCopy.png");

//copy AEGONTickets.png outside the archive
      Files.copy(fileInZip, fileOutZip);       System.out.println("The file was successfully copied!");
      }
   }
}`

如果一切正常,那么您将会看到下面的消息,并且文件AEGONTicketsCopy.png应该存在于C:\rafaelnadal\tournaments\2009目录中。


The file was successfully copied!

开发定制文件系统提供商的考虑事项

在上一节中,您看到了如何使用自定义文件系统提供程序。如果您决定尝试编写自己的自定义文件系统提供程序,那么考虑本节中列出的注意事项是一个好主意。首先,您必须知道支持这种尝试的主类是java.nio.file.spi.FileSystemProvider.,一个定制的文件系统提供者将实现这个类作为java.nio.file.FileSystem实例的工厂。文件系统提供商由 URI 方案标识,例如filejarmemorycd,文件系统的 URI 具有与文件系统提供商的 URI 方案相匹配的 URI 方案。

因此,实现自定义文件系统提供程序需要编写至少两个类,并记住一组必需的步骤。

创建自定义文件系统提供程序类

您可以按照以下步骤创建这样一个类:

  1. 扩展java.nio.file.spi.FileSystemProvider类。
  2. 为提供者定义一个 URI 方案(getScheme()方法应该返回这个 URI 方案)。
  3. 创建内部缓存,用于管理提供者创建的文件系统。
  4. 实现newFileSystem()getFileSystem()方法,用于创建文件系统和检索对现有文件系统的引用。
  5. 实现newFileChannel()newAsyncronousFileChannel()方法,该方法返回一个FileChannel对象,该对象允许在文件系统中读取或写入文件。

创建自定义文件系统类

按照以下步骤创建这样一个类:

  1. 扩展java.nio.file.FileSystem类。
  2. 根据您的需要实现文件系统的方法(您可能需要定义根的数量、读/写访问、文件存储等。).

要了解更多细节,您可能需要仔细查看官方文档,网址为[download.oracle.com/javase/7/docs/technotes/guides/io/fsp/filesystemprovider.html](http://download.oracle.com/javase/7/docs/technotes/guides/io/fsp/filesystemprovider.html)

有用的方法

我们几乎已经完成了 NIO.2 的旅程。这最后一节介绍了一些有用的方法,可以在任何 NIO.2 应用中帮助您。

默认文件系统

您已经在本书中多次看到了如何获取默认文件系统,但是我们把它放在这里,以便您在忘记时可以轻松地访问这些信息。获取默认文件系统是通过FileSystems.getDefault()方法完成的:

FileSystem fs = FileSystems.getDefault();

文件存储

获得文件系统文件存储是本书中另一个广泛讨论的主题,但是为了快速提醒,请到这里来。下面是所需的代码:

for (FileStore store: FileSystems.getDefault().getFileStores()) {
   ...
}

文件的路径

下面是获取文件路径的方法:

Path path = Paths.get("...");
Path path = FileSystems.getDefault().getPath("...");
Path path = Paths.get(URI.create("file:///..."));
Path path = Paths.get(System.getProperty("user.home"), "...");

路径字符串分隔符

如您所知,路径字符串分隔符是操作系统相关的。要检索默认文件系统的Path字符串分隔符,您可以使用以下方法之一:

String separator = File.separator;
String separator = FileSystems.getDefault().getSeparator();

总结

在本章中,你学习了如何将基于java.io.File类的代码转换成基于java.nio.file.Path类的代码。此外,您还学习了如何使用 ZIP 文件系统提供程序,以及一些关于创建自定义文件系统提供程序的信息。这一章(和这本书)以书中最常用的代码片段结尾。

posted @ 2024-08-06 16:33  绝不原创的飞龙  阅读(32)  评论(0编辑  收藏  举报