Java NIO 通道

一、通道基础

通道(Channel)是 java.nio 的第二个主要创新。他们既不是一个扩展也不是一个增强,而是全新、极好的Java IO 示例,提供与 IO 服务的直接连接。Channel 用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地数据传输。

通道可以形象地比喻为银行出纳窗口使用的气动导管。你的薪水支票就是你要传送的信息,载体(Carrier)就好比一个缓冲区。你先填充缓冲区(将你的支票放到载体上),接着将缓冲写到通道中(将载体丢进导管中),然后信息负载就被传递到通道另一侧 IO 服务。

通道是一种途径,借助该途径,可以用最小的总开销来访问操作系统本身的 IO 服务。缓冲区则是通道内部用来发送和接收数据的端点。

与缓冲区不同,通道 API 主要由接口指定。不同的操作系统上实现(ChannelImplementation)会有根本性的差异,所以通道 API 仅仅描述了可以做什么。因此很自然的,通道实现经常使用操作系统本地代码。通道接口允许你以一种受控且可移植的方式来访问底层 IO 服务。

你可以从顶层的 Channel 接口看到,对所有通道来说只有两种共同的操作:检查一个通道是否打开(IsOpen())和关闭一个打开的通道(close())。所有有趣的东西都是那些实现Channel接口以及它的子接口的类。

通道是访问 IO 服务的导管。IO 可以分为广义的两大类别:File IO 和 Stream IO。那么相应的有两种类型的通道也就不足为怪。他们是文件(file)通道和套接字(socket)通道。你会发现有一个 FileChannel 类和三个 socket 通道类:SocketChannel、ServerSocketChannel 和 DatagramChannel。

通道可以以多种方式创建。Socket 通道有可以直接创建新 socket 通道的工厂方法。但是一个 FileChannel 对象却只能通过一个打开的 RandomAccessFile、FileInputStream 或 FileOutputStream 对象上调用 getChannel() 方法来获取。你不能直接创建一个 FileChannel 对象。

使用通道

通道是可以单向(undirectional)或者双向的(bidirectional)。一个 channel 类可能实现定义 read() 方法的 ReadableByteChannel 接口,而另一个 channel 类也去实现 WritableByteChannel 接口已提供 write() 方法。实现这两种接口其中之一的类都是单向的,只能在一个方向上传输数据。如果一个类同时实现这两个接口,那么它是双向的,可以双向传输数据。

通道会连接一个特定的 IO 服务且通道实例(channel instance)的性能受它所连接的 IO 服务的特征限制,记住这很重要。一个连接到只读文件的 Channel 实例不能进行写操作,即使该实例所属的类可能有 write() 方法。基于此,程序员需要知道通道是如何打开的,避免试图尝试一个底层 IO 服务不允许的操作。

通道可以以阻塞(blocking)或非阻塞(nonblocking)模式运行。非阻塞模式的通道永远不会让调用的线程休眠。请求的操作要么立即完成,要么返回一个结果表明未进行任何操作。只有面向流(stream-oriented)的通道,如 sockets 和 pipes 才能使用非阻塞模式。

关闭通道

通过调用通道的 close 方法进行关闭,但是可能会导致关闭底层IO服务时发生阻塞(非阻塞模式和阻塞模式都一样)。通过 isopen 方法来测试通道的开放状态,如果返回 true,那么说明通道可以使用。反之,说明通道已经关闭,不能使用。

二、FileChannel 类

文件通道总是阻塞式的,因此不能被置于非阻塞模式。现代操作系统都有复杂的缓存和预取机制,使得本地磁盘 IO 操作延迟很少。网络文件系统一般而言延迟会多些,不过却也因该优化而受益。面向流的 IO 的非阻塞范例对于面向文件的操作并无多大意义。这是由文件 IO 本质上的不同性质造成的。对于文件 IO,最强大之处在于异步 IO(asynchronous IO),它允许一个进程可以从操作系统请求一个或多个 IO 操作而不必等待这些操作的完成。发起请求的进程之后会收到它请求的 IO 操作已完成的通知。异步 IO 是一种高级性能,当前的很多操作系统都还不具备。

文件通道创建

FileChannel 对象不能直接创建。一个FileChannel 实例只能通过一个打开的 file 对象(RandomAccessFile、FileInputStream 或 FileOutputStream)上调用 getChannel() 方法来获取。调用 getChannel() 方法会返回一个连接到相同文件的 FileChannel 对象且该 FileChannel 对象具有与 file 对象相同的访问权限,然后你就可以使用该通道对象来利用强大的 FileChannel API 了。

FileChannel 对象是线程安全(thread-safe)的。多个进程可以在同一个实例上并发调用方法而不会引起任何问题,不过并非所有操作都是多线程的(multithreaded)。影响通道位置或者影响文件大小的操作都是单线程的(single-threaded)。如果有一个线程已经执行会影响通道位置或者文件大小的操作,那么其他尝试进行此类操作之一的线程必须等待。并发行为也会受到底层操作系统或文件系统影响。

同大多数 IO 相关的类一样,FileChannel 是一个反应 Java 虚拟机外部一个具体对象的抽象。FileChannel 类保证同一个 Java 虚拟机上所有实例看到的某个文件的视图均是一致的,但是 Java 虚拟机却不能对超出它控制范围的因素提供担保。通过一个 FileChannel 实例看到的某个文件的视图同通过一个外部的非 Java 进程看到的该文件的视图可能一致,也可能不一致。多个进程发起的并发文件的语义高度取决于底层的操作系统和(或)文件系统。一般而言,由运行在不同 Java 虚拟机上的 FileChannel对象发起的对某个文件的并发访问和由非 Java 进程发起的对该文件的并发访问是一致的。

访问文件

在通道这块我们可以使用 FileChannel 的 read 和 write 方法进行文件的访问,以及配合 position() 进行文件操作。

FileChannel 位置(position)是从底层的文件描述符获得的,该 position 同时被作为通道引用获取来源的文件对象共享。这也就意味着一个对象对该 position 的更新可以被另一个对象看到。

position 能够决定文件中哪一处的数据接下来将被读或者写。类似于缓冲区的 get() 和 put() 方法,当字节被 read() 或 write() 方法传输时,文件 position 会自动更新。如果 position 值达到了文件大小的值(文件大小的值可以通过 size() 方法返回),read() 方法返回一个文件尾条件值(-1)。可是,不同于缓冲区的是,如果实现 write() 方法时, position 前进到超过文件大小的值,该文件会扩展以容纳新写入的字节。

三、Socket 通道

新的 socket 通道类可以运行非阻塞模式并且是可选择的。这两个性能可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。本节中我们会看到,再也没有为每个 socket 连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换总开销。借助新的 NIO 类,一个或几个线程就可以管理成百上千的活动 socket 连接了,并且只有很少甚至可能没有性能损失。

全部 socket 通道类(DatagramChannel、SocketChannel 和 ServerSocketChannel)在被实例化时都会创建一个对等 socket 对象。这些是我们所熟悉的来自 java.net 的类(Socket、ServerSocket 和 DatagramSocket),它们已经被更新已识别通道。对等 socket 可以通过调用 socket() 方法从一个通道上获取。此外,这三个 java.net 类现在都有 getChannel() 方法。

虽然每个 socket 通道(在 java.nio.channels 包中)都有一个关联的 java.net socket 对象,却并非所有的 socket 都有一个关联的通道。如果用传统方式(直接实例化)创建一个 socket 对象,它就不会有关联的 SocketChannel 并且它的 getChannel() 方法将总是返回 null。

非阻塞模式

Socket 通道可以在非阻塞模式下运行。这个说法虽然简单却有着深远的含义。传统 Java socket 的阻塞性质性质曾经是 Java 程序可伸缩性的最重要制约之一。非阻塞 IO 是许多复杂的、高性能的程序构建的基础。

设置或重新设置一个通道的阻塞模式是很简单的,只要调用 configureBlocking() 方法即可,传递参数值为 true 则设为阻塞模式,参数值为 false 值设为非阻塞模式。可以通过调用 isBlocking() 方法来判断某个 socket 通道当前处于哪种模式。

服务器端的使用经常会考虑到非阻塞 socket 通道,因为它们使管理很多 socket 通道变得更容易。但是,在客户端使用一个或几个非阻塞模式的 socket 通道也是有益处的,例如,在客户端使用一个或几个非阻塞模式的 socket 通道也是有益处的,例如,借助非阻塞 socket 通道,GUI 程序可以传与用户请求并且同时维护与一个或多个服务器的会话。在很多程序上,非阻塞模式都是有用的。

ServerSocketChannel

它是一个基于通道的 socket 监听器。它同我们所熟悉的 java.net.ServerSocket 执行相同的基本任务,不过它增加了通道语义,因此能够在非阻塞模式下运用静态的 open() 工厂方法创建一个新的 ServerSocketChannel 对象,将会返回同一个未绑定的 java.net.ServerSocket 关联的通道。该对等 ServerSocket 可以通过在返回的 ServerSocketChannel 上调用 socket() 方法来获取。作为 ServerSocketChannel 的对等体被创建的 ServerSocket 对象依赖通道实现。这些 socket 关联的 SocketImpl 能识别通道。

通道不能被封装在随意的 socket 对象外面。由于ServerSocketChannel 没有 bind() 方法,因此有必要取出对等的 socket 并使用它来绑定到一个端口以开始监听连接。我们也是使用对等 ServerSocket 的 API 来根据需要设置其他的 socket 选项。

和 java.net.ServerSocket 一样,ServerSocektChannel 也有 socket() 方法。一旦创建了一个 ServerSocketChannel 并用对等 socket 帮i当了它,就可以在其中一个上调用 accept()。如果选择在 ServerSocekt 上调用 accept() 方法,那么它会同任何其他的 ServerSocket 表现一样的行为:总是阻塞并返回一个 java.net.Socket 对象。如果选择在 ServerSocketChannel 上调用 accept() 方法则会返回 SocketChannel 类型的对象,返回的对象能够在非阻塞模式下运行。假设系统已经有一个安全管理器(security manager),两种形式的方法调用都执行相同的安全检查。

如果以非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept() 会立即返回 null。正是这种检查连接而不阻塞的能力实现了可伸缩性并降低了复杂性。可选择性也因此得到实现。我们可以使用一个选择器实例来注册一个 ServerSocektChannel 对象以实现新连接到达时自动通知的功能。

SocketChannel

Socket 和 SocketChannel 类封装点对点、有序的网络连接,类似于我们所熟知并喜爱的 TCP/IP 连接。SocketChannel 扮演客户端发起同一个监听服务器的连接。直到连接成功,它才能收到数据并且只会从连接到的地址接收。

每个 SocketChannel 对象创建时都是同一个对等的 java.net.Socket 对象串联的。静态的 open() 方法可创建一个新的 SocketChannel 对象,而在新创建的 SocketChannel 上调用 socket() 方法能返回它对等的 Socket 对象;在该 Socket 上调用 getChannel() 方法则能返回最初的哪个 SocketChannel。

新创建的 SocketChannel 虽已打开却是未连接的。在一个未连接的 SocketChannel 对象上尝试一个 I/O 操作会导致 NotYetConnectedException 异常。我们可以通过在通道上直接调用 connect() 方法或在通道关联的 Socket 对象上调用 connect() 来将该 socket 通道连接。一旦一个 socket 通道被连接,它将保持连接状态直到被关闭。可以通过调用布尔型的 isConnected() 方法来测试某个 SocketChannel 当前是否已连接。

如果选择使用传统方法进行连接——通过在对等 Socket 对象上调用 connect() 方法,那么传统的连接语义将适用于此。线程在连接建立好或超时过期之前都将保持阻塞。如果选择通过在通道上直接使用 connect() 方法来建立连接并且通道处于阻塞模式(默认模式),那么连接过程实际上是一样的。

在 SocketChannel 上并没有一种 connect() 方法可以让你指定超时(timeout)值,当 connect() 方法在非阻塞模式下被调用时, SocketChannel 提供并发连接:它发起对请求地址的连接并且立即返回值。如果返回值是 true,说明连接立即建立了(这可能是本地环回连接);如果连接不能立即建立,connect() 方法会返回 false 且并发地继续连接建立过程。

面向流的 socket 建立连接状态需要一定的时间,因为两个待连接系统之间必须进行包对话以建立维护流 socket 所需的状态信息。跨越开放互联网连接到远程系统会特别耗时。假如某个 SocketChannel 上当前正有一个并发连接,isConnectPeding() 方法就会返回 true 值。

调用 finishConnect() 方法来完成连接过程,该方法任何时候都可以安全地进行调用。假如在一个非阻塞模式的 SocketChannel 对象上调用 finishConnect() 方法,将可能出现下列情形之一:
1. connect() 方法尚未被调用。那么将产生 NoConnectionPendingException 异常。
2. 连接建立过程正在进行,尚未完成。那么什么都不会发生,finishConnect() 方法会立即返回 false 值。
3. 在非阻塞模式下调用 connect() 方法之后,SocketChannel 又被切换回阻塞模式。那么如果有必要的话,调用线程会阻塞直到连接建立完 finishConnect() 方法接着就会返回 true 值。
4. 在初次调用 connect() 或最后一次调用 finishConnect() 之后,连接建立过程已经完成。那么 SocketChannel 对象的内部状态将被更新到已连接状态,finishConnect() 方法会返回 true 值,然后SocketChannel 对象就可以被用来传输数据了。
5. 连接已经建立,那么什么都不会发生,finishConnect() 方法会返回 true 值。

你可能感兴趣的:(NIO,java,nio)