通道用于在字节缓冲区和通道另外一侧的实体(文件或Socket)进行数据的有效传输。下面就是Java NIO中最重要的通道的实现:
其主要的类的框架结构如下:
首先看一下所有通道的父接口Channel定义如下:
public interface Channel extends Closeable {
public boolean isOpen(); // 判断通道是否已经打开
public void close() throws IOException; // 关闭通道
}
1、读出与写入
WritableByteChannel接口:
public interface WritableByteChannel extends Channel{
public int write(ByteBuffer src) throws IOException; // 将ByteBuffer中的字节数据写入通道中
}
ReadableByteChannel接口:
public interface ReadableByteChannel extends Channel {
public int read(ByteBuffer dst) throws IOException; // 将通道中的字节数据读取到ByteBuffer中
}
WritableByteChannel接口只定义了写操作,而ReadableByteChannel接口只定义了读操作,为什么不将写和读的操作定义到一起呢?因为有的通道可能只支持单向的写入或读取,例如ScatteringByteChannel和GatherByteChanel。
如果一个通道支持双向操作,则直接继承ByteChannel即可,如下:
public interface ByteChannel extends ReadableByteChannel, WritableByteChannel{}
仅起到了一个聚合的作用。
2、分散与聚合
Java NIO开始支持scatter/gather用于描述从Channel中读取或者写入到Channel的操作。
(1)分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。
接口的定义如下:
public interface ScatteringByteChannel extends ReadableByteChannel{
public long read(ByteBuffer[] dsts, int offset, int length) throws IOException;
public long read(ByteBuffer[] dsts) throws IOException;
}
(2)聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。
接口的定义如下:
public interface GatheringByteChannel extends WritableByteChannel{
public long write(ByteBuffer[] srcs, int offset, int length) throws IOException;
public long write(ByteBuffer[] srcs) throws IOException;
}
1、Socket和ServerSocket
socket 通道有三个类:SocketChannel、ServerSocketChannel 和 DatagramChannel。请注意:DatagramChannel 和 SocketChannel 实现定义读和写功能的接口,而 ServerSocketChannel 不实现。ServerSocketChannel 负责监听传入的连接和创建新的 SocketChannel对象,它本身从不传输数据。
全部 socket 通道类在被实例化时都会创建一个对等 socket 对象,与 java.net 的类(Socket、ServerSocket和DatagramSocket)对应,它们已经被更新以识别通道。对等的 socket 对象可以通过调用通道类的 socket() 方法从通道上获取。此外,这三个 java.net 类现在都有 getChannel()方法。
虽然每个 socket 通道都有一个关联的 java.net socket 对象,却并非所有的 socket 都有一个关联的通道。如果你用传统方式(直接实例化)创建了一个 Socket 对象,它就不会有关联的 SocketChannel 并且它的 getChannel() 方法将总是返回 null。
Socket 通道可以在非阻塞模式下运行。只有面向流的如Socket和Pipes才可以置于非阻塞模式,我们可以依靠所有 socket 通道类的公有超级类:SelectableChannel,如下:
public abstract class SelectableChannel extends AbstractChannel implements Channel {
// This is a partial API listing
public abstract void configureBlocking (boolean block) throws IOException; // 设置通道的阻塞模式
public abstract boolean isBlocking();
public abstract Object blockingLock();
}
调用 configureBlocking() 方法来设置阻塞模式,传递参数值为 true 则设为阻塞模式, false 值设为非阻塞模式。调用isBlocking() 方法来判断某个 socket 通道当前处于哪种模式:
SocketChannel sc = SocketChannel.open();
sc.configureBlocking (false); // 非阻塞模式
...
if ( ! sc.isBlocking( )) {
doSomething (cs);
}
偶尔地,我们也会需要防止 socket 通道的阻塞模式被更改。API 中有一个 blockingLock()方法,该方法会返回一个非透明的对象引用。返回的对象是通道实现修改阻塞模式时内部使用的。只有拥有此对象的锁的线程才能更改通道的阻塞模式。使用 blockingLock() 的示例:
Socket socket = null;
Object lockObj = serverChannel.blockingLock();
// have a handle to the lock object, but haven't locked it yet may block here until lock is acquired
synchronize (lockObj) {
// This thread now owns the lock; mode can't be changed
boolean prevState = serverChannel.isBlocking();
serverChannel.configureBlocking(false);
socket = serverChannel.accept();
serverChannel.configureBlocking(prevState);
}
// lock is now released, mode is allowed to change
if (socket != null) {
doSomethingWithTheSocket(socket);
}
以下是 ServerSocketChannel 的完整API:
public abstract class ServerSocketChannel extends AbstractSelectableChannel {
public static ServerSocketChannel open() throws IOException
public abstract ServerSocket socket();
public abstract ServerSocket accept() throws IOException;
public final int validOps();
}
ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉的 java.net.ServerSocket 执行相同的基本任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。
ServerSocketChannel ssc = ServerSocketChannel.open();
// 取到 ServerSocketChannel 对等的 serverSocket 对象
ServerSocket serverSocket = ssc.socket();
// Listen on port 1234
serverSocket.bind (new InetSocketAddress (1234));
ServerSocketChannel 和 serverSocket 均有 accept() 方法,你可以在其中一个上调用 accept()。如果你选择在 ServerSocket 上调用 accept() 方法,那么它会同任何其他的 ServerSocket 表现一样的行为:总是阻塞并返回一个 java.net.Socket 对象。如果你选择在 ServerSocketChannel 上调用 accept() 方法则会返回 SocketChannel 类型的对象,返回的对象能够在非阻塞模式下运行。
下面开始学习 SocketChannel,它是使用最多的 socket 通道类:
public abstract class SocketChannel extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel {
// This is a partial API listing
public static SocketChannel open() throws IOException
public static SocketChannel open (InetSocketAddress remote) throws IOException
public abstract Socket socket();
public abstract boolean connect (SocketAddress remote) throws IOException;
public abstract boolean isConnectionPending();
public abstract boolean finishConnect() throws IOException;
public abstract boolean isConnected();
public final int validOps();
}
每个 SocketChannel 对象创建时都是同一个对等的 java.net.Socket 对象串联的。静态的 open() 方法可以创建一个新的 SocketChannel 对象,而在新创建的 SocketChannel 上调用 socket() 方法能返回它对等的 Socket 对象;在该 Socket 上调用 getChannel() 方法则能返回最初的那个 SocketChannel。虽然每个 SocketChannel 对象都会创建一个对等的 Socket 对象,反过来却不成立。直接创建的 Socket 对象不会关联 SocketChannel 对象,它们的 getChannel() 方法只返回 null。
新创建的 SocketChannel 虽已打开却是未连接的。在一个未连接的 SocketChannel 对象上尝试一个 I/O 操作会导致 NotYetConnectedException 异常。我们可以通过在通道上直接调用 connect() 方法或在通道关联的 Socket 对象上调用 connect() 来将该 socket 通道连接。一旦一个 socket 通道被连接,它将保持连接状态直到被关闭。你可以通过调用布尔型的 isConnected() 方法来测试某个 SocketChannel 当前是否已连接。创建 SocketChannel 时创建连接:
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress ("somehost", somePort));
等价于:
SocketChannel socketChannel = SocketChannel.open();socketChannel.connect(new InetSocketAddress ("somehost", somePort));
如果你选择使用传统方式进行连接 -- 通过在对等 Socket 对象上调用 connect()方法,那么传统的连接语义将适用于此:线程在连接建立好或超时过期之前都将保持阻塞。如果你选择通过在通道上直接调用 connect() 方法来建立连接并且通道处于阻塞模式(默认模式),那么连接过程实际上是一样的。 在 SocketChannel 上并没有一种 connect() 方法可以让你指定超时(timeout)值,当 connect() 方法在非阻塞模式下被调用时 SocketChannel 提供并发连接:它发起对请求地址的连接并且立即返回值。如果返回值是 true,说明连接立即建立了(这可能是本地环回连接);如果连接不能立即建立,connect() 方法会返回 false 且并发地继续连接建立过程。
面向流的的 socket 建立连接状态需要一定的时间,因为两个待连接系统之间必须进行包对话以建立维护流 socket 所需的状态信息。跨越开放互联网连接到远程系统会特别耗时。假如某个 SocketChannel 上当前正有一个并发连接,isConnectPending() 方法就会返回 true 值。
调用 finishConnect() 方法来完成连接过程,该方法任何时候都可以安全地进行调用。假如在一个非阻塞模式的 SocketChannel 对象上调用 finishConnect() 方法,将可能出现下列情形之一: connect( )方法尚未被调用。那么将产生 NoConnectionPendingException 异常。 连接建立过程正在进行,尚未完成。那么什么都不会发生,finishConnect() 方法会立即返回 false 值。 在非阻塞模式下调用 connect() 方法之后,SocketChannel 又被切换回了阻塞模式。那么如果有必要的话,调用线程会阻塞直到连接建立完成,finishConnect() 方法接着就会返回 true 值。 在初次调用 connect() 或最后一次调用 finishConnect() 之后,连接建立过程已经完成。那么 SocketChannel 对象的内部状态将被更新到已连接状态,finishConnect() 方法会返回 true 值,然后 SocketChannel 对象就可以被用来传输数据了。 连接已经建立。那么什么都不会发生,finishConnect() 方法会返回 true 值。 当通道处于中间的连接等待(connection-pending)状态时,你只可以调用 finishConnect()、isConnectPending() 或 isConnected()方法。一旦连接建立过程成功完成,isConnected()将返回 true 值。finishConnect() 示例:
InetSocketAddress addr = new InetSocketAddress (host, port);
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(addr);
while ( ! sc.finishConnect( )) {
doSomethingElse();
}
doSomethingWithChannel(sc);
sc.close();
上面的代码中,我们通过 while 轮询并在连接进行过程中判断通道所处的状态,后续我们将了解到如何使用选择器来避免进行轮询并在异步连接建立之后收到通知。如果尝试异步连接失败,那么下次调用 finishConnect() 方法会产生一个适当的经检查的异常以指出问题的性质。通道然后就会被关闭并将不能被连接或再次使用。
Socket 通道是线程安全的。并发访问时无需特别措施来保护发起访问的多个线程,不过任何时候都只有一个读操作和一个写操作在进行中。请记住,sockets 是面向流的而非包导向的。它们可以保证发送的字节会按照顺序到达但无法承诺维持字节分组。某个发送器可能给一个 socket 写入了 20 个字节而接收器调用 read() 方法时却只收到了其中的 3 个字节,剩下的 17 个字节仍然在传输中。由于这个原因,让多个不配合的线程共享某个流 socket 的同一侧绝非一个好的设计选择。
connect() 和 finishConnect() 方法是互相同步的,并且只要其中一个操作正在进行,任何读或写的方法调用都会阻塞,即使是在非阻塞模式下。如果此情形下你有疑问或不能承受一个读或写操作在某个通道上阻塞,请用 isConnected() 方法测试一下连接状态。
2、FileChannel
Java NIO中的FileChannel是一个连接到文件的通道。可以通过文件通道读写文件。FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下。FileChannel类的寻址能力意味着开发人员可以更加灵活地处理文件内容。
打开FileChannel通道,可以通过工厂方法来获取,如下:
public static FileChannel open(Path path,Set extends OpenOption> options,FileAttribute>... attrs) throws IOException {
FileSystemProvider provider = path.getFileSystem().provider();
return provider.newFileChannel(path, options, attrs);
}
或者使用RandomAccessFile类的getChannel()方法来获取。
由于Java在处理大数据量时如果使用FileReader,会把所有的内容加载到内存中,所以一般使用java.io.RandomAccessFile来做,读取部分信息。在使用NIO中使用java.nio.channels.FileChannel来处理,举个例子如下:
Path logFile=Paths.get("C:\\temp.txt");
ByteBuffer buffer=ByteBuffer.allocate(1024);
FileChannel channel=FileChannel.open(logFile, StandardOpenOption.READ);
channel.read(buffer,channel.size()-100);// 读取日志文件最后的100个字符
buffer.flip(); // 由写模式变换为读模式
while (buffer.hasRemaining()) {
System.out.print((char)buffer.get());
}
channel.close(); // 关闭通道
上述程序实现了输出日志文件最后的100个字符的功能。
NIO采用内存映射文件的方式来处理输入/输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样访问文件了。Channel提供了一个map()方法,可以直接将“一块数据”映射到内存中。传统的输入/输出系统是面向流的处理,而NIO是面向块的处理。
File f = new File("C:\\FileChannelTest.txt");
try ( FileChannel inChannel = new FileInputStream(f).getChannel();
FileChannel outChannel = new FileOutputStream("C:\\a.txt").getChannel()) {
// 将FileChannel里的全部数据映射成ByteBuffer
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
outChannel.write(buffer); // 直接将buffer里的数据全部输出
buffer.clear(); // 再次调用buffer的clear()方法,复原limit、position的位置
Charset charset = Charset.forName("GBK"); // 使用GBK的字符集来创建解码器
CharsetDecoder decoder = charset.newDecoder();// 创建解码器对象
// 使用解码器将ByteBuffer转换成CharBuffer
CharBuffer charBuffer = decoder.decode(buffer);
// CharBuffer的toString方法可以获取对应的字符串
System.out.println(charBuffer);
} catch (IOException ex) {
ex.printStackTrace();
}
查看a.txt文件后发现其中的内容与FileChannelTest.txt内容一样。