在本章中,我们将介绍:
- 管理简单文件
- 对文件使用缓冲 IO
- 使用
SeekableByteChannel
的随机访问 IO - 使用
AsynchronousServerSocketChannel
类管理异步通信 - 使用
AsynchronousFileChannel
类写入文件 - 使用
AsynchronousFileChannel
类读取文件 - 使用
SecureDirectoryStream
类
在 Java7 中,我们发现它的 IO 功能有很多改进。其中大部分是在被称为NIO2的 java.nio
包中发现的。在本章中,我们将重点介绍对流媒体和基于通道的 IO 的新支持。流是一个连续的数据序列。流 IO一次作用于单个字符,而通道 IO为每个操作提供一个缓冲区。
我们从用于处理简单文件的新技术开始。这些都由 Files
类支持,并在管理简单文件配方中讨论。缓冲 IO通常效率更高,并在文件使用缓冲 IO配方中进行了解释。
java.nio.channels
包的 ByteChannel
接口是一个可以读写字节的通道。 SeekableByteChannel
接口扩展 ByteChannel
接口,以保持通道内的位置。可使用寻道型随机 IO 操作更改位置。此功能在使用 SeekableByteChannel 配方的随机访问 IO 中讨论。
Java7 增加了对异步通道功能的支持。这些操作的异步本质是它们不会阻塞。异步应用程序可以继续执行,而无需等待 IO 操作完成。IO 完成后,将调用应用程序的方法。有四个新的 java.nio.channels
包异步通道类:
AsynchronousSocketChannel
AsynchronousServerSocketChannel
AsynchronousFileChannel
AsynchronousChannelGroup
前两者在服务器/客户机环境中一起使用,并在使用 AsynchronousServerSocketChannel 类管理异步通信的中详细介绍。
AsynchronousFileChannel
类用于需要以异步方式执行的文件操作。支持写入和读取操作的方法分别在使用 AsynchronousFileChannel 类的写入文件和使用 AsynchronousFileChannel 类的读取文件中进行了说明。
AsynchronousChannelGroup
类提供了一种将异步通道分组在一起以共享资源的方法。该类的用法如所示中有更多的部分使用 AsynchronousFileChannel 类方法读取文件。
java.nio.file
包的 SecureDirectoryStream
类支持更安全地访问目录。使用 SecureDirectoryStream 配方的中解释了此类的用法。但是,底层操作系统必须为此类提供本地支持。
users.txt
文件用于本章中的几个示例。假设 users.txt
文件的内容最初包含以下内容:
- 上下快速移动
- 玛丽
- 俏皮话
- 汤姆
- 特德
如果文件的内容不同,那么示例的输出也会相应不同。
本章中的几个配方将打开一个文件。其中一些打开方法将使用枚举参数指定文件的打开方式。 java.nio.file
包的 OpenOption
接口指定如何打开文件, StandardOpenOption
枚举实现此接口。枚举值汇总如下表所示:
列举
|
意思
|
| --- | --- |
| APPEND
| 字节被写入文件的末尾 |
| CREATE
| 如果新文件不存在,则创建新文件 |
| CREATE_NEW
| 仅当文件不存在时才创建新文件 |
| DELETE_ON_CLOSE
| 关闭文件时删除该文件 |
| DSYNC
| 文件的每次更新都是同步写入的 |
| READ
| 开放供读访问 |
| SPARSE
| 稀疏文件 |
| SYNC
| 对文件或元数据的每次更新都是同步写入的 |
| TRUNCATE_EXISTING
| 打开文件时将文件长度截断为 0 |
| WRITE
| 打开文件以进行写访问 |
虽然这里没有讨论, java.nio.channels
包的 NetworkChannel
接口是在 Java7 中引入的。这表示到网络套接字的通道。包括本章讨论的 AsynchronousServerSocketChannel
和 AsynchronousSocketChannel
类在内的几个类实现了它。它有一个将套接字绑定到本地地址的 bind
方法,允许检索和设置各种查询套接字选项。它允许使用特定于操作系统的选项,这些选项可用于高性能服务器。
java.nio.channels
包的 MulticastChannel
对 Java7 来说也是新的。它用于支持组的多播操作。它由 DatagramChannel
类实现。此接口的方法支持组成员的加入和离开。
套接字直接协议****SDP是一种网络协议,支持使用InfiniBand****IB进行流连接。IB 技术支持高速外围设备(如磁盘)之间的点对点双向串行链路。IB 的一个重要部分是它能够将数据从一台计算机的内存直接移动到另一台计算机的内存。
Java 7 在 Solaris 和 Linux 操作系统上支持 SDP。 java.net
和 java.nio.channels
包中的几个类透明地支持它。但是,必须先启用 SDP,然后才能使用它。有关如何启用 IB 然后创建 SDP 配置文件的详细信息,请参见http://download.oracle.com/javase/tutorial/sdp/sockets/index.html 。
有些文件很小,包含简单的数据。这通常适用于文本文件。当一次读取或写入文件的全部内容是可行的时,有几个 Files
类方法可以很好地工作。
在这个配方中,我们将研究处理简单文件的技术。首先,我们将研究如何读取这些类型文件的内容。在中还有更多的部分,我们将演示如何向它们写入。
一次读取文件的全部内容:
- 创建一个表示文件的
java.nio.file.Path
对象。 - 使用
java.nio.file.Files
类的readAllBytes
方法。
-
创建一个新的控制台应用程序。我们将读取并显示 docs 目录中找到的
users.txt
文件的内容。向应用程序中添加以下主要方法:public static void main(String[] args) throws IOException { Path path = Paths.get("/home/docs/users.txt"); byte[] contents = Files.readAllBytes(path); for (byte b : contents) { System.out.print((char)b); } }
-
Execute the application. Your output should reflect the contents of the file. Here is one possible output:
鲍勃
玛丽
莎莉
汤姆
Ted
我们首先创建一个 Path
对象,它表示 users.txt
文件。使用 path
对象作为参数执行 Files
类的 readAllBytes
方法。该方法返回一个字节数组。
接下来,使用 for 语句对数组进行迭代。每个 byte
被转换成 char
并显示。
一旦读取了所有字节或发生异常,该方法将自动关闭文件。除了可能出现的 IOException
之外,如果无法创建足够大的数组来保存文件内容,还可能抛出 OutOfMemoryError
。如果发生这种情况,则应使用替代方法。
我们还关注:
- 写入简单文件
- 读取作为列表返回的文件的所有行
在下面的示例中,我们将获取 users.txt
文件的内容并向列表中添加一个新名称。使用前面的代码,注释输出内容值的 for 循环。然后,在调用 Path
对象上的 readAllBytes
方法后,创建一个指向新的、不存在的文本文件的新 path
对象。然后声明一个名为 name
的 String
变量,并调用字符串上的 getBytes
方法以返回一个新的 byte
数组。
Path newPath = Paths.get("/home/docs/newUsers.txt");
byte[] newContents = "Christopher".getBytes();
接下来,我们将使用 Files
类写入方法创建一个与 users.txt
文件内容相同的新文件,然后将 String
名称附加到此列表中。在 write
方法的第一次调用中,我们使用 newPath
指定应该在哪里创建文件,使用内容字节数组指定应该使用哪些信息,使用 StandardOpenOption.CREATE
参数指定如果文件不存在应该创建文件。在 write
方法的第二次调用中,我们再次使用 newPath
,但随后我们使用字节数组 newContents
和 StandardOpenOption.APPEND
来指定名称应附加到现有文件中。
Files.write(newPath, contents, StandardOpenOption.CREATE);
Files.write(newPath, newContents, StandardOpenOption.APPEND);
如果您打开 newUsers.txt
文件,您将看到 users.txt
文件中的名称列表,后面是您使用 newContents
字节数组指定的名称。
还有一个重载的 write
方法,它使用相同的 Path
对象作为第一个参数,并使用 Iterable
接口迭代 CharSequence
作为第二个参数。此方法的第三个参数定义要使用的 Charset
。 StandardOpenOptions
作为可选参数提供,如前一版本所示。本章导言中列出了开放式期权。
如果您有一个简单的文件要从中读取,那么使用 readAllLines
方法是有效的。该方法采用两个参数,即一个 Path
对象和一个 Charset
。该方法可能抛出一个 IOException
。在下面的示例中,我们使用 users.txt
文件的路径和 Charset
类的 defaultCharset
方法来执行 readAllLines
方法。该方法返回一个字符串列表,我们在 for 循环中打印出来。
try {
Path path = Paths.get("/home/docs/users.txt");
List<String> contents = Files.readAllLines(path, Charset.defaultCharset());
for (String b : contents) {
System.out.println(b);
}
} catch (IOException e) {
e.printStackTrace();
}
您的输出应类似于以下内容:
Bob
Mary
Sally
Tom
Ted
请注意, readAllLines
方法返回的字符串不包括行尾字符。
readAllLines
方法识别以下线路终端:
\u000D
后接\u000A
(CR/LF)\u000A
(左前)\u000D
(CR)
在本章中:
- 将缓冲的IO*用于文件:*此方法检查 Java 7 中如何处理缓冲的 IO
- *使用 AsynchronousFileChannel 类写入文件:*此配方说明了如何以异步方式执行文件 IO
- *使用 AsynchronousFileChannel 类读取文件:*此配方说明了如何以异步方式执行文件 IO
缓冲 IO 为访问文件提供了更有效的技术。 java.nio.file
包的 Files
类的两个方法返回 java.io
包 BufferedReader
或 BufferedWriter
对象。这些类为处理文本文件提供了一种易于使用且高效的技术。
我们将首先说明读取操作。在中还有更多的部分,我们将演示如何写入文件。
要使用 BufferedReader
对象读取文件:
- 创建一个表示感兴趣文件的
Path
对象 - 使用
newBufferedReader
方法创建一个新的BufferedReader
对象 - 使用适当的
read
方法读取文件
-
使用以下
main
方法创建一个新的控制台应用程序。在此方法中,我们将读取users.txt
文件的内容,然后显示其内容。public static void main(String[] args) throws IOException { Path path = Paths.get("/home/docs/users.txt"); Charset charset = Charset.forName("ISO-8859-1"); try (BufferedReader reader = Files.newBufferedReader(path, charset)) { String line = null; while ((line = reader.readLine()) != null) { System.out.println(line); } }
-
执行应用程序。您的输出应反映
users.txt
文件的内容,该文件应类似于以下内容:Bob Mary Sally Tom Ted
创建了表示 users.txt
文件的 Path
对象,然后创建了 Charset
。本例使用 ISO 拉丁字母表 1。根据使用的平台,可以使用其他字符集。
try with resource 块用于创建 BufferedReader
对象。这种类型的 try
块对 Java 7 来说是新的,在第一章Java 语言改进中的使用资源块来改进异常处理代码配方中有详细说明。这将导致在块完成时自动关闭 BufferedReader
对象。
while 循环读取文件的每一行。然后将每一行显示到控制台。根据需要抛出任何 IOExceptions
。
当一个字节存储在一个文件中时,其含义可能会因预期的编码方案而有所不同。 java.nio.charset
包的 Charset
类提供字节序列和 16 位 Unicode 代码单元之间的映射。 newBufferedReader
方法的第二个参数指定要使用的编码。JVM 支持一组标准的字符集,如 Charset
类的 Java 文档所述。
我们还需要考虑:
- 使用
BufferedWriter
类写入文件 Files
类中的无缓冲 IO 支持
newBufferedWriter
方法打开或创建一个用于写入的文件,并返回一个 BufferedWriter
对象。该方法需要两个参数, Path
对象和指定的 Charset
,并且可以使用可选的第三个参数。第三个参数指定了一个 OpenOption
,详见简介中的表格。如果未指定任何选项,则该方法将表现为指定了 CREATE, TRUNCATE_EXISTING
和 WRITE
选项,并将创建新文件或截断现有文件。
在下面的示例中,我们指定一个新的 String
对象,该对象包含要添加到 users.txt
文件的名称。在声明了我们的 Path
对象之后,我们使用 try with resource 块打开一个新的 BufferedWriter
。在本例中,我们使用默认的系统字符集和 StandardOpenOption.APPEND
指定要将名称附加到 users.txt
文件的末尾。在 try 块中,我们首先对 BufferedWriter
对象调用 newline
方法,以确保我们的名字出现在新行上。然后我们对我们的 BufferedWriter
对象调用 write
方法,使用我们的 String
作为第一个参数,零表示字符串的开始字符,而 String
的长度表示整个 String
应该被写入。
String newName = "Vivian";
Path file = Paths.get("/home/docs/users.txt");
try (BufferedWriter writer = Files.newBufferedWriter(file, Charset.defaultCharset(), StandardOpenOption.APPEND)) {
writer.newLine();
writer.write(newName, 0, newName.length());
}
如果检查 users.txt
文件的内容,则新名称应附加到文件中其他名称的末尾。
虽然非缓冲 IO 不如缓冲 IO 有效,但它有时仍然很有用。 Files
类通过 newInputStream
和 newOutputStream
方法为 InputStream
和 OutputStream
类提供支持。这些方法在需要访问非常小的文件,或者方法或构造函数需要 InputStream
或 OutputStream
作为参数的情况下非常有用。
在下面的示例中,我们将执行一个简单的复制操作,将 users.txt
文件的内容复制到 newUsers.txt
文件。我们首先声明两个 Path
对象,一个引用源文件 users.txt
,另一个指定目标文件 newUsers.txt
。然后,在 try-with-resource 块中,我们使用 newInputStream
和 newOutputStream
方法同时打开 InputStream
和 OutputStream
。在块中,我们从源文件读入数据并将其写入目标文件。
Path file = Paths.get("/home/docs/users.txt");
Path newFile = Paths.get("/home/docs/newUsers.txt");
try (InputStream in = Files.newInputStream(file);
OutputStream out = Files.newOutputStream( newFile,StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
int data = in.read();
while (data != -1){
out.write(data);
data = in.read();
fileunbuffered IO}
}
在检查 newUsers.txt
文件时,您应该看到内容与 users.txt
文件的内容相匹配。
在本章中:
- *管理简单文件:*这个方法展示了 Java 7 中如何处理非缓冲 IO
- *使用 AsynchronousFileChannel 类写入文件:*此配方说明了如何以异步方式执行文件 IO
- *使用 AsynchronousFileChannel 类读取文件:*此配方说明了如何以异步方式执行文件 IO
对文件的随机访问对于更复杂的文件很有用。它允许以非顺序方式访问文件中的特定位置。 java.nio.channels
包的 SeekableByteChannel
接口基于通道 IO 提供此支持。通道为批量数据传输提供了一种低级方法。在本配方中,我们将使用 SeekableByteChannel
访问文件。
要获取 SeekableByteChannel
对象,请执行以下操作:
- 创建一个表示文件的
Path
对象。 - 使用
Files
类的静态newByteChannel
方法获取SeekableByteChannel
对象。
-
Create a new console application using the following
main
method. We will define abufferSize
variable to control the size of the buffer used by the channel. We will create aSeekableByteChannel
object and use it to display the first two names in theusers.txt
file.public static void main(String[] args) throws IOException { int bufferSize = 8; Path path = Paths.get("/home/docs/users.txt"); try (SeekableByteChannel sbc = Files.newByteChannel(path)) { ByteBuffer buffer = ByteBuffer.allocate(bufferSize); sbc.position(4); sbc.read(buffer); for(int i=0; i<5; i++) { System.out.print((char)buffer.get(i)); } System.out.println(); buffer.clear(); sbc.position(0); sbc.read(buffer); for(int i=0; i<4; i++) { System.out.print((char)buffer.get(i)); } System.out.println(); } }
确保
users.txt
文件包含以下内容:Bob Mary Sally Tom Ted
-
执行应用程序。输出应按相反顺序显示前两个名称:
Mary Bob
我们创建了一个 bufferSize
变量来控制通道使用的缓冲区的大小。接下来,为 users.txt
文件创建了一个 Path
对象。此路径用作 newByteChannel
方法的参数,该方法返回一个 SeekableByteChannel
对象。
我们将文件中的读取位置移到了第四个位置。这将我们置于文件中第二个名称的开头。然后使用 read
方法,将大约 8 个字节读入缓冲区。然后显示缓冲区的前五个字节。
我们重复了这个序列,但将位置移到了零,即文件的开头。再次执行 read
操作,然后我们显示前四个字符,这是文件中的第一个名称。
本例使用了文件中名称大小的显式知识。通常,除非通过其他技术获得,否则这些知识是不可用的。我们在这里使用这些知识只是为了演示 SeekableByteChannel
接口的性质。
read
方法将从文件中的当前位置开始读取。它将一直读取,直到缓冲区填满或到达文件结尾。该方法返回一个整数,指示读取了多少字节。到达流的末尾时返回一个 -1
。
读写操作可能正在访问多个线程使用的同一 SeekableByteChannel
对象。因此,当另一个线程关闭通道或以其他方式中断当前线程时,可能会引发 AsynchronousCloseException
或 ClosedByInterruptException
异常。
有一个 size
方法返回流的大小。有一种 truncate
方法可用于丢弃文件中超过特定位置的所有字节。此位置作为长参数传递给方法。
Files
类的静态 newByteChannel
方法可以接受第二个参数,该参数指定打开文件时使用的选项。在使用缓冲 IO 处理文件配方的中有更多的部分使用 BufferedWriter 类写入文件。
此外,我们需要考虑:
- 处理整个文件的内容
- 使用
SeekableByteChannel
接口写入文件 - 查询职位
将以下代码添加到应用程序中。目的是演示如何以顺序方式处理整个文件,并了解各种 SeekableByteChannel
接口方法。
// Read the entire file
System.out.println("Contents of File");
sbc.position(0);
buffer = ByteBuffer.allocate(bufferSize);
String encoding = System.getProperty("file.encoding");
int numberOfBytesRead = sbc.read(buffer);
System.out.println("Number of bytes read: " + numberOfBytesRead);
while (numberOfBytesRead > 0) {
buffer.rewind();
System.out.print("[" + Charset.forName(encoding). decode(buffer) + "]");
buffer.flip();
numberOfBytesRead = sbc.read(buffer);
System.out.println("\nNumber of bytes read: " + numberOfBytesRead);
}
执行应用程序。您的输出应类似于以下内容:
Contents of File
Number of bytes read: 8
[Bob
Mar]
Number of bytes read: 8
[y
Sally]
Number of bytes read: 8
[
Tom
T]
Number of bytes read: 2
[edTom
T]
Number of bytes read: -1
我们首先使用 position
方法将 read
重新定位到文件的开头。通过访问 system
属性 file.encoding
为系统确定了系统的编码字符串。我们跟踪每次读取操作读取的字节数,并显示此计数。
在 while 循环中,我们通过将缓冲区包含在一组括号中来显示缓冲区的内容。这样就更容易看到所读内容。 rewind
方法将缓冲器内的位置设置为 0
。这不能与文件中的位置混淆。
要显示实际的缓冲区,我们需要应用 forName
方法获得 Charset
对象,然后对其使用 decode
方法将缓冲区中的字节转换为 Unicode 字符。然后是 flip
方法,该方法将缓冲区的限制设置为当前位置,然后将缓冲区中的位置设置为 0
。这将为后续读取设置缓冲区。
您可能需要调整 bufferSize
值,以查看应用程序在不同值下的行为。
write
方法获取 java.nio
包的 ByteBuffer
对象并将其写入通道。操作从文件中的当前位置开始。例如,如果文件是使用 append 选项打开的,则第一次写入将在文件末尾。该方法返回写入的字节数。
在下面的示例中,我们将在 users.txt
文件的末尾追加三个名称。我们使用 StandardOpenOption.APPEND
作为 newByteChannel
方法的开放选项。这会将光标移动到文件的末尾,并在该位置开始写入。创建一个 ByteBuffer
,其中三个名称由系统行分隔符属性分隔。使用此属性可以使代码更易于移植。然后执行 write
方法。
final String newLine = System.getProperty("line.separator");
try (SeekableByteChannel sbc = Files.newByteChannel(path, StandardOpenOption.APPEND)) {
String output = newLine + "Paul" + newLine + "Carol" + newLine + "Fred";
ByteBuffer buffer = ByteBuffer.wrap(output.getBytes());
sbc.write(buffer);
}
users.txt
文件的初始内容应为:
Bob
Mary
Sally
Tom
Ted
将代码序列添加到应用程序并执行程序。检查 users.txt
文件的内容。现在应显示如下:
Bob
Mary
Sally
Tom
Ted
Paul
Carol
Fred
重载的 position
方法返回一个长值,指示文件中的位置。这由一个 position
方法补充,该方法接受一个长参数并将位置设置为该值。如果该值超过流的大小,则该位置设置为流的末尾。 size
方法将返回通道使用的文件大小。
为了演示这些方法的使用,我们将重复上一节中的示例。这意味着我们将把文件光标定位到 users.txt
文件的末尾,然后在单独的行中写入三个不同的名称。
在下面的代码序列中,我们使用 size
方法确定文件有多大,然后使用该值作为 position
方法的参数。这会将光标移动到文件的末尾。
接下来,创建三次 ByteBuffer
,每次使用不同的名称写入文件。显示该位置是为了提供信息。
Path path = Paths.get("/home/docs/users.txt");
final String newLine = System.getProperty("line.separator");
try (SeekableByteChannel sbc = Files.newByteChannel(path, StandardOpenOption.WRITE)) {
ByteBuffer buffer;
long position = sbc.size();
sbc.position(position);
System.out.println("Position: " + sbc.position());
buffer = ByteBuffer.wrap((newLine + "Paul").getBytes());
sbc.write(buffer);
System.out.println("Position: " + sbc.position());
buffer = ByteBuffer.wrap((newLine + "Carol").getBytes());
sbc.write(buffer);
System.out.println("Position: " + sbc.position());
buffer = ByteBuffer.wrap((newLine + "Fred").getBytes());
sbc.write(buffer);
System.out.println("Position: " + sbc.position());
}
users.txt
文件的内容最初应包含:
Bob
Mary
Sally
Tom
Ted
将此序列添加到应用程序并执行程序。检查 users.txt
文件的内容。现在应该显示如下:
鲍勃
玛丽
莎莉
汤姆
Ted
保罗
卡罗尔
弗雷德
在本章中
- 使用 SeekableByteChannel配方的随机访问 IO 有更多的部分:该配方向您简要介绍用于打开文件的选项
- 使用文件配方的使用缓冲 IO 的BufferedWriter 类写入文件。
Java7 支持服务器和客户端之间的异步通信。 java.nio.channels
包的 AsynchronousServerSocketChannel
类以线程安全的方式支持流式 IO 的服务器操作。使用充当客户端的 AsynchronousSocketChannel
对象进行通信。我们可以一起使用这些类来构建以异步方式进行通信的服务器/客户机应用程序。
需要同时创建服务器和客户机。要创建服务器,请执行以下操作:
- 使用静态
AsynchronousServerSocketChannel
类的open
方法获取AsynchronousServerSocketChannel
对象的实例 - 将通道绑定到本地地址和端口号
- 使用
accept
方法接受客户端的连接请求 - 在收到消息时对其进行处理
要创建客户端,请执行以下操作:
- 使用静态
open
方法创建AsynchronousSocketChannel
对象 - 为服务器创建一个
InetSocketAddress
对象的实例 - 连接到服务器
- 根据需要发送消息
我们将创建两个应用程序:一个在服务器上,一个在客户机上。它们共同支持一个简单的服务器/客户端应用程序,该应用程序将解释如何使用 AsynchronousSocketChannel
执行异步通信。
-
在服务器上创建一个新的控制台应用程序,并添加以下
main
方法。服务器将只显示发送给它的任何消息。它打开一个服务器套接字并将其绑定到一个地址。然后,它将使用带有CompletionHandler
的accept
方法来处理来自客户端的任何请求。public static void main(String[] args) {throws Exception final AsynchronousServerSocketChannel listener = AsynchronousServerSocketChannel.open(); InetSocketAddress address = new InetSocketAddress("localhost", 5000); listener.bind(address); listener.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() { public void completed(AsynchronousSocketChannel channel, Void attribute) { AsynchronousServerSocketChannel classasynchronous communication, managingtry { System.out.println("Server: completed method executing"); while(true) { ByteBuffer buffer = ByteBuffer.allocate(32); Future<Integer> readFuture = channel.read(buffer); Integer number = readFuture.get(); System.out.println("Server: Message received: " + new String(buffer.array())); } } catch (InterruptedException | ExecutionException ex) { ex.printStackTrace(); } } public void failed(Throwable ex, Void atttribute) { System.out.println("Server: CompletionHandler exception"); ex.printStackTrace(); } }); while(true) { // wait — Prevents the program from // terminating before the client can connect } } catch (IOException ex) { ex.printStackTrace(); } }
-
Next, create a second console application that will act as a client. It will use the
open
method to create anAsynchronousSocketChannel
object and then connect to the server. Ajava.util.concurrent
package'sFuture
object'sget
method is used to block until the connection is complete, and then a message is sent to the server.public static void main(String[] args) {throws Exception try { AsynchronousSocketChannel client = AsynchronousSocketChannel.open(); InetSocketAddress address = new InetSocketAddress("localhost", 5000); Future<Void> future = client.connect(address); System.out.println("Client: Waiting for the connection to complete"); future.get(); AsynchronousServerSocketChannel classasynchronous communication, managingString message; do { System.out.print("Enter a message: "); Scanner scanner = new Scanner(System.in); message = scanner.nextLine(); System.out.println("Client: Sending ..."); ByteBuffer buffer = ByteBuffer.wrap(message.getBytes()); System.out.println("Client: Message sent: " + new String(buffer.array())); client.write(buffer); } while(!"quit".equals(message)) { } }
您将需要执行这两个应用程序。根据您的环境,您可能需要在命令窗口中执行其中一个应用程序,在 IDE 中执行第二个应用程序。如果一次只能运行一个 IDE 实例,就会出现这种情况。
首先执行服务器应用程序。接下来,执行客户机应用程序。它应该提示您输入消息,然后将消息发送到将显示消息的服务器。您的输出应该具有以下常规输出。客户机和服务器输出分别显示在下表中的不同列中:
客户
|
服务器
|
| --- | --- |
| 客户端:正在等待连接完成输入消息:第一条消息客户:发送。。。客户端:已发送消息:第一条消息 | |
| | 服务器:已完成方法的执行服务器:收到消息:第一条消息 |
| 输入消息: Most excellent message
客户:发送。。。客户端:已发送消息:最优秀的消息 | |
| | 服务器:收到的消息:最优秀的消息 |
| 输入消息: quit
客户:发送。。。客户端:已发送消息:退出 | |
| | 服务器:收到消息:退出 java.util.concurrent.ExecutionException:java.io.IOException:指定的网络名称不再可用。... |
- 请注意,当客户端应用程序终止时,服务器中发生了一个
ExecutionException
。通常,我们会在生产应用程序中更优雅地处理此异常。
让我们首先检查服务器应用程序。使用 open
方法创建了一个 AsynchronousServerSocketChannel
对象。然后使用 bind
方法将套接字与系统确定的套接字地址和端口号 5000
相关联。
接下来,调用 accept
方法来接受连接。第一个参数指定了一个用于附件的 null
值。稍后,我们将看到如何使用附件。第二个论点是一个 CompletionHandler
对象。此对象是作为匿名内部类创建的,当客户端发出通信请求时,将调用其方法。
在 completed
方法中,我们显示了一条消息,指示该方法正在执行。然后,我们进入一个无限 while 循环,在该循环中,我们将 32 个字节分配给一个缓冲区,然后尝试从客户机读取数据。 read
方法返回了一个 Future
对象,我们随后使用 get
方法对其进行了处理。这实际上阻止了执行,直到客户端发送消息。然后显示消息。
注意, get
方法返回了一个类型为 Integer
的泛型 Future object
。我们可以用它来确定实际读取了多少字节。这里使用它只是为了阻塞,直到 IO 完成。如果通道通信发生异常, failed
方法将被调用。
try 块末尾的无限 while 循环阻止服务器终止。为了简单起见,这在这里是可以接受的,但通常情况下,我们会以更优雅的方式处理它。
在客户端应用程序中,我们使用 open
方法创建 AsynchronousSocketChannel
对象。创建了与服务器相对应的网络地址,然后与 connect 方法一起使用以连接到服务器。此方法返回了一个 Future
对象。我们使用这个对象和 get
方法来阻塞,直到与服务器建立连接。
注意, connect
方法返回了一个类型为 Void
的 Future
对象。 Void
类位于 java.lang
包中,是 void
的包装类。这里使用它是因为 connect
方法没有有效返回任何内容。
输入了一个 while 循环,当用户输入 quit
时该循环终止。提示用户输入一条消息,然后使用该消息创建一个 ByteBuffer
对象。然后将缓冲区写入服务器。
请注意,在两个应用程序的 catch 块中都使用了多个异常。这是一个新的 Java 7 语言改进,在第 1 章Java 语言改进中的捕获多个异常类型以改进类型检查配方中讨论。
bind
方法重载。两个版本的第一个参数都是一个 SocketAddress
对象,对应于一个本地地址。可以使用一个 null
值,该值将自动分配一个套接字地址。第二个 bind
方法接受第二个参数的整数值。这将配置以依赖于实现的方式允许的最大挂起连接数。小于或等于零的值将使用特定于实现的默认值。
我们应该解决这种通信技术的两个方面:
- 在服务器中使用
Future
对象 - 了解
AsynchronousServerSocketChannel
类选项
AsynchronousServerSocketChannel
类的 accept
方法重载。有一个无参数方法接受连接并为通道返回一个 Future
对象。 Future
对象的 get
方法将为连接返回一个 AsynchronousSocketChannel
对象。这种方法的优点是它返回一个 AsynchronousSocketChannel
对象,可以在其他上下文中使用。
我们可以使用下面的顺序来做同样的事情,而不是使用 accept
方法,即使用 CompletionHandler
。注释掉前面的 accept
方法并添加以下代码:
try {
Future<AsynchronousSocketChannel> future = listener.accept();
AsynchronousSocketChannel worker = future.get();
while (true) {
// Wait
stem.out.println("Server: Receiving ...");
ByteBuffer buffer = ByteBuffer.allocate(32);
Future<Integer> readFuture = worker.read(buffer);
Integer number = readFuture.get();
ystem.out.println("Server: Message received: " + new String(buffer.array()));
}
} catch (IOException | InterruptedException | ExecutionException ex) {
ex.printStackTrace();
}
再次执行应用程序。您应该得到与以前相同的输出。
supportedOptions
方法返回 AsynchronousServerSocketChannel
类使用的一组选项。 getOption
方法将返回选项的值。在上例中使用 bind
方法后添加以下代码:
Set<SocketOption<?>> options = listener.supportedOptions();
for (SocketOption<?> socketOption : options) {
System.out.println(socketOption.toString() + ": " + listener.getOption(socketOption));
}
执行代码。将显示默认值,并应类似于以下内容:
SO_RCVBUF: 8192
SO_REUSEADDR: false
可使用 setOption
方法设置选项。此方法采用选项的名称和值。下面说明如何将接收缓冲区大小设置为 16384 字节:
listener.setOption(StandardSocketOptions.SO_RCVBUF, 16384);
StandardSocketOptions
类定义套接字选项。 AsynchronousServerSocketChannel
频道只支持 SO_REUSEADDR
和 SO_RCVBUF
选项。
- 在本章中,还有更多关于使用 AsynchronousFileChannel 类配方读取文件的章节:该配方解释了带完成处理程序的附件的使用和
AsynchronousChannelGroup
类的使用
java.nio.channels
包的 AsynchronousFileChannel
类允许以异步方式执行文件 IO 操作。当调用 IO 方法时,它将立即返回。实际操作可能在其他时间发生(并且可能使用不同的线程)。在此配方中,我们将探索 AsynchronousFileChannel
类如何执行异步写入操作。读取操作将在使用 AsynchronousFileChannel 类配方的文件读取中演示。
要执行写入操作,请执行以下操作:
- 创建一个表示要从中读取的文件的
Path
对象。 - 使用此路径和
open
方法打开文件通道。 - 使用
write
方法将数据写入文件,可以选择使用完成处理程序。
在本例中,我们将对文件执行一系列写操作。有两种重载的 write
方法。两者都将 java.nio
包的 ByteBuffer
作为初始参数,其中包含要写入的数据,第二个参数指定要写入文件的位置。
两个参数的 write
方法返回 java.util.concurrent
包的 Future<Integer>
对象,该对象也可用于写入文件,如中的部分所示。第二个 write
方法有一个第三个参数,一个附件,和一个第四个参数,一个 CompletionHandler
对象。完成处理程序在写入操作完成时执行。
-
创建一个新的控制台应用程序。使用以下
main
方法。我们打开一个名为asynchronous.txt
的文件进行写入。创建一个完成处理程序,并与write
方法一起使用。执行两个写入操作。显示线程信息以解释操作的异步性质以及完成处理程序的工作方式。public static void main(String[] args) {throws Exception try (AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(Paths.get( "/home/docs/asynchronous.txt"), READ, WRITE, StandardOpenOption.CREATE)) { CompletionHandler<Integer, Object> handler = new CompletionHandler<Integer, Object>() { @Override public void completed(Integer result, Object attachment) { System.out.println("Attachment: " + attachment + " " + result + " bytes written"); System.out.println("CompletionHandler Thread ID: " + Thread.currentThread().getId()); } @Override public void failed(Throwable e, Object attachment) { System.err.println("Attachment: " + attachment + " failed with:"); e.printStackTrace(); } }; AsynchronousFileChannel classfile, writing toSystem.out.println("Main Thread ID: " + Thread.currentThread().getId()); fileChannel.write(ByteBuffer.wrap("Sample".getBytes()), 0, "First Write", handler); fileChannel.write(ByteBuffer.wrap("Box".getBytes()), 0, "Second Write", handler); } }
-
Execute the application. Your application may not behave as you expect. Due to the asynchronous nature of the operations, the order of execution of the various elements may vary from execution to execution. The following is one possible output:
Main Thread ID: 1 Attachment: Second Write 3 bytes written Attachment: First Write 6 bytes written CompletionHandler Thread ID: 13 CompletionHandler Thread ID: 12
重新执行应用程序可能会给出不同的执行顺序。下一节将解释此行为。
我们首先使用 docs
目录中 asynchronous.txt
文件的 Path
对象创建 AsynchronousFileChannel
对象。该文件已打开进行读写操作,如果该文件不存在,则应创建该文件。创建了一个 CompletionHandler
对象。在本例中,这用于确认写入操作的执行。
write
方法执行了两次。第一次将字符串 Sample
写入文件时,从文件中的位置 0
开始。第二次写入操作将字符串 Box
写入文件,也从位置 0
开始。这导致覆盖,文件内容包含字符串 Boxple
。这是有意的,并且说明了 write
方法的 position
参数的使用。
当前线程的 ID 显示在代码中的各个点上。它显示了一个线程用于 main
方法,另外两个线程用于内容处理程序。当 write
方法被执行时,它是以异步方式执行的。 write
方法执行并立即返回。实际写入可能在以后发生。写入操作完成后,成功完成将导致内容处理程序的 completed
方法执行。这将显示其线程的 ID,以及显示附件和写入字节数的消息。如果发生异常,将执行 failed
方法。
从输出中可以看到,使用了一个单独的线程来执行完成处理程序。完成处理程序被定义为返回一个 Integer
值。此值表示写入的字节数。附件可以是所需的任何对象。在本例中,我们使用它来显示哪个 write
方法已经完成。写入操作的异步性质导致内容处理程序的执行顺序不可预测。然而, write
方法确实按照预期的顺序执行。
请注意 try with 资源块的使用。在第 1 章、Java 语言改进中的使用资源块尝试改进异常处理代码配方中探讨了 Java 7 的这一特性。
两个参数的“ write
方法返回一个 Future<Integer>
对象。稍后,在程序中,我们可以使用它的 get
方法,该方法将阻塞直到写入操作完成。注释掉前面示例的写操作,并用以下代码序列替换它们:
Future<Integer> writeFuture1 = fileChannel.write(ByteBuffer.wrap("Sample".getBytes()), 0);
Future<Integer> writeFuture2 = fileChannel.write(ByteBuffer.wrap("Box".getBytes()), 0);
int result = writeFuture1.get();
System.out.println("Sample write completed with " + result + " bytes written");
result = writeFuture2.get();
System.out.println("Box write completed with " + result + " bytes written");
执行应用程序。输出应类似于以下内容:
Main Thread ID: 1
Sample write completed with 6 bytes written
Box write completed with 3 bytes written
write
方法返回一个 Future
对象。 get
方法被阻止,直到写入操作完成。我们使用结果显示一条消息,指示执行了哪个写入操作以及写入了多少字节。
异步文件通道 IO 还有很多方面可以解决。可能感兴趣的其他方面包括:
- 强制写入对频道的更新
- 锁定文件的部分或全部以进行独占访问
- 使用
AsynchronousChannelGroup
管理相关异步操作
- 在本章中,*使用 AsynchronousFileChannel 类读取文件:*这个方法演示了如何执行异步读取,以及
AsynchronousChannelGroup
类的使用。
异步读取操作也可以使用两种重载 read
方法中的任何一种。我们将演示如何使用 java.nio.channels
包的 AsynchronousChannelGroup
对象实现这一点。这将为我们提供一种观察这些方法的方法,并提供一个 AsynchronousChannelGroup
的示例。
要执行写入操作,请执行以下操作:
- 创建一个表示要从中读取的文件的
Path
对象。 - 使用此路径和
open
方法打开文件通道。 - 使用
read
方法从文件中读取数据。
-
创建一个新的控制台应用程序。在
main
方法中,创建一个大小为 3 的java.util.concurrent
包的ScheduledThreadPoolExecutor
对象的实例。我们将使用ScheduledThreadPoolExecutor
类,主要是因为它易于创建。三个大小将有助于说明如何管理线程。ExecutorService pool = new ScheduledThreadPoolExecutor(3);
-
接下来,添加一个 try-with-resource 块,并为文件
items.txt
创建一个AsynchronousFileChannel
对象。使用StandardOpenOption.READ
的open
选项和先前创建的池对象。try (AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open( Paths.get("/home/docs/items.txt"), EnumSet.of(StandardOpenOption.READ), pool)) {
-
接下来,显示主线程 ID,然后创建一个
CompletionHandler
对象,我们将使用它来显示异步读取操作的结果。System.out.println("Main Thread ID: " + Thread.currentThread().getId()); CompletionHandler<Integer, ByteBuffer> handler = new CompletionHandler<Integer, ByteBuffer>() { @Override public synchronized void completed(Integer result, ByteBuffer attachment) { for (int i = 0; i < attachment.limit(); i++) { System.out.print((char) attachment.get(i)); } System.out.println(""); System.out.println("CompletionHandler Thread ID: " + Thread.currentThread().getId()); System.out.println(""); } @Override public void failed(Throwable e, ByteBuffer attachment) { System.out.println("Failed"); } };
-
接下来,添加代码来创建一个
ByteBuffer
对象数组。为每个缓冲区分配10
字节,然后使用缓冲区作为read
方法的第一个参数和附件。使用它作为附件,允许我们在完成处理程序中访问读取操作的结果。开始读取位置在第二个参数中指定,并设置为每读取文件的 10 字节段。final int bufferCount = 5; ByteBuffer buffers[] = new ByteBuffer[bufferCount]; for (int i = 0; i < bufferCount; i++) { buffers[i] = ByteBuffer.allocate(10); fileChannel.read(buffers[i], i * 10, buffers[i], handler); }
-
添加对
awaitTermination
方法的调用,以允许读取操作完成。然后再次显示缓冲区。pool.awaitTermination(1, TimeUnit.SECONDS); System.out.println("Byte Buffers"); for (ByteBuffer byteBuffer : buffers) { for (int i = 0; i < byteBuffer.limit(); i++) { System.out.print((char) byteBuffer.get(i)); } System.out.println(); }
-
使用以下内容作为
items.txt
文件的内容,其中每个条目是由一个项目和一个数量组成的 10 字节序列:Nail 34Bolt 12Drill 22Hammer 24Auger 24
-
Execute the application. Your output should be similar to the following:
Main Thread ID: 1 Nail 34 CompletionHandler Thread ID: 10 Drill 22 CompletionHandler Thread ID: 12 Bolt 12 CompletionHandler Thread ID: 11 Auger 24 CompletionHandler Thread ID: 12 Hammer 24 CompletionHandler Thread ID: 10 Byte Buffers Nail 34 Bolt 12 Drill 22 Hammer 24 Auger 24
请注意,完成处理程序线程使用了三个 ID。这些对应于作为线程池一部分创建的三个线程。
创建了一个带有大小为 3 的线程池的 java.util.concurrent
包 ExecutorService
,以演示线程组的使用并强制重用线程。 items.txt
文件包含长度相等的数据。这简化了示例。
在内容处理程序中,成功完成后,执行 completed
方法。附件中包含缓冲区 read
,该缓冲区随后与内容处理程序的线程 ID 一起显示。请注意 completed
方法使用了 synchronized
关键字。虽然该方法并不总是必需的,但这里使用了它,以便输出更具可读性。删除关键字将导致缓冲区输出的交错,使其无法读取。
请注意完成处理程序线程的非确定性行为。它们没有按照相应的 read
方法执行的顺序执行。重复执行应用程序会产生不同的输出。
知道输入文件只包含五项,我们创建了五个 ByteBuffer
对象,每个对象的大小为 10
。 read
方法使用不同的缓冲区执行了五次。
执行了 awaitTermination
方法,有效地暂停了应用程序一秒钟。这允许完成处理程序的线程完成。然后再次显示缓冲区以验证操作。
无论何时创建异步通道,都会将其分配给通道组。通过定义自己的组,可以更好地控制组中使用的线程。当使用 open
方法创建频道时,它属于全局频道组。
异步通道组提供完成绑定到组的异步 IO 操作所需的技术。每个组都有一个线程池。这些线程用于 IO 操作和 CompletionHandler
对象。
在前面的示例中,我们使用 open
方法将线程池与异步操作关联起来。还可以使用以下静态 AsynchronousChannelGroup
方法之一创建异步通道组:
withFixedThreadPool:
使用ThreadFactory
创建新线程的固定大小池。池的大小由其第一个参数指定。withCachedThreadPool:
此池使用ExecutorService
创建新线程。第二个参数指定池的建议初始线程数。withThreadPool:
这也使用了ExecutorService
,但没有指定初始尺寸。
异步通道组提供了有序关闭组的能力。一旦启动停机:
- 它试图创建一个新的频道,结果导致
ShutdownChannelGroupException
- 运行完成处理程序的线程不会中断
组在以下情况下终止:
- 所有频道都关闭了
- 所有完成处理程序都已运行到完成
- 所有组资源都已释放
其他感兴趣的方法包括:
isShutdown
方法,用于确定组是否关闭。isTerminated
方法,用于确定组是否已终止。shutdownNow
方法,强制关闭一个组。所有频道都已关闭,但内容处理程序不会中断。
在本章中:
- *使用 AsynchronousFileChannel 类写入文件:*此配方演示如何执行异步写入
java.nio.file
包的 SecureDirectoryStream
类设计用于依赖比其他 IO 类更严格安全性的应用程序。它支持目录上的无竞争(顺序一致)操作,其中的操作与其他应用程序同时执行。
此类需要操作系统的支持。通过将 Files
类的 newDirectoryStream
方法的返回值强制转换为 SecureDirectoryStream
对象,获得该类的一个实例。如果强制转换失败,则底层操作系统不支持这种类型的流。
获取并使用 SecureDirectoryStream
对象:
- 创建一个表示感兴趣目录的
Path
对象。 - 使用
Files
类的newDirectoryStream
方法,将结果强制转换为SecureDirectoryStream
。 - 使用此对象影响
SecureDirectoryStream
操作。
-
创建一个新的控制台应用程序。在
main
方法中,添加以下代码。我们将为docs
目录创建一个Path
对象,然后为其获取一个SecureDirectoryStream
对象。这将用于查看目录的 POSIX 权限。public static void main(String args[]) throws IOException { Path path = Paths.get("home/docs"); SecureDirectoryStream<Path> sds = (SecureDirectoryStream) Files.newDirectoryStream(path); PosixFileAttributeView view = sds.getFileAttributeView(PosixFileAttributeView.class); PosixFileAttributes attributes = view.readAttributes(); Set<PosixFilePermission> permissions = attributes.permissions(); for (PosixFilePermission permission : permissions) { System.out.print(permission.toString() + ' '); } System.out.println(); }
-
在支持
SecureDirectoryStream
类的系统上执行应用程序。通过在 Ubuntu 系统上运行应用程序获得以下输出:GROUP_EXECUTE OWNER_WRITE OWNER_READ OTHERS_EXECUTE GROUP_READ OWNER_EXECUTE OTHERS_READ
获取了 docs
目录的 Path
对象,并将其用作 Files
类的 newDirectoryStream
方法的参数。该方法的结果被转换为 SecureDirectoryStream
类。然后执行 getFileAttributeView
方法以获取视图,该视图用于显示目录的 POSIX 文件权限。在第 3 章获取文件和目录信息中的使用 PosixFileAttributeView 维护 POSIX 文件属性配方中讨论了 PosixFileAttributeView
类的使用。
SecureDirectoryStream 类支持的其他方法包括删除文件或目录的功能、将文件移动到其他目录的移动方法,以及创建访问文件的 SeekableByteChannel
。