概述
之前学习的IO都叫做BIO阻塞IO.
NIO是非阻塞IO流。能够在高并发的情况下提高读写效率。
NIO分为三大知识点Buffer缓冲区、Channel通道、Selector选择器
一、Buffer缓冲区
1、概述
Buffer是缓冲区数组,用来代替之前的普通数组。
分类
ByteBuffer 用来代替之前的byte[]
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
2、ByteBuffer的创建方式
代码演示
//在堆中创建缓冲区:allocate(int capacity)*
ByteBuffer buffer1 = ByteBuffer.allocate(1024);
//在系统内存创建缓冲区:allocateDirect(int capacity)
ByteBuffer buffer2 = ByteBuffer.allocateDirect(1024);
//通过普通数组创建缓冲区:wrap(byte[] arr)
byte[] arr = new byte[1024];
ByteBuffer buffer3 = ByteBuffer.wrap(arr);
3、常用方法
put(byte b) : 给数组添加元素
//ByteBuffer的方法
//创建长度为10的缓冲区数组
ByteBuffer b = ByteBuffer.allocate(10);
//- put(byte b) : 给数组添加元素
//添加元素(默认从头往后逐个添加)
b.put((byte)10);
b.put((byte)20);
b.put((byte)30);
//添加数组
byte[] arr = {11,22,33};
b.put(arr);
//把缓冲区数组变成普通数组打印内容
System.out.println(Arrays.toString(b.array()));
//[10, 20, 30, 11, 22, 33, 0, 0, 0, 0]
capacity() :获取容量,容量是一个定值。
//- capacity() :获取容量,容量是一个定值。数组一旦创建长度不能改变,长度就是数组的容量
int capacity = b.capacity();
System.out.println("容量是" + capacity);
limit() : 限制。limit可以指定一个索引,从limit开始后面的位置不能操作。
//ByteBuffer的方法
//创建长度为10的缓冲区数组
ByteBuffer b = ByteBuffer.allocate(10);
//limit() :限制.
//获取当前限制
int limit = b.limit();
System.out.println("默认的limit:" + limit); //默认情况下limit=capacity
//修改限制为3(从3索引开始后面的位置就不能被操作了)
b.limit(3);
//添加元素
b.put((byte)10);
b.put((byte)20);
b.put((byte)30);
b.put((byte)40); //操作了3索引就报错了
position() :位置。位置代表将要存放的元素的索引,每次添加元素position会往后移动。位置不能小于0,也不能大于limit。position只会自动完后走,不会自动往前走。
//ByteBuffer的方法
//创建长度为10的缓冲区数组
ByteBuffer b = ByteBuffer.allocate(10);
//position() :位置。位置默认是0 每次存放元素位置就会向后移动
//获取当前位置
int position = b.position();
System.out.println("当前的位置" + position);
//添加元素
b.put((byte)10);
b.put((byte)20);
//修改位置为4
b.position(4);
//添加元素
b.put((byte)30);
//打印
System.out.println(Arrays.toString(b.array()));
//[10, 20, 0, 0, 30, 0, 0, 0, 0, 0]
mark() : 标记。当调用缓冲区的reset()重置方法时,会将缓冲区的position重置为mark的位置。
//ByteBuffer的方法
//创建长度为10的缓冲区数组
ByteBuffer b = ByteBuffer.allocate(10);
//mark() :标记 reset():重置
//默认情况下缓冲区是没有标记的
b.put((byte)10);
b.put((byte)20);
//记录标记(标记当前位置)
b.mark();
b.put((byte)30);
b.put((byte)40);
//重置
b.reset();
b.put((byte)50);
//打印
System.out.println(Arrays.toString(b.array()));
//[10, 20, 50, 40, 0, 0, 0, 0, 0, 0]
clear():还原缓冲区的各个指针。 只移动指针不会清空数据。
-
将position设置为初始状态
-
将限制limit设置为初始状态
-
丢弃标记mark
//ByteBuffer的方法
//创建长度为10的缓冲区数组
ByteBuffer b = ByteBuffer.allocate(10);
//clear() :还原指针的状态,不清空数据
b.put((byte)10);
b.put((byte)20);
//设置限制
b.limit(4);
System.out.println(b); //[pos=2 lim=4 cap=10]
//还原
b.clear();
System.out.println(b); //[pos=0 lim=10 cap=10]
//打印数组的内容
System.out.println(Arrays.toString(b.array()));
//[10, 20, 0, 0, 0, 0, 0, 0, 0, 0]数据还在
flip():切换读写状态。在读写数据之间要调用这个方法。
-
将limit设置为当前position位置
-
将当前position设置为初始位置
-
丢弃mark标记
//ByteBuffer的方法
//创建长度为10的缓冲区数组
ByteBuffer b = ByteBuffer.allocate(10);
//flip() :读写切换方法
b.put((byte)10);
b.put((byte)20);
b.put((byte)30);
System.out.println(b); //[pos=3 lim=10 cap=10]
//切换
b.flip();
System.out.println(b); //[pos=0 lim=3 cap=10]
2、Channel通道
1、概述
通道相当于之前的IO流,通道可以读写数据,通道不区分输入和输出的方向,通道类型既可以读也可以写。
2、分类
- FileChannel:从文件读取数据的
- DatagramChannel:读写UDP网络协议数据
- SocketChannel:读写TCP网络协议数据
- ServerSocketChannel:可以监听TCP连接
3、FileChannel基本使用
使用FileChannel完成文件的复制
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class Test02_FileChannel完成文件复制 {
public static void main(String[] args) throws Exception {
//创建输入流
FileInputStream fis = new FileInputStream("C:\\Users\\jin\\Desktop\\timg.jpg");
//创建输出流
FileOutputStream fos = new FileOutputStream("day13\\复制.jpg");
//通过IO流获取Channel通道对象
FileChannel c1 = fis.getChannel();
FileChannel c2 = fos.getChannel();
//在通道中使用的数组是缓冲区数组
ByteBuffer buffer = ByteBuffer.allocate(1024);
//循环
while(c1.read(buffer) != -1){
//切换
buffer.flip();
//写出
c2.write(buffer);
//清空
buffer.clear();
}
//关闭资源
fos.close();
fis.close();
}
}
4、FileChannel结合MappedByteBuffer实现读写
MappedByteBuffer是ByteBuffer的子类。他也是一个缓冲数组。能够把硬盘中的数据一次映射到内存中。
可以减少硬盘和内存之间的IO次数,能够提高效率。
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
public class Test03_高效复制 {
public static void main(String[] args) throws IOException {
//指向源文件
//r代表read 是对这个文件进行只读操作
RandomAccessFile f1 = new RandomAccessFile("C:\\Users\\jin\\Desktop\\timg.jpg","r");
//指向目标文件
//rw代表readwrite
RandomAccessFile f2 = new RandomAccessFile("day13\\复制.jpg","rw");
//获取通道
FileChannel c1 = f1.getChannel();
FileChannel c2 = f2.getChannel();
//获取源文件大小
long size = c1.size();
//获取映射数组
//对源文件进行只读操作,从文件的0位置开始读取size个字节
MappedByteBuffer map1 = c1.map(FileChannel.MapMode.READ_ONLY, 0, size);
MappedByteBuffer map2 = c2.map(FileChannel.MapMode.READ_WRITE, 0, size);
//把map1中的数据移动到map2中
for(int i=0; i < size; i++){
//从数组map1中获取字节
byte b = map1.get();
//把字节存放到数组map2中
map2.put(b);
}
//关闭资源
f1.close();
f2.close();
}
}
5、网络编程收发信息
客户端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Test客户端 {
public static void main(String[] args) throws IOException {
//创建客户端对象
SocketChannel s = SocketChannel.open();
//指定连接的服务器
s.connect(new InetSocketAddress("192.168.171.32",8888));
//缓冲区数组
ByteBuffer buffer = ByteBuffer.allocate(1024);
//把数据放在缓冲区数组
buffer.put("你好呀~".getBytes());
//切换(如果不切换的话,会输出数组中后的空格反而没有数据)
buffer.flip();
//调用输出方法把数组中的数据发出去
s.write(buffer);
//关流
s.close();
}
}
服务器端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class Test服务端 {
public static void main(String[] args) throws IOException {
//创建服务端对象
ServerSocketChannel ssc = ServerSocketChannel.open();
//绑定端口号
ssc.bind(new InetSocketAddress(8888));
//等待客户端连接
SocketChannel s = ssc.accept();
//准备数组
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取数据
int len = s.read(buffer);
//打印数据
System.out.println(new String(buffer.array(),0,len));
//服务器不需要关闭资源
}
}
6、accept阻塞问题
服务器端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class Test服务端非阻塞 {
public static void main(String[] args) throws IOException, InterruptedException {
//创建服务端对象
ServerSocketChannel ssc = ServerSocketChannel.open();
//绑定端口号
ssc.bind(new InetSocketAddress(8888));
//设置非阻塞
ssc.configureBlocking(false);
//循环查看有没有客户来连接
while(true) {
//等待客户端连接
SocketChannel s = ssc.accept();
//判断有没有客户端的连接
if(s != null) {
//准备数组
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取数据
int len = s.read(buffer);
//打印数据
System.out.println(new String(buffer.array(), 0, len));
//非阻塞
break;
}else{
//没有客户端的连接
System.out.println("服务器执行别的业务代码");
Thread.sleep(3000);
}
}
//服务器不需要关闭资源
}
}
三、Selector选择器
1、多路复用的概念
之前的代码一个端口需要一个新的线程,如果端口很多导致程序效率低。
多路复用的意思是把多个服务器端口交给一个选择器对象去管理,提高效率。
2、Selector介绍
Selector是一个选择器,可以用一个线程处理多个事件。可以注册到多个Channel上,帮多个Channel去处理事件。用一个线程处理了之前多个线程的事务,就给系统减轻负担,提高效率。
select() :选择器连接客户端的方法
阻塞问题:
在没有客户端连接的情况下是方法阻塞的。
在有客户端连接且客户端没有被处理时的情况下方法是非阻塞的。
在有客户端连接且客户端已经被梳理时的情况下方法又变回阻塞的。
selectedKeys() :返回一个集合,集合里面装的是被访问过的服务器。
3、Selector方法的演示
import com.sun.source.doctree.StartElementTree;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Set;
public class Test服务器 {
public static void main(String[] args) throws IOException {
//创建服务器对象
ServerSocketChannel ssc1 = ServerSocketChannel.open();
//绑定端口
ssc1.bind(new InetSocketAddress(7777));
//设置非阻塞(Selector的使用必须让服务器非阻塞)
ssc1.configureBlocking(false);
//创建服务器对象
ServerSocketChannel ssc2 = ServerSocketChannel.open();
//绑定端口
ssc2.bind(new InetSocketAddress(8888));
//设置非阻塞
ssc2.configureBlocking(false);
//创建选择器对象
Selector s = Selector.open();
//把选择器注册到服务器上,选择器就可以管理服务器(OP_ACCEPT表示让选择器去管理accept这个方法)
ssc1.register(s, SelectionKey.OP_ACCEPT);
ssc2.register(s, SelectionKey.OP_ACCEPT);
//获取集合
//选择器会把被访问的服务器对象放在这个集合中
Set<SelectionKey> set = s.selectedKeys();
System.out.println("集合中的对象个数是:" + set.size()); // 个数 0
while (true) {
//选择器连接客户端的方法
s.select();
System.out.println("集合中的对象个数是:" + set.size());
//连接了几个服务器个数就是几
}
}
}
4、Selector管理多个ServerSocketChannel的问题
在客户端访问服务器的时候,选择器会把服务器对象放在Set集合中,但是在使用完之后没有把服务器从集合中取出。下次在连接别的服务器的时候,已经被使用过的服务器对象会出现空指针异常。
解决办法:在没次用完一个服务器之后就从集合中删除。不能使用集合的删除方法,因为会有并发修改异常,必须使用迭代器的删除方法。
import com.sun.source.doctree.StartElementTree;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class Test服务器 {
public static void main(String[] args) throws IOException, InterruptedException {
//创建服务器对象
ServerSocketChannel ssc1 = ServerSocketChannel.open();
//绑定端口
ssc1.bind(new InetSocketAddress(7777));
//设置非阻塞(Selector的使用必须让服务器非阻塞)
ssc1.configureBlocking(false);
//创建服务器对象
ServerSocketChannel ssc2 = ServerSocketChannel.open();
//绑定端口
ssc2.bind(new InetSocketAddress(8888));
//设置非阻塞
ssc2.configureBlocking(false);
System.out.println("ssc1" + ssc1);
System.out.println("ssc2" + ssc2);
//创建选择器对象
Selector s = Selector.open();
//把选择器注册到服务器上,选择器就可以管理服务器(OP_ACCEPT表示让选择器去管理accept这个方法)
ssc1.register(s, SelectionKey.OP_ACCEPT);
ssc2.register(s, SelectionKey.OP_ACCEPT);
//获取集合
//选择器会把被访问的服务器对象放在这个集合中
Set<SelectionKey> set = s.selectedKeys();
System.out.println("集合中的对象个数是:" + set.size());
while (true) {
System.out.println("集合中的对象个数是:" + set.size());
//选择器连接客户端的方法
s.select();
//遍历集合获取集合中的服务器对象
//for (SelectionKey key : set) {
//使用迭代器
Iterator<SelectionKey> it = set.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
//获取里面的通道对象
SelectableChannel channel = key.channel();
//ServerSocketChannel --> AbstractSelectableChannel --> SelectableChannel
//向下转型
ServerSocketChannel ssc = (ServerSocketChannel) channel;
//使用服务端对象接受客户端数据
//获取客户端对象
SocketChannel sc = ssc.accept();
//准备数组
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取数据
int len = sc.read(buffer);
//打印数据
System.out.println(new String(buffer.array(),0,len));
//集合删除元素
//set.remove(key);
//使用迭代器删除当前元素
it.remove();
}
//使用服务器对象接受数据
}
}
}