NIO简介
Java NIO属于IO多路复用模型,NIO是面向缓冲区。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络,Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。NIO可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个。它类库包含以下三个核心组件:
- Channel(通道)
- Buffer(缓冲区)
- Selector(选择器)
缓冲区Buffer
- 通道的读取,就是将数据从通道读取到缓冲区中
- 通道的写入,就是将数据从缓冲区写入通道中
NIO的Buffer内部是一个内存块(数组),与普通的内存块(Java数组)不同的是:NIO Buffer对象提供了一组比较有效的方法,用来进行写入和读取的交替访问。
capacity属性
Buffer类的capacity属性表示内部容量的大小。一旦写入的对象数量超过了capacity,缓冲区就满了,不能再写入了。capacity并不是指内部的内存块byte[]数组的字节数量,而是指能写入的数据对象的最大限制数量。
position属性
Buffer类的position属性表示当前的位置。在刚进入写模式时,position值为0,表示当前的写入位置为从头开始。初始的position值为0,最大可写值为limit-1。读模式时,position会被重置为0。
limit属性
在写模式下,limit属性值的含义为可以写入的数据最大上限。在刚进入写模式时,limit的值会被设置成缓冲区的capacity值,表示可以一直将缓冲区的容量写满。
在读模式下,limit值的含义为最多能从缓冲区读取多少数据。
public class BasicBuffer {
public static void main(String[] args) {
// 创建缓冲区,可以存3个int类型的数据
IntBuffer intBuffer = IntBuffer.allocate(3);
for (int i = 0; i < intBuffer.capacity(); i++) {
intBuffer.put(i * 1); // 一共3个int类型数据
}
// 切换到读模式
intBuffer.flip();
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
}
通道Channel
在NIO中,一个网络连接使用一个通道表示,所有NIO的IO操作都是通过连接通道完成的。一个通道类似于OIO中输入流和输出流的结合体,既可以从通道读取数据,也可以向通道写入数据。Java NIO中一个socket连接使用一个Channel来表示。从更广泛的层面来说,一个通道可以表示一个底层的文件描述符,例如硬件设备、文件、网络连接等。然而,远不止如此,Java NIO的通道可以更加细化。例如,不同的网络传输协议类型,在Java中都有不同的NIO Channel实现。
常用通道
- FileChannel:文件通道,用于文件的数据读写。
- SocketChannel:套接字通道,用于套接字TCP连接的数据读写。
- ServerSocketChannel:服务器套接字通道(或服务器监听通道),允许我们监听TCP连接请求,为每个监听到的请求创建一个SocketChannel通道。
- DatagramChannel:数据报通道,用于UDP的数据读写。
和Buffer的联系
常用方法
- read() 从通道读取数据并写入缓冲区
- write() 把缓冲区的数据读出来并写入到通道中
read()读取通道的数据时,对于通道来说是读模式(方法的使用者为动作操作者),对于ByteBuffer缓冲区来说是写入数据,这时ByteBuffer缓冲区处于写模式
选择器Selector
从模型意义上讲,选择器就是避免了通道准备数据过程中,程序代码被阻塞,当通道注册到选择中后,数据准备好后可以通知做相关业务。所以单独线程可以管理多个输入和输出通道。
通道和选择器之间的关联通过register(注册)的方式完成,可供选择器监控的通道IO事件(这里的IO事件不是对通
道的IO操作,而是通道处于某个IO操作的就绪状态,表示通道具备执行某个IO操作的条件)类型包括以下四种:
- 可读:SelectionKey.OP_READ
- 可写:SelectionKey.OP_WRITE
- 连接:SelectionKey.OP_CONNECT
- 接收:SelectionKey.OP_ACCEPT
并不是所有的通道都是可以被选择器监控或选择的。例如,FileChannel就不能被选择器复用。注册到选择器的通道必须处于非阻塞模式下,否则将抛出IllegalBlockingModeException异常。
实战
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* @author pangjian
* @ClassName DisCardServer
* @Description :仅读取客户端通道的输入数据,读取完成后直接关闭客户端通道,并且直接抛弃掉(Discard)读取到的数据
* @date 2022/6/10 16:20
*/
public class DisCardServer {
public static void main(String[] args) throws IOException {
// 1.创建选择器
Selector selector = Selector.open();
// 2.创建通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3.将通道设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4.绑定链接
serverSocketChannel.bind(new InetSocketAddress(18899));
// 5.将通道注册的“接收新连接”IO事件注册到选择器上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6.轮询感兴趣的IO就绪事件(选择键集合)
while (selector.select() > 0) {
//7.获取选择键集合
Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
while (selectionKeys.hasNext()) {
// 8.获取单个的选择键,并处理
SelectionKey selectionKey = selectionKeys.next();
/* 9.判断key是具体的什么事件(在事件处理过程中,
对于新建立的socketChannel客户端传输通道,也要注册到同一个选择器上,
这样就能使用同一个选择线程不断地对所有的注册通道进行选择键的查询。) */
if (selectionKey.isAcceptable()) {
// 10.若选择键的IO事件是“连接就绪”,就获取客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
// 11.将新连接切换为非阻塞模式
socketChannel.configureBlocking(false);
// 12.将新连接的通道的可读事件注册到选择器上
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
// 13.若选择键的IO事件是“可读”,则读取数据
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 14.读取数据,然后丢弃
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int length = 0;
while ((length = socketChannel.read(byteBuffer)) > 0) { // channel.read(buf)读取通道的数据时,对于通道来说是读模式,对于ByteBuffer缓冲区来说是写入数据,这时ByteBuffer缓冲区处于写模式
byteBuffer.flip();
byteBuffer.clear();
}
socketChannel.close();
}
// 15.移除选择键
selectionKeys.remove();
}
// 16.关闭连接
serverSocketChannel.close();
}
}
}
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* @author pangjian
* @ClassName DisCardClient
* @Description TODO
* @date 2022/6/10 16:48
*/
public class DisCardClient {
public static void main(String[] args) throws IOException {
InetSocketAddress address =new InetSocketAddress("127.0.0.1",18899);
// 1.获取通道
SocketChannel socketChannel = SocketChannel.open(address);
// 2.切换成非阻塞模式
socketChannel.configureBlocking(false);
// 不断地自旋、等待连接完成,或者做一些其他的事情
while (!socketChannel.finishConnect()) {
}
// 3.分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("hello world".getBytes());
byteBuffer.flip();
// 发送到服务器
socketChannel.write(byteBuffer);
socketChannel.shutdownOutput();
socketChannel.close();
}
}