Channel 通道的简介
通道( Channel):由 java.nio.channels 包定义的。 Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel本身不能直接访问数据, Channel 只能与Buffer 进行交互。
应用程序与磁盘之间的数据写入或者读出,都需要由用户地址空间和内存地址空间之间来回复制数据,内存地址空间中的数据通过操作系统层面的IO接口,完成与磁盘的数据存取。在应用程序调用这些系统IO接口时,由CPU完成一系列调度、任务分配,早先这些IO接口都是由CPU独立负责。所以当发生大规模读写请求时,CPU的占用率很高。
之后,操作系统为了避免CPU完全被各种IO接口调用占用,引入了DMA(直接存储器存储)。当应用程序对操作系统发出一个读写请求时,会由DMA先向CPU申请权限,申请到权限之后,内存地址空间与磁盘之间的IO操作就全由DMA来负责操作。这样,在读写请求的过程中,CPU不需要再参与,CPU去做其他事情。当然,DMA来独立完成数据在磁盘与内存空间中的来去,需要借助于DMA总线。但是当DMA总线过多时,大量的IO操作也会造成总线冲突,即也会影响最终的读写性能。
为了避免DMA总线冲突对性能的影响,后来便有了通道的方式。通道,它是一个完全独立的处理器。CPU是中央处理器,通道本身也是一个处理器,专门负责IO操作。既然是处理器,通道有自己的IO命令,与CPU无关。它更适用于大型的IO操作,性能更高。
Java的NIO的通道类似流,但是又有一些不同:
- 1、既可以从Channel中读数据也可以往Channel里面写数据;但是流的读写一般是单向的。
- 2、Channel可以异步的读写。
- 3、Channel的读写是通过Buffer这个中介实现的。数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
下面是Java的NIO的最重要的几个channel的实现:
- 1、FileChannel:主要是用于文件的读写。
- 2、DatagramChannel:主要用于UDP读写网络中的数据。
- 3、SocketChannel:通过TCP读写网络中的数据。
- 4、ServerSocketChannel:主要用于服务端,可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
FileChannel 类
FileChannel主要用来对本地文件进行 IO 操作,常见的方法有
- public int read(ByteBuffer dst):从通道读取数据并放到缓冲区中。
- public int write(ByteBuffer src):把缓冲区的数据写到通道中。
- public long transferFrom(ReadableByteChannel src, long position, long count):从目标通道中复制数据到当前通道。
- public long transferTo(long position, long count, WritableByteChannel target):把数据从当前通道复制给目标通道。
应用实例1——本地文件写数据
使用ByteBuffer(缓冲) 和 FileChannel(通道), 将 "hello,明天" 写入到file01.txt 中
public class NIOFileChannel01 {
public static void main(String[] args) throws Exception{
String str = "hello,明天";
//创建一个输出流->channel
FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");
//通过 fileOutputStream 获取 对应的 FileChannel
//这个 fileChannel 真实 类型是 FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel();
//创建一个缓冲区 ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将 str 放入 byteBuffer
byteBuffer.put(str.getBytes());
//对byteBuffer 进行flip
byteBuffer.flip();
//将byteBuffer 数据写入到 fileChannel
fileChannel.write(byteBuffer);
fileOutputStream.close();
}
}
应用实例2——本地文件读数据
使用ByteBuffer(缓冲) 和 FileChannel(通道), 将 file01.txt 中的数据读入到程序,并显示在控制台屏幕
public class NIOFileChannel02 {
public static void main(String[] args) throws Exception {
//创建文件的输入流
File file = new File("d:\\file01.txt");
FileInputStream fileInputStream = new FileInputStream(file);
//通过fileInputStream 获取对应的FileChannel -> 实际类型 FileChannelImpl
FileChannel fileChannel = fileInputStream.getChannel();
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//将 通道的数据读入到Buffer
fileChannel.read(byteBuffer);
//将byteBuffer 的 字节数据 转成String
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
}
}
应用实例3——使用一个Buffer完成文件读取
使用 FileChannel(通道) 和 方法 read , write,完成文件的拷贝
public class NIOFileChannel03 {
public static void main(String[] args) throws Exception {
FileInputStream fileInputStream = new FileInputStream("input.txt");
FileChannel fileChannel01 = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("input2.txt");
FileChannel fileChannel02 = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while (fileChannel01.read(byteBuffer) != -1) { //循环读取
//将buffer中的数据写入到 fileChannel02 -- 2.txt
byteBuffer.flip();
fileChannel02.write(byteBuffer);
byteBuffer.clear(); //清空buffer
}
//关闭相关的流
fileInputStream.close();
fileOutputStream.close();
}
}
应用实例4——拷贝文件transferFrom 方法
使用 FileChannel(通道) 和 方法 transferFrom ,完成文件的拷贝
public class NIOFileChannel04 {
public static void main(String[] args) throws Exception {
//创建相关流
FileInputStream fileInputStream = new FileInputStream("d:\\a.jpg");
FileOutputStream fileOutputStream = new FileOutputStream("d:\\a2.jpg");
//获取各个流对应的filechannel
FileChannel sourceCh = fileInputStream.getChannel();
FileChannel destCh = fileOutputStream.getChannel();
//使用transferForm完成拷贝
destCh.transferFrom(sourceCh,0,sourceCh.size());
//关闭相关通道和流
sourceCh.close();
destCh.close();
fileInputStream.close();
fileOutputStream.close();
}
}
关于Buffer 和 Channel的注意事项和细节
-
1、ByteBuffer 支持类型化的put 和 get,put 放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有 BufferUnderflowException 异常。
-
2、可以将一个普通Buffer 转成只读Buffer。
-
3、NIO 还提供了 MappedByteBuffer, 可以让文件直接在内存(堆外的内存)中进行修改, 而如何同步到文件由NIO 来完成。
-
4、前面我们讲的读写操作,都是通过一个Buffer 完成的,NIO 还支持 通过多个Buffer (即 Buffer 数组) 完成读写操作,即 Scattering 和 Gathering。
MappedByteBuffer案例:
MappedByteBuffer修改文件
/**
* MappedByteBuffer 可让文件直接在内存(堆外内存)修改,
* 操作系统不需要在内核空间和用户空间来回的拷贝
*/
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
RandomAccessFile randomAccessFile = new RandomAccessFile("input.txt", "rw");
//获取对应的通道
FileChannel channel = randomAccessFile.getChannel();
/**
* 参数1:FileChannel.MapMode.READ_WRITE 使用的读写模式
* 参数2:0 可以直接修改的起始位置
* 参数3: 5 是映射到内存的大小(不是索引位置) ,即将 input.txt 的多少个字节映射到内存
* 可以直接修改的范围就是 0-5
* 实际类型 DirectByteBuffer
*/
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0,(byte) 'H');
mappedByteBuffer.put(3,(byte) '9');
//mappedByteBuffer.put(5,(byte) 'Y');//IndexOutOfBoundsException
randomAccessFile.close();
System.out.println("修改成功");
}
}