文章目录
一、什么是NIO
NIO是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。在java中的全称为java non-blocking IO,它是在是从 Java 1.4 版本开始引入的一个新的IO流的API。NIO 支持面向缓冲区的、基于通道的 IO 操作。
NIO主要有Channel(通道),Buffer(缓冲区), Selector(选择器)组成,并且和原来的IO不同,它是非阻塞模式,就是在某一通道里面在运行的过程中不会因为结果没有出现而产生堵塞的效果,它会继续执行其他的任务,这样会极大的提高运行效率。
二、缓冲区(buffer)
buffer的类别
在NIO中Buffer则是顶级父类,所有的buffer都是继承于它,并且除了布尔类型的数据类型没有其实现,其他都有其相关的实现类。
类 | 作用 |
---|---|
ByteBuffer | 用于存储字节数据的缓冲区 |
LongBuffer | 用于存储长整型数据的缓冲区 |
DoubleBuffer | 用于存储双精度小数数据的缓冲区 |
FloatBuffer | 用于存储单精度小数数据的缓冲区 |
IntBuffer | 用于存储整数数据的缓冲区 |
CharBuffer | 用于存储字符数据的缓冲区 |
ShortBuffer | 用于存储短整数数据的缓冲区 |
MappedByteBuffer | 文件映射到虚拟内存 |
上图中所展现的各种的Buffer类其方法都相似,只是区分了数据类型而已。但是这些buffer类都是被abstract关键字修饰的抽象类,想要创建buffer则需要调用buffer类里面的allocate()方法才能创建除对象。
缓冲区也分直接缓冲区和非直接缓冲区。两者的不同则体现在一个是存放在堆内存里面的,一个则是在虚拟机内存之外开辟内存。非直接缓冲区所开辟的堆内存如果要进行IO操作,则有一个复制到直接内存的过程,而直接缓冲区则没有这一过程,所以直接内存的效率则会更快。但直接内存由于不是在虚拟机里面创建的,所以销毁和创建起来也要比非直接内存所开销的要大。一般要想判断是否为直接缓冲区可以使用isDirect()方法。
但是要想操作buffer,则需要先了解容量 (capacity),限制 (limit),位置 (position),标记 (mark),**重置 (reset)**这几个概念:
buffer属性 | 描述 |
---|---|
capacity | 容量,初始化容量,缓冲区容量不能为负,并且创建后不能更改。 |
limit | 表示缓冲区中可以操作数据的大小 。缓冲区的限制不能为负,不能大于capacity。 写入模式,限制等于 buffer的容量。读取模式下,limit等于写入的数据量。 |
Position | 从0开始读写,读写一次就会移动一个位置,但是这个位置是不会超越Limit的 |
Mark | 标记,只要调用则标记当前的Position位置 |
标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity
buffer的常用方法
方法 | 作用 |
---|---|
public final int capacity() | 返回缓冲区容量 |
public final int position() | 返回缓冲区当前的位置 |
public final int limit() | 返回缓冲区的限制位置 |
public Buffer mark() | 设置position当前位置为标记 |
public Buffer reset() | 返回到标记的位置 |
public Buffer clear() | 清除缓冲区,这里其实只是把缓冲区所有标注位设为初始位置,写数据覆盖旧的并不是直接清空旧缓冲区 |
public Buffer flip() | 读和写的模式切换 |
public Buffer rewind() | 将位置设为为 0, 取消设置的 mark标记 |
public final int remaining() | 返回 position 和 limit 之间的元素个数 |
public final boolean hasRemaining() | 判断缓冲区中是否还存在元素 |
public abstract boolean isReadOnly() | 判断缓冲区是否是只读 |
操作buffer的方法
方法 | 作用 |
---|---|
get() | 读取单个字节 |
get(byte[] dst) | 批量读取多个字节到 dst 中 |
get(int index) | 读取指定索引位置的字节放到入数据到Buffer中,但不会移动 position的位置 |
put(byte b) | 将给定单个字节写入缓冲区的当前位置 |
put(byte[] src) | 将 src 中的字节写入缓冲区的当前位置 |
put(int index, byte b) | 将指定字节写入缓冲区的索引 位置,同样不会移动 position |
put()添加数据
flip()读写反转过后
buffer一般操作步骤
1.创建buffer,并且写入数据
2.调用flip(),写模式切换成读模式
3.get读取数据
4.调用clear()方法清除缓冲区,或者compact()把未读数据提前清除已读数据。
byte[] s = "qwertyuiop".getBytes(StandardCharsets.UTF_8);
ByteBuffer allocate = ByteBuffer.allocate(100);//分配指定大小的缓冲区
System.out.println("-----------------put---------------");
allocate.put(s);//添加到缓冲区
System.out.println("position="+allocate.position());//当前的位置
System.out.println("limit="+allocate.limit());//界限的位置
System.out.println("capacity="+allocate.capacity());//容量的大小
System.out.println("-----------------mark---------------");
allocate.mark();//标记位置
allocate.put(new byte[]{1, 2, 3, 4});
System.out.println("position="+allocate.position());
System.out.println("limit="+allocate.limit());
System.out.println("capacity="+allocate.capacity());
System.out.println("-----------------reset---------------");
allocate.reset();
System.out.println("position="+allocate.position());
System.out.println("limit="+allocate.limit());
System.out.println("capacity="+allocate.capacity());
System.out.println("-----------------get---------------");
allocate.flip();
byte[] bytes=new byte[allocate.limit()];
allocate.get(bytes,allocate.position(),allocate.limit());//读取完buffer里面全部的数据到byte里面
System.out.println("position="+allocate.position());
System.out.println("limit="+allocate.limit());
System.out.println("capacity="+allocate.capacity());
for (int i = 0; i < bytes.length; i++) {
System.out.printf("bytes[%d]=%d\n",i,bytes[i]);
}
System.out.println("-----------------compact---------------");
allocate.clear();
allocate.put(new byte[]{1,2,3,4,5,6,7,8,9});
allocate.flip();
byte[] byte1=new byte[6];
if (allocate.hasRemaining()){//判断buffer里面是否还有数据
allocate.get(byte1,0,6);
for (int i = 0; i < byte1.length; i++) {
System.out.printf("bytes[%d]=%d\n",i,byte1[i]);
}
}
allocate.compact();//未读取的数据提前,读取了的清除
三、通道(Channel)
Channel的作用同翻译的意思是一个样子,就是一个通道,它可以读取和写入数据,是在java.nio.channels 包定义的。通道和流的不同,流只是在一个方向上移动,而且通道则是双向的,能读能写。但是Channel只能对Buffer进行操作,不能直接访问数据。并且channel同输入输出流一样,在不用的时候都需要调用close()方法来手动释放内存。
public interface Channel extends Closeable {
public boolean isOpen();
public void close() throws IOException;
}
同上面源码可以看到,channel接口是继承了Closeable接口,该接口从java 5.0版本开始引入,其中中仅声明了一个方法close,用于关闭一个资源。
Channel的常用实现类
类 | 作用 |
---|---|
FileChannel | 用于读取、写入、映射和操作文件的通道。 |
DatagramChannel | 通过 UDP 读写网络中的数据通道。 |
SocketChannel | 通过 TCP 读写网络中的数据。 |
ServerSocketChannel | 可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 |
FileChannel类
Java NIO中的FileChannel是一个连接到文件的通道。可以通过文件通道读写文件。FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下。
在使用FileChannel之前,必须先打开它。但是,我们只能通过使用一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例,因为FileChannel类是一个抽象类无法之间new创建对象。
FileInputStream in = new FileInputStream("data.txt");
FileOutputStream out = new FileOutputStream("data.txt");
RandomAccessFile rw = new RandomAccessFile("data.txt", "rw");
FileChannel c2 = rw.getChannel();
FileChannel c = in.getChannel();
FileChannel c1 = out.getChannel();
而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) | 会将Channel里面还未写入的数据全部刷新到磁盘 |
下面就是对channel配合buffer最基本的使用:
public static void main(String[] args) throws Exception {
try ( RandomAccessFile in = new RandomAccessFile("data.txt","rw");
RandomAccessFile out = new RandomAccessFile("data1.txt","rw")){
FileChannel channel = in.getChannel();
FileChannel channel1 = out.getChannel();
ByteBuffer allocate = ByteBuffer.allocate(1024);
channel.read(allocate);
allocate.flip();
channel1.write(allocate);
} catch (Exception e) {
e.printStackTrace();
}
}
transferFrom()和transferTo()的使用
transferFrom(ReadableByteChannel src,long position, long count)
//src指的是目标文件,position意思是从哪里开始,count指的是大小
//transferFrom()是从目标文件复制到本文件。意思是将R-C.jpg复制到161H4HI-2.jpg
public static void main(String[] args) throws Exception {
try ( RandomAccessFile in = new RandomAccessFile("R-C.jpg","rw");
RandomAccessFile out = new RandomAccessFile("161H4HI-2.jpg","rw")){
FileChannel c = in.getChannel();
FileChannel c1 = out.getChannel();
c1.transferFrom(c,c.position(),c.size());
} catch (Exception e) {
e.printStackTrace();
}
}
transferTo(long position, long count,WritableByteChannel target)
//transferTo()方法的使用跟transferFrom()用法差不多都是复制,但是是把本文件复制到目标文件
public static void main(String[] args) throws Exception {
try ( RandomAccessFile in = new RandomAccessFile("R-C.jpg","rw");
RandomAccessFile out = new RandomAccessFile("161H4HI-2.jpg","rw")){
FileChannel c = in.getChannel();
FileChannel c1 = out.getChannel();
c1.transferTo(c.position(),c.size(),c);
} catch (Exception e) {
e.printStackTrace();
}
}
分散和聚集
分散指的是分散读取,将通道中的数据读入到多个缓冲区。
聚集指的是聚集写入,将多个缓存区写入到channel通道中。
public static void main(String[] args) throws Exception {
ByteBuffer allocate = ByteBuffer.allocate(10);
ByteBuffer allocate1 = ByteBuffer.allocate(100);
ByteBuffer[] byteBuffers= {allocate,allocate1};
RandomAccessFile rw = new RandomAccessFile("1.txt", "rw");
RandomAccessFile rw1 = new RandomAccessFile("2.txt", "rw");
FileChannel channel = rw.getChannel();
channel.read(byteBuffers);//分散读取
FileChannel channel1 = rw1.getChannel();
for (ByteBuffer b:
byteBuffers) {
b.flip();
}
channel1.write(byteBuffers);//聚集写入
}
四、选择器(Selector)
Selector介绍
**概述:**选择器又称为多路复用器,是用于单个线程来处理多个通道的请求,从而提高系统的效率。
selector的使用:
Selector selector= Selector.open();
因为selector的构造方法是是被protected所修饰的,只能在自己的包访问,所以不能直接new创建。
channel.configureBlocking(false);//非阻塞
channel.bind(new InetSocketAddress(9999));//绑定端口
SelectionKey key=channel.register(selector,SelectionKey.OP_ACCEPT);
要想使用selector,接必须要和channel配合使用,通过channel的register方法实现对selector的注册,但是在注册之前要先把channel设置为非阻塞模式才行。
register(Selector sel, int ops)
register方法中第一个参数所传的就是所需要注册的selector。第二个参数则需要传入监听channel的四种类型的事件,分别为:Connect,Accept,Read,Write。
select() //阻塞到至少有一个通道在注册的事件上就绪。
select(long timeout) //和 select() 一样,不一样的则是可以设置时间。
selectNow() //非阻塞,只要有通道就立即返回。
通过selector注册通道之后就可以通过select()
方法来进行轮询,来判断是否有通道接入,如果没有任何接入,则会一直阻塞在select()
方法中,如果有接入则返回,不在阻塞。
SelectionKey(选择键)
SelectionKey是不同通道在注册到Selector时所创建的对象,它让Channel与Selector建立了关系,并且还能通过选择键设置各种事件,也可以通过canel()来取消事件。
而在register方法中的第二个参数所定义的事件类型也就是在SelectionKey有着对应的常量,具体如下:
事件 | 对应的常量 |
---|---|
Connect | SelectionKey.OP_CONNECT |
Accept | SelectionKey.OP_ACCEPT |
Read | SelectionKey.OP_READ |
Write | SelectionKey.OP_WRITE |
事件也不是每次只能使用一种,可以使用逻辑或来将所需要的事件进行连接:
int i = SelectionKey.OP_ACCEPT | SelectionKey.OP_READ;
SelectionKey的相关方法:
方法 | 作用 |
---|---|
channel() | 返回与该键所对应的通道 |
selector() | 返回通道所注册的选择器 |
isValid() | 判断选择器和通道之间的注册关系是否有效 |
cancel() | 终结这种特定的关系 |
interestOps() | 获得此键的interest集合 |
readyOps() | 获取此键上ready集合 |
attach(Object ob) | 添加附加对象 |
attachment() | 获取附加对象 |
isAcceptable() | 判断是否可接收 |
isReadable() | 判断是否可读 |
isWritable() | 判断是否可写 |
isConnectable() | 判断是否可连接 |
示例代码
服务端:
public static void main(String[] args) throws Exception {
ServerSocketChannel open1 = ServerSocketChannel.open();
open1.configureBlocking(false);
open1.bind(new InetSocketAddress(8888));
Selector open = Selector.open();
open1.register(open, SelectionKey.OP_ACCEPT);
while (open.select()>0){
Iterator<SelectionKey> iterator = open.selectedKeys().iterator();
if (iterator.hasNext()){
SelectionKey next = iterator.next();
if (next.isAcceptable()){
SocketChannel accept = open1.accept();
accept.configureBlocking(false);
accept.register(open,SelectionKey.OP_READ);
}else if(next.isReadable()){
ByteBuffer allocate = ByteBuffer.allocate(1024);
SocketChannel channel =(SocketChannel) next.channel();
int len;
while ((len=channel.read(allocate))>0){
allocate.flip();
System.out.println(new String(allocate.array(),0,len));
allocate.clear();
}
}
iterator.remove();
}
}
}
客户端
public static void main(String[] args) throws IOException {
SocketChannel open = SocketChannel.open(new InetSocketAddress("127.0.0.1",9999));
open.configureBlocking(false);
ByteBuffer allocate = ByteBuffer.allocate(1024);
Scanner scanner = new Scanner(System.in);
while (true){
String next = scanner.nextLine();
allocate.put(next.getBytes());
allocate.flip();
open.write(allocate);
allocate.clear();
}
}