NIO核心API Channel, Buffer, Selector
3大核心组件
Selector(选择器):
Selector根据通道的事件,选择给哪个通道服务;
Channel(通道): 类似于bio中的socket
数据通道;
Buffer(缓冲区):
每一个通道对应一个Buffer缓冲区;
通道和Buffer之间是双向的,Buffer和通道之间可以相互读写;
通道Channel
NIO的通道类似于流,但有些区别如下:
1. 通道可以同时进行读写,而流只能读或者只能写
2. 通道可以实现异步读写数据
3. 通道可以从缓冲读数据,也可以写数据到缓冲:
缓存Buffer
缓冲区本质上是一个可以写入数据的内存块,然后可以再次读取,该对象提供了一组方法,可以更轻松地使用内存块,使用缓冲区读取和写入数据通常遵循以下四个步骤:
1. 写数据到缓冲区;
2. 调用buffer.flip()方法;
3. 从缓冲区中读取数据;
4. 调用buffer.clear()或buffer.compat()方法;
当向buffer写入数据时,buffer会记录下写了多少数据,一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式,在读模式下可以读取之前写入到buffer的所有数据,一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。
Buffer在与Channel交互时,需要一些标志:
buffer的大小/容量 - Capacity
作为一个内存块,Buffer有一个固定的大小值,用参数capacity表示。
当前读/写的位置 - Position
当写数据到缓冲时,position表示当前待写入的位置,position最大可为capacity – 1;当从缓冲读取数据时,position表示从当前位置读取。
信息末尾的位置 - limit
在写模式下,缓冲区的limit表示你最多能往Buffer里写多少数据; 写模式下,limit等于Buffer的capacity,意味着你还能从缓冲区获取多少数据。
下图展示了buffer中三个关键属性capacity,position以及limit在读写模式中的说明:
缓冲区常用的操作
向缓冲区写数据:
1. 从Channel写到Buffer;
2. 通过Buffer的put方法写到Buffer中;
从缓冲区读取数据:
1. 从Buffer中读取数据到Channel;
2. 通过Buffer的get方法从Buffer中读取数据;
flip方法:
将Buffer从写模式切换到读模式,将position值重置为0,limit的值设置为之前position的值;
clear方法 vs compact方法:
clear方法清空缓冲区;compact方法只会清空已读取的数据,而还未读取的数据继续保存在Buffer中;
Selector
一个组件,可以检测多个NIO channel,看看读或者写事件是否就绪。
多个Channel以事件的方式可以注册到同一个Selector,从而达到用一个线程处理多个请求成为可能。
Client(Socket):
程序不会直接读写Channel,是往Buffer里面读写,让通道和Buffer之间有个缓冲;
底层通过Buffer实现非阻塞,NIO就是面向缓冲区的,面向块的;
InActive
Active
每个通道发生了关心的事情(读、写、连接),那么就去处理;
还可以处理其它任务;
这是通过后台运行一个线程来进行处理的;
3)Selector、Channel、Buffer的关系:
(1)每个Channel对应一个Buffer;
(2)Selector对应一个线程,一个线程对应多个Channel(连接/传统java的一个Stream);
有多个Channel注册到了这个Selector上:
到底是哪个Channel在工作;
(3)程序切换到哪个Channel是由事件决定的,Event是一个非常重要的概念;
(4)Selector会根据不同的事件,在各个通道上切换;
(5)Buffer就是一个内存块(NIO就是面向块/Buffer编程的),底层是有一个数组的;
(6)数据的读取和写入是通过Buffer,这个是和BIO, BIO中要么是输入流,要么是输出流,不能双向;
但是NIO的Buffer是可以读和可以写的,但是需要flip进行切换;
(7)Channel也是双向的,可以反映底层操作系统的情况;
比如:Linux底层的操作系统的通道就是双向的;
Channel是和Buffer是对应的;
根据Channel上发生的事件,进行事情的处理;
4)缓冲区(Buffer)
程序不能直接从Channel读写数据,必须从Buffer缓冲区读写;
Buffer是顶级抽象类,有7个子类,没有boolean: Byte,Short,Char,Int,Long,Double,Float;
用的最多的是ByteBuffer,因为其它的也要最终转化为字节;
4个重要属性:
int mark = -1;
标记:一旦flip后就作废了;
int position = 0;
位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变该值,并为下次读写做准备;
int limit;
表示缓冲区的当前终点,不能对缓冲区超过的极限的位置进行读写操作,且极限是可以修改的;
之所以修改是因为:Buffer是可读可写的;
int capacity;
容量,既可以容纳的最大数据量,在缓冲区创建的时候被设定,并且不能改变;
断点一下一目了然;
ByteBuffer
(1)网络数据在底层都是字节形式传输的;
(2)put什么类型的数据,get也采用取什么类型的数据,不然可能有异常
5)Channel(通道)
(1)nio的通道类似于流,但是有区别:
通道可以同时进行读写,而流只能读或者写;
通道可以实现异步读写数据;
通道可以从缓冲读数据,也可以写数据到缓冲;
(2)4个重要常用Channel类:
FileChannel: 用于文件的数据读写
DatagramChannel: 用于UDP的数据读写
ServerSocketChannel(用于TCP数据的读写):
当客户端请求连接的时候,ServerSocketChannel,会生成一个对应的SocketChanel,与服务器通信;
SocketChannel(用于TCP数据的读写)
(3)继承关系
Channel-->NetworkChannel-->
ServerSocketChannel-->ServerSocketChannelImpl(真正类型)
SocketChannel-->SocketChannelImpl(真正类型)
(4)FileChannel写数据例子
"xxxxx"字符串-->ByteBuffer-->java输出流对象包含了NIOFileChannel-->文件:
是FileOutputStream内置了一个通道Channel,这个可以打断点可以知道;
flip的作用: limit设置为真实的大小;
(5)字节不可以直接显示,要转为字符串;
底层的hb数组
byteBuffer,array()
记得最后要关闭流
(6)文件拷贝
不知道数据有多大,需要用while循环读取;
流和文件关联起来;
6)Selector(选择器)
Selector上面能够注册通道;
Selector不停的在轮训哪个Channel上面有事件发生;
通过缓冲Buffer实现非阻塞;
一旦检测到有哪个事件发生了,调用select就会拿到SelectionKey的集合,然后就可以反向得到Channel,也知道是读、写、连接
等哪个事件发生了;
select(): 直到注册的Channel里面至少有一个事件发生,才会返回,否则一直阻塞;
select(2000): 最多等待2s,及时没有事件也会返回;
wakeup(): 即使正在阻塞,调用后立马返回
selectNow(): 不阻塞,立马返回
NIO非阻塞原理:
1.当客户端连接时,会通过ServerSocketChannel得到SocketChannel;
2.将SocketChannel注册到Selector上,register(Selector sel, int opts),一个Selector上面可以注册多个Channel;
3.注册后返回一个SelectionKey,会和该Selector关联(集合);
4.Selector进行监听,select方法会返回由事件发生的通道的个数;
5.进一步得到SelectionKey(有事件发生);
6.在通过SelectionKey反向获取Channel;
7.可以通过得到的Channel,完成业务的处理;
本身accept是阻塞的,但是由于是事件驱动,确切已经知道发生了连接事件了,因此accept一定是立即返回的;
给Channel关联一个Buffer:
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
OP_READ: 就是客户端发来了消息
ServerSocketChannel: 监听新的客户端连接,生成SocketChannel完事;
SocketChannel: 负责具体的读写操作;
7)零拷贝(网络编程优化)
内存拷贝:
用户态和内核态的切换次数来衡量:
DMA:
Direct Memory Access(直接内存拷贝 不经过CPU)
(1)2种零拷贝的方法:
1:mmap(内存映射 3次拷贝,3次状态的切换);
传统IO read:(4次拷贝,3次状态的切换)
先通过DMA,把数据从硬盘拷贝到内核Buffer,
然后把数据从内核Buffer用CPU拷贝到用户Buffer,
用户数据在用户Buffer修改完毕以后,再从用户Buffer使用CPU拷贝到Socket Buffer,
DMA拷贝到协议栈
优化:将文件映射到内核缓冲区,同时用户控件可以共享内核的数据,这样在网络传输时,就可以减少内核空间
到用户空间的拷贝次数;
硬件拷贝到内核缓冲;
user buffer和kernel buffer可以共享数据,这样减少了一次cpu拷贝,这样就在内核缓存进行修改,cpu拷贝到socket buffer;
socket buffer进行DMA拷贝到协议栈;
总结:mmap并不是真正的零拷贝,但是确实是少了一次拷贝,因为内核缓冲和user缓冲可以共用数据;
2:sendFile;
linux 2.1
数据根本不经过用户态,直接从内核缓冲区到socket buffer,由于和用户态完全无关,就减少了一次上下文切换;
硬件DMA拷贝到kernel buffer
kernel buffer进行一次cpu拷贝到socket buffer
socket buffer拷贝到协议栈
4-->3 拷贝
3-->2 切换
3次上下文切换变为2次
少了一次cpu
总结:
DMA拷贝是少不了的,必须存在硬件拷贝到kernel buffer,这是少不了的,因此零拷贝不是没有拷贝,而是指的没有CPU拷贝;
linux 2.4:
DMA
kernel buffer -->socket buffer: DMA拷贝到协议栈
kernel buffer需要很少数据拷贝到协议栈: 长度 offset
还是有一次CPU拷贝的,但是拷贝的信息很少,消耗很低,可以忽略(描述信息的拷贝);
4-->2拷贝: hard disk-->kernel buffer-->协议栈
3-->2上下文切换: