Channel 概述
Channel是一个通道,可以通过它读取和写入数据,它就像是水管一样,网络数据通过 Channel 进行读取和写入。通道和流的不同之处在与通道是双向的,流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStram 的子类),而且通道上可以用于读,写或者同事用于读写。因为 Channel 是全双工的,所以它可以比流更好的映射底层操作系统的 API。
NIO 中通过 Channel 封装了对数据源的操作,通过channel 我们可以操作数据源, 但是又不关心数据源的具体数据结构。这个数据源可能是很多种,比如,可以是文件,也可是网络 socket 。 在大多数应用中,channel 与文件描述符或者 socket 是一一对应的。 channel 在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效的传输数据。
Channel 接口源码
public interface Channel extends Closeable { /** * Tells whether or not this channel is open. * * @return <tt>true</tt> if, and only if, this channel is open */ public boolean isOpen(); /** * Closes this channel. * * <p> After a channel is closed, any further attempt to invoke I/O * operations upon it will cause a {@link ClosedChannelException} to be * thrown. * * <p> If this channel is already closed then invoking this method has no * effect. * * <p> This method may be invoked at any time. If some other thread has * already invoked it, however, then another invocation will block until * the first invocation is complete, after which it will return without * effect. </p> * * @throws IOException If an I/O error occurs */ public void close() throws IOException; }
与缓冲区不同,通道 api 主要是由接口指定。不同的操作系统通道实现(ChannelImplementation) 会有根本性差异,所以通道 API 仅仅描述了可以做什么。因此很自然地,通道实现机场使用操作系统本地代码。通道接口允许您以一种受控且可移植方式来访问底层的 I/O 服务。
Channel 是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做一个比较,通道就是流,所有的数据通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入了一个包含多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道中读入缓冲区,再从缓冲区中获取这些字节。
Java NIO 的通道类似流,但是又有些不同:
- 既可以通道汇总读取数据,又可以写数据到通道,但流的读写通常是单向的。
- 通道可以异步地读写。
- 通道中的数据总是先读取到 buffer , 或者总是需要从一个 buffer 写入。
正如上面所说, 从通道读取数据到缓冲区,从缓冲区写入数据到通道。如下图所示:
Channel 实现
下面是 Java NIO 中最重要的 Channel 实现:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
(1) FileChannel 从文件中读写数据
(2) DatagramChannel 能够通过 UDP 读写网络中的数据
(3) SocketChannel 能够通过 TCP 写网络中的数据
(4) ServerSocketChannel 可以监听新进来的 TCP 连接, 就像 WBE 服务那样,对每个新进来的连接都会创建一个 SocketChannel 。
正如你所看到的,这些通道涵盖了 UDP 和 TCP 网络IO,以及文件 IO。
FileChannel 介绍和实例
FileChannel 类可以实现常用的 read、write 以及 scatter、gather 操作,同时它也可以提供很多专用于文件的新方法。这些方法中许多都是我们熟悉的文件操作。
方法 | 操作 |
int read *( *ByteBuffer dst ) | 从 Channel 中读取到数据到 ByteBuffer |
long read *( *ByteBuffer *[] *dsts ) | 将 Channel 中数据“分散”到 ByteBuffer |
int write *( *ByteBuffer src ) | 将 ByteBuffer 中的数据写入到 Channel 中 |
long write *( *ByteBuffer *[] *srcs ) | 将 ByteBuffer[] 中的数据“聚集”到 Channel 中 |
long position () | 返回此通道的文件位置 |
FileChannel position *( *long newPosition ) | 设置此通道的文件位置 |
long size () | 返回此通道的文件的当前大小 |
FileChannel truncate *( *long size ) | 将此通道的文件截取为给定大小 |
void force *( *boolean metaData ) | 强制将所有对此通道文件更新到写入到存储设备中 |
下面是一个使用 FileChannel 读取数据到 buffer 中的一个实例:
public class FileChannelDemo { // FileChannel 读取数据到 buffer 中 public static void main(String[] args) throws IOException { // 创建 FileChannel RandomAccessFile accessFile = new RandomAccessFile("C:\a.txt", "rw"); FileChannel fileChannel = accessFile.getChannel(); // 创建 buffer ByteBuffer byteBuffer = ByteBuffer.allocate(48); while (fileChannel.read(byteBuffer) != -1) { System.out.println("读取到了: " + byteBuffer); byteBuffer.flip(); while (byteBuffer.hasRemaining()) { System.out.println((char) byteBuffer.get()); } byteBuffer.clear(); } fileChannel.close(); System.out.println("end"); } }
Buffer 通常的操作
将数据写入缓冲区
调用 buffer.filp() 反转读写模式
从缓冲区读取数据
调用 buffer.clear() 或 buffer.compact() 清除缓冲区内容
FileChannel 操作和详解
打开 FileChannel
使用 FileChannel 之前,必须先打开它, 但是,我们无法直接打开一个 FileChannel ,需要通过使用一个 InputStream, OutputStream 或者 RandomAccessFile 来获取一个 FileCannel 实例, 下面是通过 RandomAccessFile 打开 FileChannel 的实际例子:
RandomAccessFile accessFile = new RandomAccessFile("C:\a.txt", "rw"); FileChannel fileChannel = accessFile.getChannel();
从 FileChannel 中读取数据
调用读取数据 read() 方法之一从 File Channel 中读取数据。如:
ByteBuffer byteBuffer = ByteBuffer.allocate(48); byteBuffer = accessFile.read(byteBuffer);
首先分配一个 Buffer , 从 FileChannel 中读取的数据将被读取到 Buffer 中。然后,调用 FileChannel.read() 方法。该方法将数据从 FileChanel 读取到 Buffer 中。read() 方法返回的 int 值表示有多少个字节读取到了 Buffer 中。如果返回 -1 , 表示到了文件末尾。
向 FileChannel 写数据
通过 FileChannel.write() 方法向 FileChannel 写数据, 该方法的一个参数是 buffer。
如:
public class FileChannelDemo2 { // FileChannel 读取数据到 buffer 中 public static void main(String[] args) throws IOException { // 创建 FileChannel RandomAccessFile accessFile = new RandomAccessFile("C:\a.txt", "rw"); FileChannel fileChannel = accessFile.getChannel(); String newData = "new string to write to file ..." + System.currentTimeMillis(); // 创建 buffer ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.clear(); byteBuffer.put(newData.getBytes()); byteBuffer.flip(); while (byteBuffer.hasRemaining()) { fileChannel.write(byteBuffer); } fileChannel.close(); System.out.println("end"); } }
注意 FileChannel.wrte() 是在 while 循环中调用的,因为无法保证 write 方法一次能向 FileChannel 写入多少字节,因此需要重复调用 write() 方法,直到 Buffer 中已经没有尚未写入通道的字节。
关闭 FileChannel
用完 FileChannel 后必须将其关闭。如:
fileChannel.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()