Java NIO系列教程(二) Channel通道介绍及FileChannel详解
目录:
《Java NIO系列教程(三) Channel之Socket通道》
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中写入。
正如上面所说,从通道读取数据到缓冲区,从缓冲区写入数据到通道。如下图所示:
Channel的实现
这些是Java NIO中最重要的通道的实现:
- FileChannel:从文件中读写数据
- DatagramChannel:通过UDP读写网络中的数据
- SocketChannel:通过TCP读写网络中的数据
- ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
正如你所看到的,这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO。
FileChannel
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的更多细节。
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; 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(); } }
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; 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被填满。