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

目录:html

Java NIO系列教程(二) Channeljava

Java NIO系列教程(三) Channel之Socket通道spring

 

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

NIO中经过channel封装了对数据源的操做,经过channel 咱们能够操做数据源,但又没必要关心数据源的具体物理结构。
这个数据源多是多种的。好比,能够是文件,也能够是网络socket。在大多数应用中,channel与文件描述符或者socket是一一对应的。Channel用于在字节缓冲区和位于通道另外一侧的实体(一般是一个文件或套接字)之间有效地传输数据。
channel接口源码:缓存

package java.nio.channels;
public interface Channel;
{
    public boolean isOpen();
    public void close() throws IOException;
}

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

 

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

 

Java NIO的通道相似流,但又有些不一样:网络

  • 既能够从通道中读取数据,又能够写数据到通道。但流的读写一般是单向的。
  • 通道能够异步地读写。
  • 通道中的数据老是要先读到一个Buffer,或者老是要从一个Buffer中写入。

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

 

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;
    public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
    {
        // This is a partial API listing
        // All methods listed here can throw java.io.IOException
        public abstract int read (ByteBuffer dst, long position);
        public abstract int write (ByteBuffer src, long position);
        public abstract long size();
        public abstract long position();
        public abstract void position (long newPosition);
        public abstract void truncate (long size);
        public abstract void force (boolean metaData);
        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);
        public abstract MappedByteBuffer map (MapMode mode, long position, long size);
        public static class MapMode;
        public static final MapMode READ_ONLY;
        public static final MapMode READ_WRITE;
        public static final MapMode PRIVATE;
        public abstract long transferTo (long position, long count, WritableByteChannel target);
        public abstract long transferFrom (ReadableByteChannel src, long position, 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天然也是很类似的。

  三者之间的方法对比:

  
FILECHANNEL RANDOMACCESSFILE POSIX 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;

public class FileChannelTest {

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        RandomAccessFile aFile = new RandomAccessFile("d:\\soft\\nio-data.txt", "rw");
        FileChannel inChannel = aFile.getChannel();
        ByteBuffer buf = ByteBuffer.allocate(48);
        int bytesRead = 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 48
1234567qwertrewq
uytrewq
hgfdsa
nbvcxz
iop89wan

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

一、打开FileChannel

在使用FileChannel以前,必须先打开它。可是,咱们没法直接打开一个FileChannel,须要经过使用一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例。下面是经过RandomAccessFile打开FileChannel的示例:

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

二、从FileChannel读取数据

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

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);

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

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

三、向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中已经没有还没有写入通道的字节。

四、关闭FileChannel

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

channel.close();

五、FileChannel的position方法

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

也能够经过调用position(long pos)方法设置FileChannel的当前位置。

这里有两个例子:

long pos = channel.position();
channel.position(pos +123);

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

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

六、FileChannel的size方法

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

long fileSize = channel.size();

七、FileChannel的truncate方法

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

channel.truncate(1024);

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

八、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;

public class FileChannelWrite {
    static private final byte message[] = { 83, 111, 109, 101, 32, 98, 121, 116, 101, 115, 46 };

    static public void main(String args[]) throws Exception {
        FileOutputStream fout = new FileOutputStream("e:\\logs\\test2.txt");
        FileChannel fc = fout.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        for (int i = 0; i < message.length; ++i) {
            buffer.put(message[i]);
        }
        buffer.flip();
        fc.write(buffer);
        fout.close();
    }
}

九、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;

public class FileChannelTest2 {

    public static void main(String[] args) throws IOException {
        RandomAccessFile aFile = new RandomAccessFile("d:\\soft\\fromFile.txt", "rw");
        FileChannel fromChannel = aFile.getChannel();
        
        RandomAccessFile bFile = new RandomAccessFile("d:\\soft\\toFile.txt", "rw");
        FileChannel toChannel = bFile.getChannel();
        long position = 0;
        long count = 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;

public class FileChannelTest3 {

    public static void main(String[] args) throws IOException {
        RandomAccessFile aFile = new RandomAccessFile("d:\\soft\\fromFile.txt", "rw");
        FileChannel fromChannel = aFile.getChannel();
        
        RandomAccessFile bFile = new RandomAccessFile("d:\\soft\\toFile.txt", "rw");
        FileChannel toChannel = bFile.getChannel();
        long position = 0;
        long count = fromChannel.size();
        fromChannel.transferTo(position, count, toChannel);
        aFile.close();
        bFile.close();
        System.out.println("over!");
    }

}

是否是发现这个例子和前面那个例子特别类似?除了调用方法的FileChannel对象不同外,其余的都同样。上面所说的关于SocketChannel的问题在transferTo()方法中一样存在。SocketChannel会一直传输数据直到目标buffer被填满。

相关文章
相关标签/搜索