JavaNIO-Channel

Channel

通道(Channel)的作用有类似于流(Stream),用于传输文件或者网络上的数据。

image.png

上图中,箭头就相当于通道。一个不是很准确的例子:把通道想象成铁轨,缓冲区则是列车,铁轨的起始与终点则可以是socket,文件系统和我们的程序。假如当我们在代码中要写入数据到一份文件的时候,我们先把列车(缓冲区)装满,然后把列车(缓冲区)放置到铁轨上(通道),数据就被传递到通道的另一端,文件系统。读取文件则相反,文件的内容被装到列车上,传递到程序这一侧,然后我们在代码中就可以读取这个列车中的内容(读取缓冲区)。

通道与传统的流还是有一些区别的:

  • 通道可以同时支持读写(不是一定支持),而流只支持单方向的操作,比如输入流只能读,输出流只能写。

  • 通道可以支持异步的读或写,而流是同步的。

  • 通道的读取或写入是通过缓冲区来进行的,而流则写入或返回字节。

FileChannel

通道大致上可以分为两类:文件通道和socket通道。看一下文件通道:

文件通道可以由以下几个方法获得:

RandomAccessFile file = new RandomAccessFile(new File(fileName), "rw");
FileChannel channel = file.getChannel();

FileInputStream stream = new FileInputStream(new File(fileName));
FileChannel channel = stream.getChannel();

FileOutputStream stream = new FileOutputStream(new File(fileName));
FileChannel channel = stream.getChannel();

FileChannel channel = FileChannel.open(Paths.get(fileName));

FileChannel 类结构:

image.png

可见FileChannel实现了读写接口、聚集、发散接口,以及文件锁功能。下面会提到。

看一下FileChannel的基本方法:

public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel {
    // 这里仅列出部分API
    public abstract long position()
    public abstract void position (long newPosition)
    public abstract int read (ByteBuffer dst)
    public abstract int read (ByteBuffer dst, long position)
    public abstract int write (ByteBuffer src)
    public abstract int write (ByteBuffer src, long position)
    public abstract long size()
    public abstract void truncate (long size)
    public abstract void force (boolean metaData)
}

在通道出现之前,底层的文件操作都是通过RandomAccessFile类的方法来实现的。FileChannel模拟同样的 I/O 服务,因此它们的API自然也是很相似的。

image.png

上图是FileChannel、RandomAccessFile 和 POSIX I/O system calls 三者在方法上的对应关系。

POSIX接口我们在上一篇文章中也略有提及,他是一个系统级别的接口。下面看一下这几个接口,主要也是和上一篇文章文件描述符的介绍做一个呼应。

  • position()和position(long newPosition)

position()返回当前文件的position值,position(long newPosition)将当前position设置为指定值。当字节被read()或write()方法传输时,文件position会自动更新。

position的含义与Buffer类中的position含义相似,都是指向下一个字节读取的位置。

回想一下介绍文件描述符的文章当中提到,当进程打开一个文件时,内核就会创建一个新的file对象,这个file对象有一个字段loff_t f_pos描述了文件的当前位置,position相当于loff_t f_pos的映射。由此可知,如果是使用同一文件描述符读取文件,那么他们的position是相互影响的:

RandomAccessFile file = new RandomAccessFile(new File(fileName), "rw");
FileChannel channel = file.getChannel();
System.out.println("position: " + channel.position());
file.seek(30);
System.out.println("position: " + channel.position());

打印如下:

position: 0
position: 30

这是因为,file与channel使用了同一个文件描述符。如果新建另一个相同文件的通道,那么他们之间的position不会相互影响,因为使用了不同的文件描述符,指向不同的file对象。

  • truncate(long size)

当需要减少一个文件的size时,truncate()方法会砍掉指定的size值之外的所有数据。这个方法要求通道具有写权限。

如果当前size大于给定size,超出给定size的所有字节都会被删除。如果提供的新size值大于或等于当前的文件size值,该文件不会被修改。

RandomAccessFile file = new RandomAccessFile(new File(fileName), "rw");
FileChannel channel = file.getChannel();
System.out.println("size: " + channel.size());
System.out.println("position: " + channel.position());
System.out.println("trucate: 90");
channel.truncate(90);
System.out.println("size: " + channel.size());
System.out.println("position: " + channel.position());

打印如下:

size: 100
position: 0
trucate: 90
size: 90
position: 0
  • force(boolean metaData)

force()方法告诉通道强制将全部待定的修改都应用到磁盘的文件上。

如果文件位于一个本地文件系统,那么一旦force()方法返回,即可保证从通道被创建(或上次调用force())时起的对文件所做的全部修改已经被写入到磁盘。但是,如果文件位于一个远程的文件系统,如NFS上,那么不能保证待定修改一定能同步到永久存储器。

force()方法的布尔型参数表示在方法返回值前文件的元数据(metadata)是否也要被同步更新到磁盘。元数据指文件所有者、访问权限、最后一次修改时间等信息。

FileChannel对象是线程安全的。如果有一个线程已经在执行会影响通道位置或文件大小的操作,那么其他尝试进行此类操作之一的线程必须等待。

ReadableByteChannel、WritableByteChannel

通道可以是单向或者双向的。

public interface ReadableByteChannel extends Channel{
    public int read (ByteBuffer dst) throws IOException;
}

public interface WritableByteChannel extends Channel{
    public int write (ByteBuffer src) throws IOException;
}

public interface ByteChannel extends ReadableByteChannel, WritableByteChannel{
}
image.png

实现ReadableByteChannel或WritableByteChannel其中之一的channel是单向的,只可以读或者写。如果一个类同时实现了这两种接口,那么他就具备了双向传输的能力。

java为我们提供了一个接口ByteChannel,同时继承了上述两个接口。所以,实现了ByteChannel接口的类可以读,也可以写。

FlieChannel这一节中我们知道,文件在不同的方式下以不同的权限打开。比如FileInputStream.getChannel()方法返回一个FileChannel实例,FileChannel是个抽象类,间接的实现了ByteChannel接口,也就意味着提供了read和write接口。但是FileInputStream.getChannel()方法返回的FileChannel实际上是只读的,很简单,因为FileInputStream本身就是个输入流啊~在这样一个通道上调用write方法将抛出NonWritableChannelException异常,因为FileInputStream对象总是以read-only的权限打开通道。看一下代码:

FileInputStream.getChannel()

public FileChannel getChannel() {
        synchronized (this) {
            if (channel == null) {
                // 第三个参数指定通道是否可读,第四个参数指定通道是否可写
                channel = FileChannelImpl.open(fd, path, true, false, this);

                /*
                 * Increment fd's use count. Invoking the channel's close()
                 * method will result in decrementing the use count set for
                 * the channel.
                 */
                fd.incrementAndGetUseCount();
            }
            return channel;
        }
}

同样的,FileOutputStream.getChannel()返回的通道是不可读的。

InterruptibleChannel

InterruptibleChannel是一个标记接口,当被通道使用时可以标示该通道是可以中断的。

如果一个线程在一个通道上处于阻塞状态时被中断(另外一个线程调用该线程的interrupt()方法设置中断状态),那么该通道将被关闭,该被阻塞线程也会产生一个ClosedByInterruptException异常。也就是说,假如一个线程的interrupt status被设置并且该线程试图访问一个通道,那么这个通道将立即被关闭,同时将抛出相同的ClosedByInterruptException异常。

java任务取消中提到了,传统的java io 在读写时阻塞,是不会响应中断的。解决办法就是使用InterruptibleChannel,在线程被中断时可以关闭通道并返回。

可中断的通道也是可以异步关闭。实现InterruptibleChannel接口的通道可以在任何时候被关闭,即使有另一个被阻塞的线程在等待该通道上的一个I/O操作完成。当一个通道被关闭时,休眠在该通道上的所有线程都将被唤醒并接收到一个AsynchronousCloseException异常。接着通道就被关闭并将不再可用。

Scatter/Gather

发散(Scatter)读取是将数据读入多个缓冲区(缓冲区数组)的操作。通道将数据依次填满到每个缓冲区当中。

汇聚(Gather)写出是将多个缓冲区(缓冲区数组)数据依次写入到通道的操作。

在FileChannel中提到的两个接口,提供了发散汇聚的功能:

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;
}

发散汇聚在某些场景下是很有用的,比如有一个消息协议格式分为head和body(比如http协议),我们在接收这样一个消息的时候,通常的做法是把数据一下子都读过来,然后解析他。使用通道的发散功能会使这个过程变得简单:

// head数据128字节
ByteBuffer header = ByteBuffer.allocate(128);
// body数据1024字节
ByteBuffer body   = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);

通道会依次填满这个buffer数组的每个buffer,如果一个buffer满了,就移动到下一个buffer。很自然的把head和body的数据分开了,但是要注意head和body的数据长度必须是固定的,因为channel只有填满一个buffer之后才会移动到下一个buffer。

FileLock

摘抄一段oracle官网上FileLock的介绍吧,感觉说的挺清楚了。(因为懒,就不翻译了,读起来不是很费劲)

A token representing a lock on a region of a file.
A file-lock object is created each time a lock is acquired on a file via one of the lock or tryLock methods of the FileChannel class, or the lock or tryLock methods of the AsynchronousFileChannel class.

A file-lock object is initially valid. It remains valid until the lock is released by invoking the release method, by closing the channel that was used to acquire it, or by the termination of the Java virtual machine, whichever comes first. The validity of a lock may be tested by invoking its >isValid method.

A file lock is either exclusive or shared. A shared lock prevents other concurrently-running programs from acquiring an overlapping exclusive lock, but does allow them to acquire overlapping shared locks. An exclusive lock prevents other programs from acquiring an overlapping lock of either type. >Once it is released, a lock has no further effect on the locks that may be acquired by other programs.

Whether a lock is exclusive or shared may be determined by invoking its isShared method. Some platforms do not support shared locks, in which case a request for a shared lock is automatically converted into a request for an exclusive lock.

The locks held on a particular file by a single Java virtual machine do not overlap. The overlaps method may be used to test whether a candidate lock range overlaps an existing lock.

A file-lock object records the file channel upon whose file the lock is held, the type and validity of the lock, and the position and size of the locked region. Only the validity of a lock is subject to change over time; all other aspects of a lock's state are immutable.

File locks are held on behalf of the entire Java virtual machine. They are not suitable for controlling access to a file by multiple threads within the same virtual machine.

File-lock objects are safe for use by multiple concurrent threads.

Platform dependencies

This file-locking API is intended to map directly to the native locking facility of the underlying operating system. Thus the locks held on a file should be visible to all programs that have access to the file, regardless of the language in which those programs are written.

Whether or not a lock actually prevents another program from accessing the content of the locked region is system-dependent and therefore unspecified. The native file-locking facilities of some systems are merely advisory, meaning that programs must cooperatively observe a known locking protocol in >order to guarantee data integrity. On other systems native file locks are mandatory, meaning that if one program locks a region of a file then other programs are actually prevented from accessing that region in a way that would violate the lock. On yet other systems, whether native file locks are >advisory or mandatory is configurable on a per-file basis. To ensure consistent and correct behavior across platforms, it is strongly recommended that the locks provided by this API be used as if they were advisory locks.

On some systems, acquiring a mandatory lock on a region of a file prevents that region from being mapped into memory, and vice versa. Programs that combine locking and mapping should be prepared for this combination to fail.

On some systems, closing a channel releases all locks held by the Java virtual machine on the underlying file regardless of whether the locks were acquired via that channel or via another channel open on the same file. It is strongly recommended that, within a program, a unique channel be used to >acquire all locks on any given file.

Some network filesystems permit file locking to be used with memory-mapped files only when the locked regions are page-aligned and a whole multiple of the underlying hardware's page size. Some network filesystems do not implement file locks on regions that extend past a certain position, often 230 >or 231. In general, great care should be taken when locking files that reside on network filesystems.

FileLock可以由以下几个方法获得:

public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel {
// 这里仅列出部分API
    public final FileLock lock()
    public abstract FileLock lock (long position, long size, boolean shared)
    public final FileLock tryLock()
    public abstract FileLock tryLock (long position, long size, boolean shared)
}

其中,lock是阻塞的,tryLock是非阻塞的。position和size决定了锁定的区域,shared决定了文件锁是共享的还是独占的。

不带参数的lock方法等价于fileChannel.lock(0L, Long.MAX_VALUE, false),tryLock亦然。

lock方法是响应中断的,当线程被中断时方法抛出FileLockInterruptionException异常。如果通道被另外一个线程关闭,该暂停线程将恢复并产生一个 AsynchronousCloseException异常。

上面还提到了,文件锁是针对于进程级别的。如果有多个进程同时对一个文件锁定,并且其中有独占锁的话,这些锁的申请会被串行化。

如果是同一个进程(Jvm实例)的多个线程同时请求同一个文件区域的lock的话,会抛出OverlappingFileLockException异常。

Channel-to-Channel

FileChannel提供了接口,用于通道和通道之间的直接传输。

public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel {
    // 这里仅列出部分API
    public abstract long transferTo (long position, long count, WritableByteChannel target)
    public abstract long transferFrom (ReadableByteChannel src, long position, long count)
}

只有FileChannel类有这两个方法,因此Channel-to-Channel传输中通道之一必须是FileChannel。不能在socket通道之间直接传输数据,不过socket通道实现WritableByteChannel和ReadableByteChannel接口,因此文件的内容可以用transferTo()方法传输给一个socket通道,或者也可以用transferFrom()方法将数据从一个socket通道直接读取到一个文件中。

直接的通道传输不会更新与某个FileChannel关联的position值。请求的数据传输将从position参数指定的位置开始,传输的字节数不超过count参数的值。实际传输的字节数会由方法返回。

直接通道传输的另一端如果是socket通道并且处于非阻塞模式的话,数据的传输将具有不确定性。比如,transferFrom从socket通道读取数据,如果socket中的数据尚未准备好,那么方法将直接返回。

例子:

RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel      fromChannel = fromFile.getChannel();

RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel      toChannel = toFile.getChannel();

long position = 0;
long count    = fromChannel.size();

toChannel.transferFrom(fromChannel, position, count);


RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel      fromChannel = fromFile.getChannel();

RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel      toChannel = toFile.getChannel();

long position = 0;
long count    = fromChannel.size();

fromChannel.transferTo(position, count, toChannel);

参考

Java nio入门教程详解

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,738评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,377评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,774评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,032评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,015评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,239评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,724评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,374评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,410评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,457评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,132评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,733评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,804评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,022评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,515评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,116评论 2 341

推荐阅读更多精彩内容

  • 高二那年,遇见你,一眼就喜欢上了你。 我努力让自己变得更好,让你也能从人群中一眼看见我。 后来我们经常一起坐车回家...
    安静老太太阅读 337评论 0 2
  • 她总是会骂你甚至打你 她总是唠叨你甚至烦你 她总是埋怨你甚至凶你 她总是担心你甚至失眠 她总是想你却不告诉你 她总...
    其實生活沒道里阅读 168评论 0 0
  • 每天做着一些无可奈何的事,忙着一些无法改变的现实,但是如果不做,就是咸鱼。
    浮夸hh阅读 101评论 0 0