Java NIO系列教程(二) Channel通道介绍及FileChannel详解

Channel是一个通道,可以通过它读取和写入数据,它就像自来水管一样,网络数据通过Channel读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而且通道可以用于读、写或者同事用于读写。因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。

NIO中通过channel封装了对数据源的操作,通过channel 我们可以操作数据源,但又不必关心数据源的具体物理结构。

这个数据源可能是多种的。比如,可以是文件,也可以是网络socket。在大多数应用中,channel与文件描述符或者socket是一一对应的。Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。

channel接口源码:

package java.nio.channels;publicinterface Channel;

{

    publicboolean isOpen();

    publicvoidclose()throws IOException;

}

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


Channel是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流。所有数据都通过Buffer对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。


Java NIO的通道类似流,但又有些不同:

既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。

通道可以异步地读写。

通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

正如上面所说,从通道读取数据到缓冲区,从缓冲区写入数据到通道。如下图所示:


Channel的实现

这些是Java NIO中最重要的通道的实现:

FileChannel:从文件中读写数据

DatagramChannel:通过UDP读写网络中的数据

SocketChannel:通过TCP读写网络中的数据

ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

正如你所看到的,这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO。

FileChannel

FileChannel类可以实现常用的read,write以及scatter/gather操作,同时它也提供了很多专用于文件的新方法。这些方法中的许多都是我们所熟悉的文件操作。

FileChannel类的JDK源码:

package java.nio.channels;

    publicabstractclassFileChannelextendsAbstractChannelimplements ByteChannel, GatheringByteChannel, ScatteringByteChannel

    {

        // This is a partial API listing

        // All methods listed here can throw java.io.IOExceptionpublicabstractintread (ByteBuffer dst,long position);

        publicabstractintwrite (ByteBuffer src,long position);

        publicabstractlong size();

        publicabstractlong position();

        publicabstractvoidposition (long newPosition);

        publicabstractvoidtruncate (long size);

        publicabstractvoidforce (boolean metaData);

        publicfinal FileLock lock();

        publicabstractFileLock lock (longposition,longsize,boolean shared);

        publicfinal FileLock tryLock();

        publicabstractFileLock tryLock (longposition,longsize,boolean shared);

        publicabstractMappedByteBuffer map (MapMode mode,longposition,long size);

        publicstaticclass MapMode;

        publicstaticfinal MapMode READ_ONLY;

        publicstaticfinal MapMode READ_WRITE;

        publicstaticfinal MapMode PRIVATE;

        publicabstractlongtransferTo (longposition,long count, WritableByteChannel target);

        publicabstractlongtransferFrom (ReadableByteChannel src,longposition,long count);

    }

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

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

  每个FileChannel对象都同一个文件描述符(file descriptor)有一对一的关系,所以上面列出的API方法与在您最喜欢的POSIX(可移植操作系统接口)兼容的操作系统上的常用文件I/O系统调用紧密对应也就不足为怪了。本质上讲,RandomAccessFile类提供的是同样的抽象内容。在通道出现之前,底层的文件操作都是通过RandomAccessFile类的方法来实现的。FileChannel模拟同样的I/O服务,因此它的API自然也是很相似的。

  三者之间的方法对比:

  

FILECHANNELRANDOMACCESSFILEPOSIX SYSTEM CALL

read( )read( )read( )

write( )write( )write( )

size( )length( )fstat( )

position( )getFilePointer( )lseek( )

position (long newPosition)seek( )lseek( )

truncate( )setLength( )ftruncate( )

force( )getFD().sync( )fsync( )


下面是一个使用FileChannel读取数据到Buffer中的示例:

package com.dxz.springsession.nio.demo1;import java.io.IOException;import java.io.RandomAccessFile;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;publicclass FileChannelTest {

    /**    * @param args

    * @throws IOException

    */publicstaticvoidmain(String[] args)throws IOException {

        RandomAccessFile aFile =newRandomAccessFile("d:\\soft\\nio-data.txt", "rw");

        FileChannel inChannel = aFile.getChannel();

        ByteBuffer buf = ByteBuffer.allocate(48);

        intbytesRead = inChannel.read(buf);

        while(bytesRead != -1) {

            System.out.println("Read " + bytesRead);

            buf.flip();

            while (buf.hasRemaining()) {

                System.out.print((char) buf.get());

            }

            buf.clear();

            bytesRead = inChannel.read(buf);

        }

        aFile.close();

        System.out.println("wan");

    }

}

文件内容:

1234567qwertrewq

uytrewq

hgfdsa

nbvcxz

iop89

输出结果:

Read 481234567qwertrewq

uytrewq

hgfdsa

nbvcxz

iop89wan

注意 buf.flip() 的调用,首先读取数据到Buffer,然后反转Buffer,接着再从Buffer中读取数据。下一节会深入讲解Buffer的更多细节。

1、打开FileChannel

在使用FileChannel之前,必须先打开它。但是,我们无法直接打开一个FileChannel,需要通过使用一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例。下面是通过RandomAccessFile打开FileChannel的示例:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");

FileChannel inChannel = aFile.getChannel();

2、从FileChannel读取数据

调用多个read()方法之一从FileChannel中读取数据。如:

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf);

首先,分配一个Buffer。从FileChannel中读取的数据将被读到Buffer中。

然后,调用FileChannel.read()方法。该方法将数据从FileChannel读取到Buffer中。read()方法返回的int值表示了有多少字节被读到了Buffer中。如果返回-1,表示到了文件末尾。

3、向FileChannel写数据

使用FileChannel.write()方法向FileChannel写数据,该方法的参数是一个Buffer。如:

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);

buf.clear();

buf.put(newData.getBytes());


buf.flip();

while(buf.hasRemaining()) {

  channel.write(buf);

}

注意FileChannel.write()是在while循环中调用的。因为无法保证write()方法一次能向FileChannel写入多少字节,因此需要重复调用write()方法,直到Buffer中已经没有尚未写入通道的字节。

4、关闭FileChannel

用完FileChannel后必须将其关闭。如:

channel.close();

5、FileChannel的position方法

有时可能需要在FileChannel的某个特定位置进行数据的读/写操作。可以通过调用position()方法获取FileChannel的当前位置。

也可以通过调用position(long pos)方法设置FileChannel的当前位置。

这里有两个例子:

long pos = channel.position();

channel.position(pos +123);

如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回-1 —— 文件结束标志。

如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。

6、FileChannel的size方法

FileChannel实例的size()方法将返回该实例所关联文件的大小。如:

long fileSize = channel.size();

7、FileChannel的truncate方法

可以使用FileChannel.truncate()方法截取一个文件。截取文件时,文件将中指定长度后面的部分将被删除。如:

channel.truncate(1024);

这个例子截取文件的前1024个字节。

8、FileChannel的force方法

FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上。要保证这一点,需要调用force()方法。

force()方法有一个boolean类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。

下面的例子同时将文件数据和元数据强制写到磁盘上:

channel.force(true);

示例:

package com.dxz.nio;

import java.io.FileInputStream;

import java.nio.ByteBuffer;

import java.nio.channels.FileChannel;

public class FileChannelRead {

    static public void main(String args[]) throws Exception {

        FileInputStream fin = new FileInputStream("e:\\logs\\test.txt");

        // 获取通道

        FileChannel fc = fin.getChannel();

        // 创建缓冲区

        ByteBuffer buffer = ByteBuffer.allocate(1024);

        // 读取数据到缓冲区

        fc.read(buffer);

        buffer.flip();

        while (buffer.remaining() > 0) {

            byte b = buffer.get();

            System.out.print(((char) b));

        }

        fin.close();

    }

}

写入:

package com.dxz.nio;import java.io.FileOutputStream;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;publicclass FileChannelWrite {

    staticprivatefinalbytemessage[] = { 83, 111, 109, 101, 32, 98, 121, 116, 101, 115, 46 };

    staticpublicvoidmain(String args[])throws Exception {

        FileOutputStream fout =newFileOutputStream("e:\\logs\\test2.txt");

        FileChannel fc = fout.getChannel();

        ByteBuffer buffer = ByteBuffer.allocate(1024);

        for(inti = 0; i < message.length; ++i) {

            buffer.put(message[i]);

        }

        buffer.flip();

        fc.write(buffer);

        fout.close();

    }

}

9、FileChannel的transferTo和transferFrom方法--通道之间的数据传输

如果两个通道中有一个是FileChannel,那你可以直接将数据从一个channel(译者注:channel中文常译作通道)传输到另外一个channel。

transferFrom()

FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中(译者注:这个方法在JDK文档中的解释为将字节从给定的可读取字节通道传输到此通道的文件中)。下面是一个简单的例子:

通过FileChannel完成文件间的拷贝:

package com.dxz.springsession.nio.demo1;import java.io.IOException;import java.io.RandomAccessFile;import java.nio.channels.FileChannel;publicclass FileChannelTest2 {

    publicstaticvoidmain(String[] args)throws IOException {

        RandomAccessFile aFile =newRandomAccessFile("d:\\soft\\fromFile.txt", "rw");

        FileChannel fromChannel = aFile.getChannel();


        RandomAccessFile bFile =newRandomAccessFile("d:\\soft\\toFile.txt", "rw");

        FileChannel toChannel = bFile.getChannel();

        longposition = 0;

        longcount = fromChannel.size();

        toChannel.transferFrom(fromChannel, position, count);

        aFile.close();

        bFile.close();

        System.out.println("over!");

    }

}

方法的输入参数position表示从position处开始向目标文件写入数据,count表示最多传输的字节数。如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数。

此外要注意,在SoketChannel的实现中,SocketChannel只会传输此刻准备好的数据(可能不足count字节)。因此,SocketChannel可能不会将请求的所有数据(count个字节)全部传输到FileChannel中。

transferTo()

transferTo()方法将数据从FileChannel传输到其他的channel中。下面是一个简单的例子:

package com.dxz.springsession.nio.demo1;import java.io.IOException;import java.io.RandomAccessFile;import java.nio.channels.FileChannel;publicclass FileChannelTest3 {

    publicstaticvoidmain(String[] args)throws IOException {

        RandomAccessFile aFile =newRandomAccessFile("d:\\soft\\fromFile.txt", "rw");

        FileChannel fromChannel = aFile.getChannel();


        RandomAccessFile bFile =newRandomAccessFile("d:\\soft\\toFile.txt", "rw");

        FileChannel toChannel = bFile.getChannel();

        longposition = 0;

        longcount = fromChannel.size();

        fromChannel.transferTo(position, count, toChannel);

        aFile.close();

        bFile.close();

        System.out.println("over!");

    }

}

是不是发现这个例子和前面那个例子特别相似?除了调用方法的FileChannel对象不一样外,其他的都一样。

上面所说的关于SocketChannel的问题在transferTo()方法中同样存在。SocketChannel会一直传输数据直到目标buffer被填满。

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

推荐阅读更多精彩内容

  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java I...
    zhisheng_blog阅读 1,076评论 0 7
  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java I...
    JackChen1024阅读 7,491评论 1 143
  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java I...
    编码前线阅读 2,244评论 0 5
  • 英语 舞蹈 和瑜伽跑步 我最爱的。一天至少做其中一件 耶耶耶
    时间有脚阅读 75评论 0 0
  • 从去年冬天开始,这今天第一次重新找到在球场上的感觉。 什么感觉呢,就仿佛球是你手的一部分,你可以完全控制它,你的意...
    李砍柴阅读 412评论 2 4