一、Java NIO 简介
1.1、Java NIO
- Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
二、Java NIO 与IO 的主要区别
2.1、Java NIO 与IO 的主要区别
IO | NIO |
---|---|
面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞IO(NonBlocking IO) |
(无) | 选择器(Selectors) |
三、缓冲区(Buffer)和通道(Channel)
3.1、缓冲区和通道
- Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到IO 设备(例如:文件、套接字)的连接。若需要使用NIO 系统,需要获取用于连接IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
- 简而言之,Channel 负责传输,Buffer 负责存储。
3.2、缓冲区(Buffer)
3.2.1. 缓冲区(Buffer)
- 一个用于特定基本数据类型的容器。由java.nio 包定义的,所有缓冲区都是Buffer 抽象类的子类。
- Java NIO中的Buffer 主要用于与NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
- Buffer 就像一个数组,可以保存多个相同类型的数据。根据数据类型不同(boolean 除外) ,有以下Buffer 常用子类:
a.ByteBuffer
b.CharBuffer
c.ShortBuffer
d.IntBuffer
e.LongBuffer
f.FloatBuffer
g.DoubleBuffer
上述Buffer 类他们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个Buffer 对象:static XxxBuffer allocate(int capacity) : 创建一个容量为capacity 的XxxBuffer 对象。
3.2.2. 缓冲区的基本属性:
- 容量(capacity) :表示Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
- 限制(limit):第一个不应该读取或写入的数据的索引,即位于limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
- 位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制。
- 标记(mark)与重置(reset):标记是一个索引,通过Buffer 中的mark() 方法指定Buffer 中一个特定的position,之后可以通过调用reset() 方法恢复到这个position。
- 标记、位置、限制、容量遵守以下不变式:0<=mark<=position<=limit<=capacity。
3.2.3. Buffer 的常用方法:
方法 | 描述 |
---|---|
Buffer clear() | 清空缓冲区并返回对缓冲区的引用 |
Buffer flip() | 将缓冲区的界限设置为当前位置,并将当前位置充值为0 |
int capacity() | 返回Buffer 的capacity大小 |
boolean hasRemaining() | 清判断缓冲区中是否还有元素 |
int limit() | 返回Buffer 的界限(limit) 的位置 |
Bufferlimit(int n) | 将设置缓冲区界限为n, 并返回一个具有新limit 的缓冲区对象 |
Buffer mark() | 对缓冲区设置标记 |
int position() | 返回缓冲区的当前位置position |
Buffer position(int n) | 将设置缓冲区的当前位置为n , 并返回修改后的Buffer 对象 |
int remaining() | 返回position 和limit 之间的元素个数 |
Buffer reset() | 将位置position 转到以前设置的mark 所在的位置 |
Buffer rewind() | 将位置设为为0,取消设置的mark |
3.2.4.缓冲区的数据操作
- Buffer 所有子类提供了两个用于数据操作的方法:get() 与put() 方法.
- 获取Buffer 中的数据:
- get() :读取单个字节。
- get(byte[] dst):批量读取多个字节到dst 中。
- get(int index):读取指定索引位置的字节(不会移动position)。
- 放入数据到Buffer 中:
- put(byte b):将给定单个字节写入缓冲区的当前位置。
- put(byte[] src):将src 中的字节写入缓冲区的当前位置。
- put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动position)。
3.2.5.直接与非直接缓冲区
- 字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则Java 虚拟机会尽最大努力直接在此缓冲区上执行本机I/O 操作。也就是说,在每次调用基础操作系统的一个本机I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
- 直接字节缓冲区可以通过调用此类的allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
- 直接字节缓冲区还可以通过FileChannel 的map() 方法将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer。Java 平台的实现有助于通过JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
- 字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其isDirect()方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理。
3.2.6.通道(Channel)
- 通道(Channel):由java.nio.channels 包定义的。Channel 表示IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过Channel 本身不能直接访问数据,Channel 只能与Buffer 进行交互。
- Java 为Channel 接口提供的最主要实现类如下:
- FileChannel:用于读取、写入、映射和操作文件的通道。
- DatagramChannel:通过UDP 读写网络中的数据通道。
- SocketChannel:通过TCP 读写网络中的数据。
- ServerSocketChannel:可以监听新进来的TCP 连接,对每一个新进来的连接都会创建一个SocketChannel。
3.2.7.获取通道
- 获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下:
- FileInputStream
- FileOutputStream
- RandomAccessFile
- DatagramSocket
- Socket
- ServerSocket
获取通道的其他方式是使用Files 类的静态方法newByteChannel() 获取字节通道。或者通过通道的静态方法open() 打开并返回指定通道。
3.2.8.通道的数据传输
- 将Buffer 中数据写入Channel
例如:
将buffer中的数据写入到通道中:
int bytesWriten = inChannel.write(buf);
- 从Channel 读取数据到Buffer
int bytesRead = inChannel.read(buf);
3.2.9.实例
- buffer中一些方法的演示:
@Test
public void test1(){
java.lang.String str = "abcdefg";
//1.分配一个指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println("-----------allocate-----------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//2.利用put()存入数据到缓冲区
buf.put(str.getBytes());
System.out.println("-----------put-----------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//3.filp():切换成读取数据的模式
buf.flip();
System.out.println("-----------flip-----------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//4.利用get()读取缓冲区中的数据
byte[] dst = new byte[buf.limit()];
buf.get(dst);
System.out.println(new String(dst,0,dst.length));
System.out.println("-----------get-----------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//5.rewind():可重复读数据
buf.rewind();
System.out.println("-----------rewind-----------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//6.clear():清空缓冲区,但是缓冲区中的数据依然存在,但是处于"被遗忘"状态。
buf.clear();
System.out.println("-----------clear-----------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
System.out.println((char) buf.get());
}
运行结果:
2.
@Test
public void test2(){
String str = "abcde";
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put(str.getBytes());
buf.flip();
byte[] dst = new byte[buf.limit()];
buf.get(dst,0,2);
System.out.println(new String(dst,0,2));
System.out.println(buf.position());
//mark:标记
buf.mark();
buf.get(dst,2,2);
System.out.println(new String(dst,2,2));
System.out.println(buf.position());
//reset():恢复到mark位置
buf.reset();
System.out.println(buf.position());
//判断缓冲区中是否还有剩余数据
if(buf.hasRemaining()){
//获取缓冲区中可以操作的剩余数据
System.out.println(buf.remaining());
}
}
运行结果:
3. 使用非直接缓冲区实现对文件的复制
@Test
public void test1(){
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel inChannel = null;
FileChannel outChanel = null;
try {
//1.利用通道完成文件的复制(非直接缓冲区)
fis = new FileInputStream("1.jpg");
fos = new FileOutputStream("2.jpg");
//2.获取通道
inChannel = fis.getChannel();
outChanel = fos.getChannel();
//3.分配一个指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//4.将通道中的数据存入到缓冲区中
while (inChannel.read(buf)!=-1){
//切换成读取数据的模式
buf.flip();
//5.将通道中的数据写入到通道中
outChanel.write(buf);
//清空缓冲区
buf.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//资源的关闭
try {
outChanel.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:在module目录下会生成一个与1.jpg相同的名字为2.jpg的图片。
4. 使用直接缓冲区完成文件的复制(内存映射文件)
@Test
public void test2(){
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
outChannel = FileChannel.open(Paths.get("3.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW);
//内存映射文件
MappedByteBuffer inMapedBuf = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedBuf = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
//直接对缓冲区进行数据的读写
byte[] buf = new byte[1024];
inMapedBuf.get(buf);
outMappedBuf.put(buf);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:在module目录下会生成一个与1.jpg相同的名字为3.jpg的图片。
3.2.10.分散(Scatter)和聚集(Gather)
- 聚集写入(Gathering Writes)是指将多个Buffer 中的数据“聚集”到Channel。
- 将数据从源通道传输到其他Channel 中:
@Test
public void test3(){
RandomAccessFile raf1 = null;
FileChannel channel1 = null;
try {
//分散和聚集
raf1 = new RandomAccessFile("1.txt", "rw");
//1、获取通道
channel1 = raf1.getChannel();
//2.分配指定大小的缓冲区
ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
//3.分散读取
ByteBuffer[] bufs = {buf1,buf2};
channel1.read(bufs);
for (ByteBuffer byteBuffer:bufs){
//切换成读模式
byteBuffer.flip();
}
System.out.println(new String(bufs[0].array(),0,bufs[0].limit()));
System.out.println("=========================");
System.out.println(new String(bufs[1].array(),0,bufs[1].limit()));
//4.聚集写入
RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = raf2.getChannel();
channel2.write(bufs);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
raf1.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
channel1.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:在module目录下会生成一个与1.txt相同的名字为2.txt的文本。
四、文件通道(FileChannel)
4.1.FileChannel 的常用方法
方法 | 描述 |
---|---|
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 p) | 设置此通道的文件位置 |
long size() | 返回此通道的文件的当前大小 |
FileChannel truncate(long s) | 将此通道的文件截取为给定大小 |
void force (boolean metaData) | 强制将所有对此通道的文件更新写入到存储设备中 |
4.2.实例
使用FileChannel实现服务器端和客户端之间的交互,并且带有反馈
//客户端
@Test
public void client() throws IOException{
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
ByteBuffer buf = ByteBuffer.allocate(1024);
while(inChannel.read(buf) != -1){
buf.flip();
sChannel.write(buf);
buf.clear();
}
sChannel.shutdownOutput();
//接收服务端的反馈
int len = 0;
while((len = sChannel.read(buf)) != -1){
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
inChannel.close();
sChannel.close();
}
//服务端
@Test
public void server() throws IOException{
ServerSocketChannel ssChannel = ServerSocketChannel.open();
FileChannel outChannel = FileChannel.open(Paths.get("6.jpg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
ssChannel.bind(new InetSocketAddress(9898));
SocketChannel sChannel = ssChannel.accept();
ByteBuffer buf = ByteBuffer.allocate(1024);
while(sChannel.read(buf) != -1){
buf.flip();
outChannel.write(buf);
buf.clear();
}
//发送反馈给客户端
buf.put("服务端接收数据成功".getBytes());
buf.flip();
sChannel.write(buf);
sChannel.close();
outChannel.close();
ssChannel.close();
}
五、NIO 的非阻塞式网络通信
5.1、选择器(Selector)
5.1.1.Selector选择器
- 选择器(Selector)是SelectableChannle 对象的多路复用器,Selector 可以同时监控多个SelectableChannel 的IO 状况,也就是说,利用Selector 可使一个单独的线程管理多个Channel。Selector 是非阻塞IO 的核心。
- 当调用register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数ops 指定。
- 可以监听的事件类型(可使用SelectionKey 的四个常量表示):
a. 读: SelectionKey.OP_READ (1)
b.写: SelectionKey.OP_WRITE (4)
c.连接: SelectionKey.OP_CONNECT(8)
d.接收: SelectionKey.OP_ACCEPT (16) - 创建选择器Selector时,需要使用Selector.open()方法:
//创建选择器
Selector selector = Selector.open();
5.向选择器注册通道,使用SelectableChannel.register(Selector sel, int ops)方法:
//创建一个Socket套接字
Socket socket = new Socket(InetAdress.getByname("127.0.0.01"),9898);
//获取SocketChannel
SocketChannel channel = socket.getChannel();
//创建选择器
Selector selector = Selector.open();
//设置为非阻塞模式
selector.configureBlocking(false);
//向Seelctor注册Channel
channel .register(selector, SelectionKey.OP_ACCEPT);
若注册时不止监听一个事件,则可以使用“位或”操作符连接.
//注册监听事件
int len = SelectionKey.OP_ACCEPT | SelectionKey.OP_ACCEPT;
5.1.2.SelectionKey
- SelectionKey:表示SelectableChannel 和Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。
- 方法:
方法 | 描述 |
---|---|
int interestOps() | 获取感兴趣事件集合 |
int readyOps() | 获取通道已经准备就绪的操作的集合 |
SelectableChannel channel() | 获取注册通道 |
Selector selector() | 返回选择器 |
boolean isReadable() | 检测Channal 中读事件是否就绪 |
boolean isWritable() | 检测Channal 中写事件是否就绪 |
booleanisConnectable() | 检测Channel 中连接是否就绪 |
booleanisAcceptable() | 检测Channel 中接收是否就绪 |
5.1.3.实例
类似于聊天室的将数据发送给服务器端,然后反馈回来,带有时间和内容:
//客户端
@Test
public void client() throws IOException{
//1. 获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
//2. 切换非阻塞模式
sChannel.configureBlocking(false);
//3. 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//4. 发送数据给服务端
Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
String str = scan.next();
buf.put((new Date().toString() + "\n" + str).getBytes());
buf.flip();
sChannel.write(buf);
buf.clear();
}
//5. 关闭通道
sChannel.close();
}
//服务端
@Test
public void server() throws IOException{
//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
//6. 轮询式的获取选择器上已经“准备就绪”的事件
while(selector.select() > 0){
//7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext()){
//8. 获取准备“就绪”的是事件
SelectionKey sk = it.next();
//9. 判断具体是什么事件准备就绪
if(sk.isAcceptable()){
//10. 若“接收就绪”,获取客户端连接
SocketChannel sChannel = ssChannel.accept();
//11. 切换非阻塞模式
sChannel.configureBlocking(false);
//12. 将该通道注册到选择器上
sChannel.register(selector, SelectionKey.OP_READ);
}else if(sk.isReadable()){
//13. 获取当前选择器上“读就绪”状态的通道
SocketChannel sChannel = (SocketChannel) sk.channel();
//14. 读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while((len = sChannel.read(buf)) > 0 ){
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
}
//15. 取消选择键 SelectionKey
it.remove();
}
}
}
六、管道(Pipe)
6.1.管道
- Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
6.2.向管道中写入数据和读取数据
Pipe.SinkChannel sinkChannel = null;
Pipe.SourceChannel sourceChannel = null;
try {
//1.获取管道
Pipe pipe = Pipe.open();
//2.将缓冲区中的数据写入管道
ByteBuffer buf = ByteBuffer.allocate(1024);
sinkChannel = pipe.sink();
buf.put("通过单向管道发送数据".getBytes());
buf.flip();
sinkChannel.write(buf);
//读取缓冲区中的数据
sourceChannel = pipe.source();
buf.flip();
int len = sourceChannel.read(buf);
System.out.println(new String(buf.array(),0,len));
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
sourceChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
sinkChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果: