概述
NIO是jdk1.4之后提供的全新操作IO
的API
,意为NOT-BLOCKIN-IO
,即非阻塞IO
,它解决了BIO
编程中的accept
和read
方法的阻塞问题,能够大量减少服务端的线程数量,想对比BIO那样,每个客户端都对应一个服务端的线程,NIO给够提供较少的线程数,专注于用户感兴趣的事件监听。
BIO模型
NIO模型
理解NIO之前,必须先了解三个类。
Buffer
-
Buffer
:缓冲区,NIO上所有数据的读写都是基于Buffer
的,Buffer
提供了一系子类ByteBuffer
CharBuffer
IntBuffer
LongBuffer
ShortBuffer
DoubleBuffer
FloatBufffer
-
获取
Buffer
对象
ByteBuffer.wrap(byte[] byte)
ByteBuffer.allocate(1024)
使用的是堆内存
ByteBuffer.allocateDirect(1024)
使用的直接内存 -
Buffer
有3个重要的属性capacity
:容量 表示buffer
的容量大小position
:当前Buffer
能存放数据的位置 ,初始化是0,表示数据写在Buufer
的第一个位置, 每次添加数据或者读取数据position
的位置就往后移动limit
:最大读写数据的位置,初始化的值等于capicity
ByteBuffer buffer = ByterBuffer.allocate(16)
初始化一个Buffer
的时候,其内部就是一个数组。position
的值为数组的第一个位置也就是0,其capacity
与limit
的值为该数组的容量
put(Byte byte)
:position
的位置随着数据的添加往后移动一个位子,position
的值是下一个可以插入数据的位置,比如已经插入了3个数据了 那么position
的位置就是3 (数组的角标都是从0开始的)
filp()
:在读取buffer
中的数据之前,需要调用filp()
方法,将buffer
变成读模式 。此时position
的位置变成数组的第一位,也就是0,可读数据limit
的值变成之前可存储数据的position的位置。
clear()
:将buffer
变成写模式,即position
的值重新变成0,capacity
与limit
的位置相等,与初始化的值一致,不同的是,如果Buffer
里面的值没有被读取完,那么Buffer
中每个位置依旧保存之前的值
compact()
:也是将Buffer
变成写模式,与clear()
不同的是,如果在转化写模式之前Buffer
中还存在数据,那么就将已经存在的数据往前移动。position
的位置变成该可以存放数据的下一个位置
get()
:获取单个数据,返回的是获取的数据。如果返回-1则表示没有数据可以读取了
```hasRemaining()``:判断Buffer中是否还存在数据
Channel
Channel
即管道,与BIO中从socket
获取的流类似,不同的是流是单向,而Channel
是双向的,既可以读数据,也可以写数据,但是读写都必须依赖于Buffer
,将读取的数据放入Buffer
中;写数据时。先把数据存放到Buffer
中。
Channel.read(Bytebuffer buffer)
:将数据读到buffer中
Channel.writer(Bytebuffer buffer)
:将数据写到buffer中。
常用的Channle
有
FileChannel
:文件通道SocketChannel
-:套接字客户端通道 对应BIO中的socket
ServerScoketChannel
:套接字服务端通道 对应BIO中的ServerSocket
DatagramChannel
:UDP通道
Scatter(分散)
当Channel
的数据过多,而且定义的Buffer
容量又过于小的时候,除了重复的读取之外。还可以将Channel
里面的数据分散读取到不同的Buffer
中。
FileChannel inputChannel = new FileInputStream("D://Hello.txt").getChannel();
FileChannel outChannel = new FileOutputStream("D://Hello_1.txt").getChannel();
ByteBuffer byte1 = ByteBuffer.allocate(5);
ByteBuffer byte2 = ByteBuffer.allocate(5);
long read = inputChannel.read(new ByteBuffer[]{byte1, byte2});
byte1.flip();
byte2.flip();
System.out.println(new String(byte1.array(),0,byte1.limit()));
System.out.println(new String(byte2.array(),0,byte2.limit()));
inputChannel.close();
outChannel.close();
Gather(聚集)
可以将多个Buffer
中的数据写入Channel
中
FileChannel通道示例
- 可以通过文件的输入输出流获取Filehannel
FileChannel channel = new FileInputStream("D://Hello.txt").getChannel()
- 也可以通过RandomAccessFile类的getChannel方法获取Filehannel
RandomAccessFile file = new RandomAccessFile("D:\\Hello.txt", "rw");
FileChannel channel = file.getChannel();
- 值得注意的是,通过IO流获取的通道,如果是从输入流中获取的
Channel
那么该Channel
只能读取文件 ,通过输出流获取的Channel
,只能写数据到文件中 - 通过
FileChannel
复制文件
//获取到文件的输入管道
FileChannel inputChannel = new FileInputStream("D://Hello.txt").getChannel();
//获取到文件的输出管道
FileChannel outChannel = new FileOutputStream("D://Hello_1.txt").getChannel();
//定义一个bytebuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len;
while ((len=inputChannel.read(byteBuffer))!=-1){
//切换到读模式
byteBuffer.flip();
//写入到channel中
outChannel.write(byteBuffer);
byteBuffer.clear();
}
inputChannel.close();
outChannel.close();
FileChannel
也提供了一个管道向另一个管道中直接传输数据,不需要Buffer
作为数据载体
FileChannel inputChannel = new FileInputStream("D://Hello.txt").getChannel();
FileChannel outChannel = new FileOutputStream("D://Hello_1.txt").getChannel();
/**
transferTo 表示将调用者的管道数据复制到其他管道中
0 管道中数据的位置
inputChannel.size()表示数据的整体长度
outChannel 表示需要复制到的管道
**/
inputChannel.transferTo(0,inputChannel.size(),outChannel);
ServerSocketChannel通道示例
//创建服务器
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定监听端口
ServerSocketChannel bind = serverSocketChannel.bind(new InetSocketAddress(9999));
ByteBuffer buffer = ByteBuffer.allocate(1024);
List<SocketChannel> socketChannelList = new ArrayList<>();
while (true){
SocketChannel socketChannel = bind.accept();
socketChannelList.add(socketChannel);
System.out.println("连接客户端"+socketChannel.getRemoteAddress());
for (SocketChannel channel : socketChannelList) {
channel.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(),0,buffer.limit()));
buffer.clear();
}
}
}
SocketChannel通道示例
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(9999));
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
byteBuffer.clear();
String s = scanner.nextLine();
byteBuffer.put(s.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
System.out.println("发送成功");
}
}
}
上面是利用NIO的API编写的服务端与客户端通信的例子,值得注意的是该网络模型是阻塞式的,服务端的accept
方法是阻塞的,如果没有客户端连接,就会一直阻塞在那里,同样的 read
方法也是阻塞的。此时的代码与BIO编程的代码没有区别。但是为什么说NIO就是非阻塞\color{red}{非阻塞}非阻塞的呢?因为有些Channel
可以设置非阻塞。比如ServerSocketChannel
与SocketChannel
serverSocketChannel.configureBlocking(false)
当设置为非阻塞的时候 就可以不停地接受客户端的连接 从而不再accept的方法上阻塞。
public static void main(String[] args) throws IOException {
//创建服务器
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//设置ServerSocketChannel为非阻塞 accept不是阻塞的 返回null
serverSocketChannel.configureBlocking(false);
//绑定监听端口
ServerSocketChannel bind = serverSocketChannel.bind(new InetSocketAddress(9999));
ByteBuffer buffer = ByteBuffer.allocate(1024);
List<SocketChannel> socketChannelList = new ArrayList<>();
while (true){
SocketChannel socketChannel = bind.accept();
if(socketChannel!=null){
socketChannelList.add(socketChannel);
//设置SocketChannel为非阻塞 read方法为非阻塞 没有读取数据返回0
socketChannel.configureBlocking(false);
System.out.println("连接客户端"+socketChannel.getRemoteAddress());
}
for (SocketChannel channel : socketChannelList) {
int read = channel.read(buffer);//如果没有读到数据 返回0
if(read!=0){
buffer.flip();
System.out.println(new String(buffer.array(),0,buffer.limit()));
buffer.clear();
}
}
}
}
Selector
在了解Selector
之前先了解一下常见的5种IO网络模型
网络模型
- 同步阻塞
IO
(就是上一篇介绍的BIO
) - 同步非阻塞
IO
(NIO
) - 多路复用 本节主角 (
Selector
) - 事件驱动
IO
- 异步
IO
上面的非阻塞式的IO
编写的服务端和客户端已经可以保证多客户端的连接与读了,为什么还要用Selector
呢?因为尽管使用了Selector
,那么也是同步非阻塞的。在上面单线程的一个服务端与客户端通信的示例中,可以看出 无论是否有客户端的连接或者读写事件发生的时候,主线程都是不停的运行,而恰恰有时候,仅仅只有连接或者读写事件发生的时候,才需要线程处于活跃状态。所以才需要一个Selector
Selector
就是一个多路复用,意为选择器,将通道注册到选择器上,需要注意的是能够注册到Selector
的通道必须是非阻塞的,如ServerSocketChannel
,SocketChannel
等,但是FileChannel
是不能够注册到Selector
的,因为FileChannel
是非阻塞的。而ServerSocketChannel
和SocketChannel
等默认也是阻塞的,可以通道的configureBlocking(false)
设置非阻塞
注册到Selector
的通道。在没有事件的时候,这个Selector
是阻塞的。等到有事件发生的时候,Selector
会把所有已经达到的时间全部返回,然后再依次处理
Selector
的事件
- SelectionKey.OP_ACCEPT:连接事件 服务端特有的事件 客户端连接发生
- SelectionKey.OP_CONNECT:连接事件 客户端特有时间 连接服务端发生
- SelectionKey.OP_READ:可读事件 通道中有数据可以读的发生
- SelectionKey.OP_WRITE:可写事件 可以写数据到通道中发生
-
Selector selector = Selector.open()
:创建一个Selector
。 -
SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT)
,将ServerSocketChannel
的Accept
事件注册到Selector
上
返回的SelectionKey
表示注册的通道与一个选择器之间的关系 -
SelectionKey
相关的方法channel()
返回对应的通道selector()
返回通道注册的选择器cancle()
终结这种特定的注册关系isValid()
判断注册关系是否有效interestOps()
返回关系的操作 是以整数的形式进行编码的比特掩码 可以使用位运算检查所关系的操作
boolean isAccept = interestOps & SelectionKey.OP_ACCEPT == SelectionKey.OP_ACCEPT
-
readyOps()
返回通道已经就绪的操作,返回值也是一个整数,也可以使用上面相同的位操作检测通道中有哪些事件已就绪
SelectionKey.readyOps() &SelectionKey.WRITE == SelectionKey.WRITE
Selector
选择器维护着注册过的通道集合,并且这些注册关系都封装在SelectorKey
对象中,每个Selector
对象都需要维护三个集合
- 已经注册的键的集合,
keys
方法返回这个已经注册过的集合 这个集合不能修改 - 已经选择的键的集合
selectedKey()
返回,改集合中的每个成员都是相关的通道被选择器判断已经准备好的,并且包含键的interest
的事件 - 已经取消的键的集合 这里面包含了条用了
cancle
方法的键
-
select()
返回就绪通道的个数 如果没有就绪通道 那么该返回就会一直阻塞 -
selector.selectedKeys()
返回就绪通道的列表 ,包含各种就绪的通道SelectionKey
在处理完注册到Selector
上面的事件后必须将这个SelectionKey
从就绪通道的列表中移除,否则在下次处理就绪通道中还会重复处理,从而实际上却没有事件发生而异常
Selector使用示例
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(9999));
SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select()>0){
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if((key.interestOps() & SelectionKey.OP_ACCEPT )== SelectionKey.OP_ACCEPT){
ServerSocketChannel channel =(ServerSocketChannel) key.channel();
SocketChannel socketChannel = channel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
}else if(key.isReadable()){
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
SocketChannel channel = (SocketChannel)key.channel();
channel.read(byteBuffer);
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(),0,byteBuffer.limit()));
byteBuffer.clear();
}
iterator.remove();
}
}
}
如果处理的事件太多 比如在十万个数据的时候。在不停的循环操作的时候 这里只有一个线程也需要不少的时间 那么在新的连接就无法处理了,此时应该怎么做?可以将处理时间放入线程池中,通过多个线程池去处理 那么这样的话 也是无法满足 还是需要不停地处理事件 新的连接无法处理。那么可不可以有多个Selector
,在连接的时候就交给一个Selector
处理,时间处理给另外一个Selector
处理,这样就解决了新的连接来了,没有时间处理问题