一、IO和NIO的区别
引用上一篇文章的区别。IO是传统的面向流的阻塞IO,而NIO是面向缓冲区的非阻塞式IO。在NIO中使用了一个线程来作为Selectors-选择器,来管理多个输入通道,即在使用时只需要将通道注册到选择器中,即可处理输入的通道和选择已经准备好的通道进行管理。
二、学习Channel通道
Buffer缓冲区主要是用于存储数据,而通道就是用来传输缓冲区的数据,但是通道不能直接操作数据。
传统的IO流操作,来了大量请求时,这些IO会向CPU进行请求。而在NIO中这些请求会向通道进行请求,并将数据存入到缓冲区,由通道对缓冲区中的数据进行传输与CPU进行IO交互,因此在NIO中存在大量IO时,使用Channel通道更能提高CPU的利用率。并且IO中流的读写是单向的,而在NIO中通道对流数据的处理即可读取也可以写入。
三、利用通道数据传输
· Channel实现
Channel接口的主要实现有FileChannel、ServerSocketChannel、SocketChannel等。
· 获取通道
以文件传输通道举例即FileChannel,可以通过FileChannel的open方法来获取通道。
open方法中有两个必填参数。第一个参数Path代表文件路径,第二个参数options代表对Path路径的文件要做什么操作。
上述申明了两个文件传输通道,分别代表读渠道(指定要读的文件名与要做的操作为读操作)和写渠道(指定要创建的文件名与要做的操作为读写操作、创建操作)。
· 利用通道在本地进行文件的IO操作
上述代码主要做的事情是,先申明两个通道,一个是读通道,并将当前路径下的channel.jpg文件存储到readBuffer中。另一个是写通道,目的是将readBuffer中的文件复制到当前路径下并创建文件的名字为channel2.jpg。
· 通道分散读取
将通道中传输的数据依次读取到buffer中,直到buffer满了,第二个buffer进行读取,依次类推。
· 通道聚集写入
将多个缓冲区中的数据按顺序依次聚集到通道中进行传输。
四、学习Selectors选择器
有了Selectors选择器,所有的输入和输出通道都会注册到选择器中,选择器的作用是实时监控这些通道的读、写数据等IO状态。当客户端发送请求读操作时,就会有一个读通道请求到选择器中,当服务端的其中一个线程处于就绪状态时,选择器才会将客户端的读请求给服务端,进行数据的读取。
和传统IO相比,传统IO不论服务端是否处于就绪状态,都会发出读数据请求到服务端,那么服务端就会被占用一个线程来准备读请求的数据,并且在准备请求数据时该线程会阻塞等待。
NIO有了Selectors的好处在于,会直接将输入请求分配给已经处于就绪状态的服务端线程来进行数据的操作,减少线程阻塞等待时间,并且其他未处于就绪状态的线程可以继续执行自己的任务。
五、利用Selectors实现非阻塞式IO
· Selectors的创建
//选择器
Selector selector = Selector.open();
· 注册Channel到Selectors中
(1)需要将Channel设置为非阻塞状态。
serverSocketChannel.configureBlocking(false);
(2)注册到Selector中。
//注册到选择器 指定监听接收事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
这里在注册的时候指定了接受状态。当客户端发送请求时,选择器需要监听是否有接受请求就绪状态的服务端线程,如果有则进行分配。
就绪状态总共有:
读就绪状态:
public static final int OP_READ = 1 << 0;
写就绪状态:
public static final int OP_WRITE = 1 << 2;
连接就绪状态:
public static final int OP_CONNECT = 1 << 3;
接收就绪状态:
public static final int OP_ACCEPT = 1 << 4;
当服务端注册对应的就绪状态到选择器时,选择器就会分配对应的请求到对应的服务端。
· 实现一个非阻塞式IO - 客户端
@Test
public void client() throws IOException {
//客户端通道
SocketChannel socketChannel = SocketChannel.open(
new InetSocketAddress("127.0.0.1", 8080));
//设置为非阻塞模式
socketChannel.configureBlocking(false);
//缓存区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//发送数据给服务端
for (int i = 0; i < 10; i++) {
buffer.put(("当前数值:" + i + "\n").getBytes());
//切换读模式,读到通道中
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
}
//关闭资源
socketChannel.close();
}
上述客户端实现步骤为:
(1)声明客户端通道,并指定ip和端口。设置通道为非阻塞模式。
(2)建立缓存区,大小为1024。
(3)将数据存放到缓存区中,客户端通道进行数据传输。
(4)关闭通道资源。
· 实现一个非阻塞式IO - 服务端
@Test
public void server() throws IOException {
//服务端通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8080));
//选择器
Selector selector = Selector.open();
//注册到选择器 指定监听接收事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//轮询获取准备就绪的事件
while (selector.select() > 0) {
//所有监听的事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//判断是什么事件准备就绪
//接收就绪
if (selectionKey.isAcceptable()) {
//获取客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
//切换成非阻塞模式
socketChannel.configureBlocking(false);
//将该通道注册到选择器上
socketChannel.register(selector, SelectionKey.OP_READ);
//读就绪
} else if (selectionKey.isReadable()) {
//获取读就绪状态的通道
SocketChannel channel = (SocketChannel) selectionKey.channel();
//读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = 0;
while ((len = channel.read(buffer)) > 0) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, len));
buffer.clear();
}
}
//当前连接完成,需要取消选择键。
iterator.remove();
}
}
上述服务端的实现步骤为:
(1)声明一个服务端通道,并且绑定端口为8080,通道设置为非阻塞模式。
(2)声明一个选择器,将该通道注册到选择器中,并指定接收时间为accept-接收状态。此时的服务端线程只会接收客户端的accept请求。
(3)轮询监听器中准备就绪的事件。
(4)遍历事件,判断当前时间处于什么状态。
(5)根据(2)此时的判断只会进入到接收就绪的判断中,进行服务端处理,即获取客户端连接通道,重新注册该客户端连接通道到选择器中并设置事件状态为读取状态。最后服务端对客户端的请求处理完毕后需要移除本次处理的选择键,防止重复处理该请求的选择事件。
(6)继续执行步骤(3)(4),此时的事件状态为read-读状态,会进入到读就绪状态判断中,进行数据的读取。读取完毕后,删除读状态的选择键。
服务端的这个线程主要可以接收accept-接收状态和read-读状态的请求。并且当选择器的事件为其他状态时,也不会对该线程进行阻塞。
· 实现一个非阻塞式IO - 运行结果
服务端仍在不断的等待接收读状态和接收状态的事件。
六、NIO总结
NIO是一个非阻塞式的IO,内部结合了buffer、channel、selectors核心组件来实现。可将IO操作需要传输的数据存储在buffer中,并通过channel来传输数据。在selectors选择器进行不断轮询,将对应状态的请求事件分配到对应就绪状态的服务端中。以此来提高线程及cpu的利用率,提高性能。
· Selectors的多路复用机制如何支持海量请求连接?
首先Selector选择器,不需要很多的线程来支持,对于客户端发起的连接,完全可以通过一个Selector选择器来监听是否有请求Channel对应的状态,分配到对应就绪状态的服务端线程中。这里选择器的通过select机制不断轮询就已经避免了创建多个线程。
服务端可根据不同事件的状态来创建线程,来处理请求逻辑。与传统的IO相比,NIO的多路复用机制更能支持海量请求连接。