Java7-NIO2-高级教程-四-

45 阅读56分钟

Java7 NIO2 高级教程(四)

协议:CC BY-NC-SA 4.0

九、异步通道 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/Ja…](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:050 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:6050 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,包括异步通道组的概念。本章最后给出了一些关于开发基于异步的应用的技巧。