NIO比IO强在哪
NIO,即非阻塞的IO,传统IO指的是BIO,使用流来进行文件读写,而NIO使用Channel(通道)和Buffer(缓冲)来进行文件读写。NIO是双全工,非阻塞的。
NIO的"强"主要体现在网络中
1.NIO支持非阻塞IO(ServerSocketChannel 和 SocketChannel),NIO在执行IO操作时不会阻塞,可以继续执行其他的代码;
2.NIO支持多路IO复用(Selector),一个线程可以监听多个通道的请求(接受连接,完成连接,可读,可写)
3.NIO提供了ByteBuffer类,可以高效地管理缓冲区。
ServerSocketChannel和SocketChannel
用于实现非阻塞IO,ServerSocketChannel负责监听连接请求,当设置非阻塞模式后,在等待连接请求时不会阻塞线程,SocketChannel负责发送连接请求和进行数据的读写,当设置成非阻塞模式时,在读写数据时不会阻塞线程。
Selector
用于实现IO多路复用,channel能通过register注册到一个Selector选择器上,这个选择器会不断轮询注册在其上面的所有通道,当通道发生事件时(连接请求,可读,可写),Selector就把通道放到一个就绪集合中,通过get方法可以获取到就绪的Channel并对其进行相应的操作。
注意: 由于 JDK 使用了 epoll()
代替传统的 select
实现,所以它并没有最大连接句柄 1024/2048
的限制。这也就意味着只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。
Selector可以监听的四种类型
1.SelectionKey.OP_ACCEPT:表示接收连接的事件,当ServerSocketChannel收到来自SocketChannel的连接请求时。这个ServerSocketChannel会有OP_ACCEPT事件
2.SelectionKey.OP_CONNECT:表示连接成功事件,当ServerSocketChannel同意来自SocketChannel的连接请求时。这个SocketChannel会有OP_CONNECT事件
3.SelectionKey.OP_READ:表示有数据可读
4.SelectionKey.OP_WRITE:表示可以写入数据
一个Selector实例控制的3个SelectionKey集合
1.所有注册在该Selector实例上的Channel的集合,通过keys()方法获取
2.所有需要进行io操作的Channel的集合,通过selectedKeys()方法获取
3.所有已经删除的Channel的集合
Selector核心方法
select():监控所有注册的Channel,当有需要io操作的Channel出现时,select方法会返回,并且将对应的SelectorKey加入到需要进行io操作的Channel集合中。
Buffer
缓冲区,NIO对数据的操作是利用Channel和基于Buffer进行的,Buffer是一个抽象类,用得比较多的实现类是ByteBuffer,缓冲区的2个重要操作是put和get。因为我们拿到一个缓冲区时,要么是往缓冲区里传输数据,要么是从缓冲区里拿数据。
Buffer的4个变量(limit,position,Capacity,Mark)
然后要说的就是Buffer里面的4个核心变量了,与存取数据操作密切相关。
limit 缓冲区里数据的总数
position 下一个要被读写的位置,调用get和put都会导致position的改变
Capacity容量,是创建缓冲区时的容量,不能被改变,因为缓冲区底层是数组。
Mark 记录上一次读写的位置
Buffer的基础操作
创建缓冲区
ByteBuffer b =ByteBuffer.allocate(1024)
此时四个变量的变化:
limit 1024
position 0
Capacity 1024
Mark java.nio.HeapByteBuffer[pos=0 lim=1024 cap=1024]
往缓冲区添加数据
b.put("我是高手".getBytes);
此时四个变量的变化:
limit 1024
position 12(这里UTF-8一个中文是3个字节,一共12个字节)
Capacity 1024
Mark java.nio.HeapByteBuffer[pos=12 lim=1024 cap=1024]
如果要从缓冲区取出数据,必须先调用flip方法(翻转,为什么叫翻转呢?先往下看)
b.flip();
此时四个变量的变化:
limit 12
position 0
Capacity 1024
Mark java.nio.HeapByteBuffer[pos=0 lim=12 cap=1024]
方法命名为翻转是针对position和limit这2个位置之间的区域来说的,调用flip方法之前是12~1024,调用之后变成了0~12。就像翻转过来了一样。
翻转后就可以取数据了,首先定义一个大小为limit的字节数组(因为只有这么多字节的数据),然后调用get方法将数据存到这个字节数组中。
byte [] bytes =new byte[b.limit()]
b.get(bytes);
此时四个变量又有了变化
limit 12
position 12 这里变成了12
Capacity 1024
Mark java.nio.HeapByteBuffer[pos=12 lim=12 cap=1024]
Channel 通道
buffer负责读写数据,channel负责传输数据?
通道可以分为文件通道和套接字通道
FileChannel,SocketChannel,ServerSocketChannel,DatagramChannel 都属于通道
FileChannel
建立通道:调用FileChannel的open静态方法建立通道,第一个参数是文件路径,第二个参数是访问模式(只读,读写)
FileChannel sourceChannel = FileChannel.open(
Paths.get("logs/javabetter/itwanger.txt"), StandardOpenOption.READ);
使用FileChannel和ByteBuffer将source文件复制到destination文件中思路:
1.首先分别调用open方法创建内存与source文件之间的通道和内存与destination文件之间的通道。
2.调用ByteBuffer的allocate方法创建一个ByteBuffer缓冲区,大小随意,反正复制的时候是一个字节一个字节复制的。
3.开启while循环,调用source文件的通道的read方法,将source文件读取到缓冲区,缓冲区调用flip方法翻转,然后调用destination文件的通道的write方法将缓冲区写入destination文件中去,再调用一次缓冲区的clear方法重置缓冲区的四个变量以及内容。
4.当read方法返回-1时结束
使用MapperByteChannel(内存映射文件)和FileChannel将source文件复制到destination文件中思路:
1.同样的先分别创建2个文件的通道
2.一个MapperByteBuffer对象需要用FileChannel对象的map()方法创建,表示一个文件的映射,创建source和destination的内存映射(即2个MapperByteBuffer对象)。
3.循环读取source的MapperByteBuffer,并写入destination的MapperByteBuffer。
4.调用destination的force方法刷盘,如果不调用,也会刷盘,只不过可能不会立即刷盘。
使用transfer()和FileChannel将source文件复制到destination文件中思路:
1.同样的先分别创建2个文件的通道
2.sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel);调用sourceChannel的transferTo方法。transferTo方法会返回该次操作写的字节数。
底层使用了零拷贝,最大限度的减少了数据传输过程中的CPU和内存开销
直接缓冲区和非直接缓冲区
1.非直接缓冲区
创建方式:ByteBuffer.allocate()
位置:非直接缓冲区存在于JVM的堆空间中,收到GC的管理
使用非直接缓冲区进行io操作时,需要将数据从JVM堆中复制到本地内存中,再进行io操作
2.直接缓冲区
创建方式:ByteBuffer.allocateDirect()
位置:直接缓冲区分配在本地内存中,不受GC的管理
在使用直接缓冲区进行IO操作时,直接在本地内存中进行。