一、NIO简介
BIO是基于字节流和字符流进行操作的,而NIO是基于通道和缓冲区进行操作的,数据总是从通道中读取到缓冲区,或者从缓冲区写入到通道中。
BIO是阻塞式的,而NIO是非阻塞式的,读写的效率要更加地高。
二、Channel
Channel有点类似BIO中的流的概念,但是比流要强大地多,主要体现在以下方面:
- 可以支持数据双向流动;
- 可以异步地读写;
- 总是和Buffer进行数据交互;
Channel有以下几个基本类型:
- FileChannel,用于文件IO,无法设置为非阻塞方式;
- DatagramChannel,用于UDP的网络IO;
- SocketChannel,用于TCP的网络IO;
- ServerSocketChannel,用于监听TCP的网络IO,并创建SocketChannel;
关于Channel的使用,给出如下一个简单的示例,将test1中的内容使用channel和buffer拷贝到test2文件中:
public static void main(String[] args) throws IOException {
RandomAccessFile raf1 = new RandomAccessFile("test1.txt", "rw");
RandomAccessFile raf2 = new RandomAccessFile("test2.txt", "rw");
FileChannel fc1 = raf1.getChannel();
FileChannel fc2 = raf2.getChannel();
ByteBuffer bb = ByteBuffer.allocate(100);
// 将文件内容通过channel读到buffer中
log.info("读取字节个数为:{}", fc1.read(bb));
// 反转buffer的状态,由写状态变为读状态
bb.flip();
// 从buffer中将内容读出到channel中
fc2.write(bb);
// buffer中的数据已经写出,但是本身内容还存在,需要清空
bb.clear();
fc1.close();
fc2.close();
raf1.close();
raf2.close();
}
除此之外,我们还可以直接将数据从一个channer传输到另外一个channel中。
三、Buffer
buffer本质上是一块可以读写数据的缓存,一般遵循以下的使用步骤:
- 写入数据到buffer;
- 调用flip方法切换到读模式;
- 从buffer中读取数据;
- 调用clear方法清空整个缓冲区,或者调用compact方法清除已经都去过的数据;
在buffer中有如下几个重要的属性需要掌握:
- capacity,标识缓冲区buffer的字节总大小量;
- position,在写模式下,初始值为0,写完后,切换为读模式,则该值被重置为0;
- limit,在写模式下,等同于capacity,在读模式下,代表已经写入字节数的总量;
Buffer基本类型:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShrotBuffer
这些类型基本上涵盖了你能通过IO发送的数据类型。
channel和buffer之间的读写关系并不是严格的一对一关系,可以通过scatter方式将一个channel中的数据写入多个buffer之中,也可以通过gather方式将多个buffer中的数据读取到一个channel中。
四、Selector
一个Selector线程能注册多个Channel,并对它们进行(非)阻塞式地监听,任意一个Channel有事件就绪,Selector就将该事件交给对应的业务线程进行处理,Selector继续对注册的Channel进行监听。
我们这里以服务端的一个实现来介绍Selector多路复用器的使用:
@Slf4j
public class Test001 {
private static final Boolean RUNNING_FLAG = Boolean.TRUE;
private static Integer connectCount = 0;
private static ByteBuffer buffer = ByteBuffer.allocate(1024);
private static byte[] bytes = new byte[1024];
public static void main(String[] args) throws IOException {
// 创建Selector
Selector selector = Selector.open();
// 创建channnel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// channel设置非阻塞方式
serverSocketChannel.configureBlocking(false);
// 绑定监听端口
serverSocketChannel.socket().bind(new InetSocketAddress(8000));
// channel注册到selector中,并设置为连接接收模式
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// selector会一直循环获取就绪状态的channel
while(RUNNING_FLAG){
// 获取所有就绪状态的channel
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 循环处理所有就绪的channel
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
SelectionKey currentKey = iterator.next();
// 当前正在处理的channel需要移除,避免再次处理
iterator.remove();
// 业务逻辑,对channel进行处理
handleServerSocketChannel(currentKey, selector);
}
}
// 需要结束程序时,关闭selector即可,其上注册的所有channel资源会自动关闭
selector.close();
}
private static void handleServerSocketChannel(SelectionKey key, Selector selector) throws IOException {
if (key.isAcceptable()){
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();
// 为新的接入请求创建一个channel
SocketChannel socketChannel = serverSocketChannel.accept();
// 同样设置非阻塞方式
socketChannel.configureBlocking(false);
// channel注册到selector中,并设置为可读模式
SelectionKey selectionKey = socketChannel.register(selector, SelectionKey.OP_READ);
// 设置当前连接请求channel的编号
selectionKey.attach(++connectCount);
} else if(key.isReadable()){
SocketChannel socketChannel = (SocketChannel)key.channel();
socketChannel.read(buffer);
// 缓冲区由写模式切换为读模式
buffer.flip();
// 获取客户端发送的内容
buffer.get(bytes);
log.info("客户端发送的消息为:{}", new String(bytes, "UTF-8"));
buffer.clear();
// 缓冲区由读模式切换为写模式
buffer.flip();
// 将需要返回给客户端的内容写入buffer
buffer.put("服务端成功接收!".getBytes());
socketChannel.write(buffer);
}
}
}
五、总结
在实际的开发实践中,很少有人直接使用NIO,因为它使用起来比较复杂,而且在linux上还存在Bug,比如epoll的空轮询导致CPU100%的问题;而且重复造轮子会导致开发效率底下,无法保证程序的质量,所以大家都倾向于使用基于NIO实现的中间件工具,比如Netty。
网友评论