1、基本介绍
1、NIO通道类似于Java的输入输出流,有所区别:
通道可以同时进行读写,流只能读或写;
通道可以实现异步读写数据;
通道可以从缓冲区读数据,也可以写数据到缓冲区。
2、BIO中stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO的 通道(Channel)是双向的可以读操作,也可以写操作。
3、Channnel在NIO中是一个接口:public interface Channel extends Closeable{}
4、常用的Channnel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel。【ServerSocketChannel类似于ServerSocket,SocketChannel类似于Socket】
【注】在NIO模式下,当客户端向服务器请求连接时,客户端程序先访问服务器主线 程的ServerSocketChannel类,若服务器同意连接,则为客户端分配SocketChannel。
5、FileChannel用于文件数据读写,DatagramChannel用于UDP数据读写,ServerSocketChannel 和SocketChannel用于TCP数据读写。
2、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,WriteableByteChannel target);把数据从当前通道复制给目标通道
2-1通道应用案例1 本地文件写数据
要求:1)使用ByteBuffer(缓冲)和FileChannel(通道),将“你好,Netty”写入到file01.txt中
2)若文件不存在则创建文件
设计思路:1)首先定义一个String类型的变量,赋值为“你好,Netty”
2)创建一个文件输出流FileOutputStream内置FileChannel
3)创建一个ByteBuffer缓冲区,将String变量按照字节形式存入缓冲区
4)将缓冲区中的数据通过channel输出到指定文件
【注】使用FileChannel进行数据输入输出时,其底层使用的仍然是java.io中的输入输出流
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel01 {
public static void main(String[] args) throws Exception {
String str = "你好,Netty";
//创建一个输出流包裹到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进行反转
byteBuffer.flip();
//将byteBuffer中的数据写入到fileChannel中
fileChannel.write(byteBuffer);
fileOutputStream.close();
}
}
- 创建缓冲区ByteBuffer时,设置缓冲区大小为1024,此时缓冲区中无内容,底层数组各下标位置值为0
- 将String类型的数据写入缓冲区时,按照中文字符占3个字节,英文字符占1个字节,给ByteBuffer的0~13位置赋值【3*3+5-1=13】;写入结束后,ByteBuffer的limit=1024,position=14
- 将ByteBuffer反转(状态切换)flip()方法,此时limit=14,position=0,准备从0开始读取缓冲区数据,写入通道。
- 调用FlieChannel的write()方法,将数据写入通道,再由输出流OutputStream输出到指定文件
2-2通道应用案例2 本地文件读数据
要求:1)使用ByteBuffer和FileChannel,将file01.txt中的数据读入到程序,并显示在控制台屏幕
2)假定文件已经存在
设计思路:1)先创建文件的输入流FileInputStream获取对应的FileChannel
2)将file01.txt中的内容读入通道
3)创建一个字节缓冲区ByteBuffer,通过FileChannnel.read()方法将通道中的数据读入缓冲区
4)调用ByteBuffer底层的字节数组,将数组中的内容转换为字符串类型并定义变量接收
5)将字符串输出至控制台显示
【注】将数据读入缓冲区之后不能直接输出,因为此时缓冲区存放的并不是原始字符串,而是字符串对应的字节文件,必须将字节转换为字符串才符合要求
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel02 {
public static void main(String[] args) throws IOException {
//创建文件的输入流
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());
//将通道数据读入缓冲区中
fileChannel.read(byteBuffer);
//将byteBuffer中的字节数据转换为字符串输出
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
}
}
2-3通道应用案例3 使用一个buffer完成文件读取
要求:1)使用FileChannel(通道)和方法 read,write,完成文件拷贝
2)拷贝一个文本文件1.txt,放在项目下即可
数据流动过程如图:
设计思路:1)创建文件输入流输出流对象,并获取对应的FileChannel
2)创建ByteBuffer缓冲区
3)调用输入流通道的read()方法,将1.txt的内容循环读入缓冲区
4)每次读入缓冲区后,将缓冲区数据通过输出流通道的write()方法写入到2.txt
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel03 {
public static void main(String[] args) throws IOException {
//创建文件输入流对象
File f1 = new File("1.txt");
FileInputStream fileInputStream = new FileInputStream(f1);
//获取对应的FileChannel
FileChannel inputStreamChannel = fileInputStream.getChannel();
//创建文件输出流对象以及对应的FileChannel
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
FileChannel outputStreamChannel = fileOutputStream.getChannel();
//创建缓冲区ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate((int) f1.length());
while(true){
//循环读取文件内容,输入到缓冲区
byteBuffer.clear(); //清空buffer 重要
int read = inputStreamChannel.read(byteBuffer);
System.out.println("read = "+read);
if (read == -1){ //表示读取完毕
break;
}
//将buffer中的数据写入到outputStreamChannel中 -> 2.txt
byteBuffer.flip();
outputStreamChannel.write(byteBuffer);
}
//关闭流
fileInputStream.close();
fileOutputStream.close();
}
}
【注】1)每次进行循环读入前,必须调用ByteBuffer.clear()方法,将所有标志位重置。否则,若待读取文件不能一次读入缓冲区时,第二 次开始,limit和position的值一直相等,会导致读取内容为0。
2)将Buffer中的数据写入通道前,需要调用flip()方法将缓冲区反转。
2-4通道应用案例4 使用transferFrom方法拷贝文件
要求:1)使用FileChannel和方法transferFrom,完成文件拷贝
2)拷贝一张图片
设计思路:1)新建输入流和输出流,并分别获取对应的通道
2)调用通道的transferFrom()方法,将输入通道中的数据复制到输出通道中
import java.io.*;
import java.nio.channels.FileChannel;
public class NIOFlieChannel04 {
public static void main(String[] args) throws IOException {
File f1 = new File("1.jpg");
FileInputStream fileInputStream = new FileInputStream(f1);
FileChannel inputStreamChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("2.jpg");
FileChannel outputStreamChannel = fileOutputStream.getChannel();
outputStreamChannel.transferFrom(inputStreamChannel,0,f1.length());
fileInputStream.close();
fileOutputStream.close();
}
}
3、关于Buffer和Channel的注意事项和细节
- ByteBuffer支持类型化的put和get,put放入什么数据类型,get就应该使用相应的数据类型来取出,否则可能会产生ByteUnderflowException异常。
- 可以将一个普通的Buffer转换为只读的Buffer:asReadOnlyBuffer()方法。
【注】此时,向只读buffer中放入数据会产生异常。import java.nio.ByteBuffer; public class ReadOnlyBuffer { public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(64); for (int i = 0; i < 64; i++){ buffer.put((byte) i); } buffer.flip(); //得到一个只读的buffer ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer(); System.out.println(readOnlyBuffer.getClass()); while (readOnlyBuffer.hasRemaining()){ System.out.println(readOnlyBuffer.get()); } readOnlyBuffer.put((byte) 122); } }
- NIO提供了MapperByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,而如何同步到文件由NIO来完成。
import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; /* * 说明 * 1、MappedByteBuffer可以直接让文件在内存(堆外内存)中修改,操作系统不需要拷贝一次 * * */ public class MappedByteBufferTest { public static void main(String[] args) throws IOException { RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw"); //获取对应的文件通道 FileChannel channel = randomAccessFile.getChannel(); MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); /** * 参数1:FileChannel.MapMode.READ_WRITE 使用的是读写模式 * 参数2:0 可以直接修改的起始位置 * 参数3:5 映射到内存的大小(不是索引位置),即将文件的多少个字节映射到内存中 * 可以直接修改的范围是0~5 * 实际类型是 DirectByteBuffer * */ mappedByteBuffer.put(0, (byte) 'H'); mappedByteBuffer.put(3, (byte) '9'); mappedByteBuffer.put(5, (byte) 'D'); randomAccessFile.close(); } }
- NIO还支持通过多个Buffer(即Buffer数组)完成读写操作,即Scattering(分散)和Gathering(聚集)。
Scattering(分散):在向缓冲区写入数据时,可以使用buffer数组依次写入,一个buffer数组写满后,继续写入下一个buffer数组。
Gathering(聚集):从缓冲区读取数据时,可以依次读取,读完一个buffer再按顺序读取下一个。
import java.io.IOException;
import java.lang.reflect.Array;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
/**
* scattering[分散]:将数据写入到buffer时,可以采用buffer数组,依次写入
* 一个buffer满了,写入下一个buffer
* gathering[聚集]:从buffer中读数据时,可以采用buffer数组,依次读取
* */
public class ScatteringANDGathering {
public static void main(String[] args) throws IOException {
//使用ServerSocketChannel和SocketChannel 网络
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
//绑定端口到socket并启动
serverSocketChannel.socket().bind(inetSocketAddress);
//创建buffer数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(5);
byteBuffers[1] = ByteBuffer.allocate(3);
//等待客户端连接(telnet)
SocketChannel socketChannel = serverSocketChannel.accept();
int messageLength = 8; //假定从客户端接收8个字节
//循环读取
while (true){
int byteRead = 0;
while (byteRead < messageLength ){
long l = socketChannel.read(byteBuffers);
byteRead += l; //累计读取到在字节数
System.out.println("byte = "+byteRead);
//使用流打印,输出当前buffer的position和limit
Arrays.asList(byteBuffers).stream().map(buffer ->
"position="+buffer.position()+",limit="+buffer.limit()).forEach(System.out::println);
}
//将所有的buffer进行一次反转
Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
//将数据读出显示到客户端
long byteWrite = 0;
while (byteWrite < messageLength){
long l = socketChannel.write(byteBuffers);//
byteWrite += l;
}
//将所有的buffer进行复位(clear)
Arrays.asList(byteBuffers).forEach(buffer -> {
buffer.clear();
});
System.out.println("byteRead = "+ byteRead + " byteWrite = "+ byteWrite + "messageLength = "
+ messageLength);
}
}
}