JavaNIO中的Channel概述

前面一些Java的文章已经简要对Java NIO做了描述,也详细地介绍了NIO中最基本的Buffer。这篇文章将继续介绍NIO的第二个主要角色 —— Channel。Channel是对数据的源头和数据目标点流经途径的抽象,在这个意义上和InputStream和OutputStream类似。Channel可以译为“通道、管道”,而传输中的数据仿佛就像是在其中流淌的水。前面也提到了Buffer,Buffer和Channel相互配合使用,才是Java的NIO。

0. Channel简述

“A nexus for I/O operations.”这是Java API中对Channel的描述,说的是Channel是IO操作的连接点。

开门见山,既然Channel是Java NIO中这么重要的一个角色,那么我们来看一下Java API中给出的类结构图。

Channel类层次结构

Channel类层次结构(摘自《Java NIO》)

从上至下,前三层是描述和约定Channel功能的interface(除了java.nio.channels.spi. AbstractInterruptibleChannel),而在下面几层是和这些结构对应的Channel类。

图中也给出了各个interface和class的方法。Channel接口本身只是总体上提供了两个“不痛不痒”的方法。而紧接着的下面三个接口,各为自己的特征给出了一个方法约定。第三层的接口则是更进一步更具体的扩展。

再往下看,就是实现这些接口的类,从图中左右两侧可以看到,实际上是分为了两大类。一类是SelectableChannel,而另一类是FileChannel。而SelectableChannel实际上是网络通信相关的SocketChannel等的实现基础。

需要注意的是,图中下层的这些class实际上也都是abstract的,即抽象类。我们上面也说过,Channel是数据源和数据归宿之间通道的抽象。而实际的IO必然是和底层操作系统相关的,是需要依赖平台的具体实现,这些不会再高度抽象的跨平台Java API中体现,只是在JDK实现中有所不同。

除图中提到的以外,还有提供静态方法的工具类Channels。另外,后续的JDK1.7中页增加了新的内容。

1. Channel的使用和分类

之前在介绍ByteBuffer的时候就已经提到过了,Channel是同系统底层数据的交互,为了保证效率,统一使用字节为单位。从上面的类层次结构图中也可以看得出来,Channel的两个子接口分别是ReadableByteChannel和WritableByteChannel,而ByteChannel则对这两个接口做了合并归纳。而且SocketChannel和FileChannel都实现了这些接口。

需要注意的是,随着Java API的发展变化,接口的继承关系也和上图绘制的有所不同。如在JavaSE7中,增加了SeekableByteChannel接口等。尽管如此,大体上还是维持了原来的设计结构。

关于Channel对象的创建,有两类情况。对于Socket相关的Channel,只要调用特定类的open()方法即可,之后进行bind或者connect操作。而文件相关的FileChannel则不然,需要通过FileInputStream、FileOutputStream或者RandomAccessFile的getChannel()获取Channel对象。

SocketChannel sc = SocketChannel.open( );
sc.connect (new InetSocketAddress ("somehost", someport));

ServerSocketChannel ssc = ServerSocketChannel.open( );
ssc.socket( ).bind (new InetSocketAddress (somelocalport));

DatagramChannel dc = DatagramChannel.open( );

RandomAccessFile raf = new RandomAccessFile ("somefile", "r");
FileChannel fc = raf.getChannel( );

从这个地方,我们就发现,其实Channel根据IO服务的情况主要分为两大类,按照《Java NIO》的描述,两类IO分别是:file I/O 和 stream I/O。前者是针对文件读写操作的,而后者多是网络通信相关的和Socket相关的。Channel分类也基本如此,和前者对应的FileChannel,以及与后者对应的SocketChannel等类对象。

两者的区别不仅仅在Channel对象的构建和初始化上,更重要的还是和Selecotor的结合使用相关。也包括是否支持阻塞模式等。

接下来就是通过Channel进行实际的IO操作,即读和写。其实,这两项分别都在ReadableByteChannel和WritableByteChannel中声明了的。ByteChannel的子类都有

public int read (ByteBuffer dst) throws IOException;
public int write (ByteBuffer src) throws IOException;

这两个方法。

需要注意的是,尽管你所拿到的Channel对象都是有read()和write()方法的,但并不代表他们一定是可读或者可写的。是否可以读写需要了解这个Channel的来头,这是比较讨厌的一点。

读写之后,通常就要进行关闭操作。关闭的概念其实很简单,当一个Channel被关闭后,就不再可用。相关的close()和isOpen()方法可以进行关闭和检查Channel是否是打开状态。执行close()可能会对线程造成阻塞,当一个Channel关闭后,后续的close()操作直接返回。

当Channel对象的close和中断结合起来,就有些要提到的了。一个阻塞在Channel对象上的线程如果收到了中断操作,则Channel对象关闭,线程收到ClosedByInterruptException异常。而把顺序稍微调整一下,一个已经被set中断信号的线程访问Channel对象时,Channel对象也会被关闭,线程得到ClosedByInterruptException异常。其它阻塞在这个Channel上的线程会得到AsynchronousCloseException异常。

以上这段中提到的Channel对象需要InterruptibleChannel接口,但FileChannel和SocketChannel等大多数类都完成了对这个interface的实现。

2. Scatter/Gather集与散

Scatter的意思是分散,Gather的意思是聚集。我们注意到在上面的类层次结构图中,除了ByteChannel外,各Channel类还都实现了两个接口,分别是:

  • ScatteringByteChannel
  • GatheringByteChannel

对于这两个接口,这里不做过多描述。也许我们通过接口中约定的方法,就大概知道他们的作用了。

public interface ScatteringByteChannel extends ReadableByteChannel
{
   public long read (ByteBuffer [] dsts) throws IOException;
   public long read (ByteBuffer [] dsts, int offset, int length) throws IOException;
}
public interface GatheringByteChannel extends WritableByteChannel
{
   public long write(ByteBuffer[] srcs) throws IOException;
   public long write(ByteBuffer[] srcs, int offset, int length) throws IOException;
}

实际上就是对读写操作进行顺序多Buffer的支持。

作者原创,难免有错误,欢迎读者热心评论留言指出,以免误导他人,谢谢!

相关文章:

此条目发表在 IO, Java, Java语言, 开发, 计算机技术 分类目录,贴了 , , 标签。将固定链接加入收藏夹。