文章目录
1. 前言
IO模型是使用什么通道对数据进行发送接受,如:文件读取写入,网络传输,选取不同模型很大程度决定了通信性能,在 jdk 1.7
为止已经支持了BIO 同步阻塞
NIO 同步非阻塞
AIO 异步非阻塞
,AIO 虽然提出了方案,但是还没有广泛使用,同时现在网络高并发场景应用基本使用NIO ,如tomcat , flink , spark 等 , 同时已经有了上层封装 如:netty , 因此了解并理解使用NIO 十分有必要,本文在BIO 理解基础上通俗讲解NIO,并介绍记录我对于NIO的理解
2. NIO 几个概念
- java.nio.Buffer
- java.nio.channels.Channel
- java.nio.channels.Selector
- java.nio.channels.Pipe
在NIO中的操作都是从Channel 和 Buffer 开始的 , Channel 就像流 , Buffer 就像接受流的Byte数组 , Buffer与Channel 都是一起配合使用
2.1 Buffer
缓冲区本质上是一块可以写入数据,可以读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
2.1.1 常用实现类
Buffer 是抽象类,常用基本实现类如下(ByteBuffer 是重点 ):
ByteBuffer byteBuffer;
CharBuffer charBuffer;
IntBuffer intBuffer;
ShortBuffer shortBuffer;
LongBuffer longBuffer;
FloatBuffer floatBuffer;
DoubleBuffer doubleBuffer;
基础类型,除了boolean
2.1.2 三个属性 capacity,position和limit
capacity 代表缓存区容量 , 如ByteBuffer可以存放多少个Byte
Buffer 有两个模式 , 读模式 , 写模式 ,对于position和limit 不同情况下含义代表不同
-
写数据时
position 代表当前位置 ,初始值为0,当写入后会自动向前移动到下一个可插入的Buffer单元位置,最大为capacity -1
limit 代表你能写入多少数据,等于capacity
-
读数据时
position 代表从特定地址读,切换读模式时会重置为0,读取后自动移动到下一个可读单元limit 代表最多能读多少数据,因此切换到读模式,会设置为写模式下的position值,也就是说
你写了多少就能读多少
2.2 Channel
Channel 特点
- 双向,可以读取数据,又可以写入数据
- 可以异步,可以同步
- 数据需要先读到buffer , 或者 从buffer 写入
2.2.1 Channel 基本实现
Channel 是一个接口 , 基本实现有如下:
// 文件io
FileChannel
// udp
DatagramChannel
// tcp 客户端
SocketChannel
// tcp 服务端
ServerSocketChannel
2.3 Selector
Selector 则是一个选择器,允许单线程处理多个 Channel
因此在使用Selector 时应该把Channel 注册到Selector中 ,同时也需要将Channel 处于非阻塞模式下
2.3.1 注册时有四个类型常量
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
如果多个可以用 SelectionKey.OP_READ | SelectionKey.OP_WRITE;
2.3.2 SelectionKey
SelectionKey 在注册时会返回的一个对象,里面包含了一些重要的属性,如:
- interest集合
- ready集合
- Channel
- Selector
interest集合
可以判断某个时间是否在监听列表中
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
ready集合
ready 集合是通道已经准备就绪的操作的集合。如上面一样使用,但是还有更方便的api , 直接调用selectionKey
的以下方法:
- isAcceptable
- isConnectable
- isReadable
- isWritable
Channel + Selector
从SelectionKey访问Channel和Selector很简单。如下:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
附加对象
可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。例如,可以附加 与通道一起使用的Buffer,或是包含聚集数据的某个对象。使用方法如下:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
// 在注册的时候添加也可以
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
通过Selector选择通道
一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法。这些方法返回你所感兴趣的事件(如连接、接受、读或写)已经准备就绪的那些通道。换句话说,如果你对“读就绪”的通道感兴趣,select()方法会返回读事件已经就绪的那些通道。
// 阻塞方法
int select()
// 等待timeout 毫秒数 阻塞
int select(long timeout)
// 不阻塞,没有就返回0
int selectNow()
如果通过上面方法知道有值,再使用selectedKeys
获得集合,然后遍历分事件类别处理,处理完成后需要将其剔除集合
,再次有事件会再加入集合, 这里有点像生产者 消费者 的意思
2.4 Pipe
最后的Pipe 则是用于两个线程之间的单向数据连接 , Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
3. 如何使用
3.1 Buffer 使用
// 1. 首先分配大小
// 创建一个2048 的字节缓存区
ByteBuffer buf = ByteBuffer.allocate(2048);
// 1024 的字符缓存区
CharBuffer buf = CharBuffer.allocate(1024);
// 2. 可以写入内容或者从channel 读取到buffer
// 这里的写入后针对的是Buffer是写入
ByteBuffer buffer = ByteBuffer.allocate(2048);
buffer.put((byte) 12);
channel.read(buffer);
// 3. 切换成读模式,然后就可以读了,这里直接转换成字符串,也可以其他方式 buffer.get() 一个一个读
buffer.flip();
System.out.println(new String(buffer.array()));
// 4. 我想再读一遍
// Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素单元。
buffer.rewind();
// 5.通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。例如:
buffer.mark();
buffer.reset();
// 6.我打算接着复用换成写模式,清空后
// 如果调用的是clear()方法,position将被设回0,limit被设置成 capacity的值。换句话说,Buffer 被清空了。Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。
buffer.clear();
// 如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用compact()方法。
buffer.compact();
3.2 Channel + Buffer 使用
这里 File 举例
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
// 从文件中获取通道
FileChannel inChannel = aFile.getChannel();
// 分配空间
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
// 单个读取一定要转换模式
buf.flip();
// 循环单个读
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
// 清除再读也可以
buf.clear();
bytesRead = inChannel.read(buf);
}
// 最后关闭
aFile.close();
3.3 Selector 使用
// 实际使用 先打开一个选择器
Selector selector = Selector.open();
// 设置成非阻塞再注册,不然会报错
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
// 循环获取
int readyChannels = selector.select();
if(readyChannels == 0) continue;
// 有事件进来分类处理,这里需要循环剔除元素,使用迭代器遍历
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
// 分类处理后剔除当前触发事件通道,再有事件会自动添加进来
keyIterator.remove();
}
}
3.4 Pipe 使用
// 创建管道
Pipe pipe = Pipe.open();
// 打开sink 写管道
Pipe.SinkChannel sinkChannel = pipe.sink();
// 写入数据到buffer 后
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
// 切换模式
buf.flip();
// 写入管道
while(buf.hasRemaining()) {
sinkChannel.write(buf);
}
// 读取source管道
Pipe.SourceChannel sourceChannel = pipe.source();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buf);
4. 聊天室示例
4.1 读打印工具类ChannelUtil
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
public class ChannelUtil {
public static String println(SelectionKey next) throws IOException {
if (next.isReadable()) {
SocketChannel channel = (SocketChannel) next.channel();
channel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(2048);
try {
channel.read(buffer);
String s = new String(buffer.array());
System.out.println(s);
retu.2rn s;
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
4.2 服务端 ServerTest
import cn.hutool.core.util.StrUtil;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class ServerTest {
public static InetSocketAddress SERVER = new InetSocketAddress("127.0.0.1", 9292);
private static Selector selector;
public static void main(String[] args) throws IOException {
// 选择器打开
selector = Selector.open();
// 通道打开
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 绑定地址
serverSocketChannel.bind(SERVER);
// 非阻塞
serverSocketChannel.configureBlocking(false);
// 注册到选择器 并且监听对应时间
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环监听
while (true) {
if (selector.select(2000) > 0) {
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
SocketChannel client = serverSocketChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println(client.getRemoteAddress() + " isAcceptable");
} else if (key.isReadable()) {
String message = ChannelUtil.println(key);
sendOther(message,key);
}
keyIterator.remove();
}
}
}
}
private static void sendOther(String message, SelectionKey key) {
// 判断为空 即可 , 随意
// return str == null || str.length() == 0;
if (StrUtil.isEmpty(message)){
return ;
}
Set<SelectionKey> keys = selector.keys();
for (SelectionKey selectionKey : keys) {
if (selectionKey!=null&& selectionKey.channel() instanceof SocketChannel &&!selectionKey.equals(key)){
SocketChannel other = (SocketChannel) selectionKey.channel();
try {
other.configureBlocking(false);
other.write(ByteBuffer.wrap((message).getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4.3 客户端 ClientTest
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
public class ClientTest {
private static String name;
private static SocketChannel socketChannel;
private static Selector selector;
public static void main(String[] args) throws IOException, InterruptedException {
selector = Selector.open();
socketChannel = SocketChannel.open(ServerTest.SERVER);
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
name = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(name + " is ok !!!");
new Thread(() -> {
while (true) {
try {
readMessageInfo();
TimeUnit.SECONDS.sleep(1);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Scanner scanner = new Scanner(System.in);
while (true) {
if (scanner.hasNext()){
sendMessageInfo(scanner.next());
}
TimeUnit.SECONDS.sleep(1);
}
}
public static void readMessageInfo() throws IOException {
if (selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey next = iterator.next();
ChannelUtil.println(next);
iterator.remove();
}
}
}
public static void sendMessageInfo(String message) throws IOException {
socketChannel.write(ByteBuffer.wrap((name + ":" + message).getBytes()));
}
}