NIO是JDK1.4 java.nio.*包中引入的新的IO库,用来提高速度。
通过我的这篇文章5种IO模型的原理,我们知道非阻塞IO可以避免硬盘到内核空间的数据复制的阻塞,从而将CPU空闲出来用于其他操作。而IO多路复用可以减少线程数,使用一个线程管理多个IO操作。这明显可以提高CPU的利用率。
而NIO就是利用以上亮点,提高性能的。
package com.example.demo.nio;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.FileChannel;
public class FileDemo {
public static void main(String[] args) {
try{
RandomAccessFile aFile=new RandomAccessFile("./.gitignore","rw");
FileChannel channel=aFile.getChannel();
ByteBuffer byteBuffer=ByteBuffer.allocate(48);
int byteRead=channel.read(byteBuffer);
while (byteRead != -1){
System.out.println("read "+byteRead);
byteBuffer.flip();
while (byteBuffer.hasRemaining()){
System.out.println((char) byteBuffer.get());
}
byteBuffer.clear();
byteRead=channel.read(byteBuffer);
}
aFile.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
上面的代码就是使用nio将文件读出来,打印到控制台了。
就上面的例子来说,NIO的优势相对于BIO并不明显,在小文件时有些优势,大文件在测试时优势不明显。其实,NIO更广泛的应用场景是在网络IO时,使用多路复用减少线程数目。见下面这个例子。
传统IO需要开启多个线程来应对多个请求,当请求数上十万百万时,线程将会占据几十G甚至几百G内存,这显然是不合理的。
那么使用线程池不能解决问题吗?
不能。线程池在大量连接时,大部分线程都在队列里,需要轮询大量的队列线程才能找到真正需要发送数据的线程,这会导致大部分在在池子里的线程没有发送数据的需要,而需要的线程又在队列里。显然是低效的。
NIO正是为了解决这样的问题而生的。
public class NoBlockServer {
public static void main(String[] args) throws IOException {
//获取一个通道
ServerSocketChannel server=ServerSocketChannel.open();
//将通道设置为非阻塞模式
server.configureBlocking(false);
//将通道绑定到9091端口
server.bind(new InetSocketAddress(9091));
//生成一个Selector
Selector selector=Selector.open();
//将通道注册到该selector,实现多路复用
server.register(selector, SelectionKey.OP_ACCEPT);
//阻塞并获取有数据的channel
while (selector.select() > 0){
//获取到本次筛选出来所有需要处理的选择键,可以理解为已就绪的channel
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey selectionKey=iterator.next();
//该选择键,也等同于通道表示有新的连接过来
if (selectionKey.isAcceptable()){
//获取该连接
SocketChannel client = server.accept();
//设为非阻塞状态
client.configureBlocking(false);
//注册到selector上,监听读就绪事件
client.register(selector,SelectionKey.OP_READ);
}else if (selectionKey.isReadable()){
//如果是读就绪事件,则获取就绪的channel
SocketChannel client=(SocketChannel) selectionKey.channel();
//分配内存
ByteBuffer buffer=ByteBuffer.allocate(1024);
//生成一个图片相关的filechannel
FileChannel fileChannel=FileChannel.open(Paths.get("./Wechat-server.jpeg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//将读就绪的channel数据循环写入buffer
while (client.read(buffer)>0){
//将已写入数据的buffer改为读模式
buffer.flip();
//将buffer中的数据写入到filechannle
fileChannel.write(buffer);
//重置该buffer
buffer.clear();
}
}
//该选择键已处理,需删除
iterator.remove();
}
}
}
}
public class NoBlockClient {
public static void main(String[] args) throws IOException {
//建立一个通道并绑定到指定host
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9091));
//设置为非阻塞模式
socketChannel.configureBlocking(false);
//打开一个文件通道
FileChannel fileChannel=FileChannel.open(Paths.get("./WechatIMG2.jpeg"),StandardOpenOption.READ);
//生成一个buffer
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//将文件通道里的数据写入到buffer里
while (fileChannel.read(byteBuffer) != -1){
//改变buffer为读模式
byteBuffer.flip();
//从buffer中读出数据,到网络通道
socketChannel.write(byteBuffer);
//重置该buffer
byteBuffer.clear();
}
//关闭两个通道
fileChannel.close();
socketChannel.close();
}
}
运行一下就发现,文件已经上传成功了。
此处我们用了Selector 从而在不开启多线程的情况下,实现了client可以并发上传图片。
如果想交互一下,server在收到图片后给client一个信息
public class NoBlockServer {
public static void main(String[] args) throws IOException {
//获取一个通道
ServerSocketChannel server=ServerSocketChannel.open();
//将通道设置为非阻塞模式
server.configureBlocking(false);
//将通道绑定到9091端口
server.bind(new InetSocketAddress(9091));
//生成一个Selector
Selector selector=Selector.open();
//将通道注册到该selector,实现多路复用
server.register(selector, SelectionKey.OP_ACCEPT);
//阻塞并获取有数据的channel
while (selector.select() > 0){
//获取到本次筛选出来所有需要处理的选择键,可以理解为已就绪的channel
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey selectionKey=iterator.next();
//该选择键,也等同于通道表示有新的连接过来
if (selectionKey.isAcceptable()){
//获取该连接
SocketChannel client = server.accept();
//设为非阻塞状态
client.configureBlocking(false);
//注册到selector上,监听读就绪事件
client.register(selector,SelectionKey.OP_READ);
}else if (selectionKey.isReadable()){
//如果是读就绪事件,则获取就绪的channel
SocketChannel client=(SocketChannel) selectionKey.channel();
//分配内存
ByteBuffer buffer=ByteBuffer.allocate(1024);
//生成一个图片相关的filechannel
FileChannel fileChannel=FileChannel.open(Paths.get("./Wechat-server.jpeg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//将读就绪的channel数据循环写入buffer
while (client.read(buffer)>0){
//将已写入数据的buffer改为读模式
buffer.flip();
//将buffer中的数据写入到filechannle
fileChannel.write(buffer);
//重置该buffer
buffer.clear();
}
ByteBuffer resp=ByteBuffer.allocate(1024);
resp.put("I received your picture".getBytes());
resp.flip();
client.write(resp);
}
//该选择键已处理,需删除
iterator.remove();
}
}
}
}
client端就应该这么写
public class NoBlockClient2 {
public static void main(String[] args) throws IOException {
//新建一个通道并绑定服务器端口
SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",9091));
//设置通道为非阻塞类型
socketChannel.configureBlocking(false);
//新建一个Selector
Selector selector=Selector.open();
//将通道注册到Selector上,指定读类型的选择键
socketChannel.register(selector, SelectionKey.OP_READ);
//创建buffer
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//创建filechannel
FileChannel fileChannel=FileChannel.open(Paths.get("./WechatIMG2.jpeg"), StandardOpenOption.READ);
//将filechannel数据写入到buffer
while (fileChannel.read(byteBuffer) != -1){
//将buffer改为读模式
byteBuffer.flip();
//将buffer里的数据读出,并写入到网络channel
socketChannel.write(byteBuffer);
//重置buffer
byteBuffer.clear();
}
//筛选就绪的选择键
while (selector.select() > 0){
Iterator<SelectionKey> iterator=selector.selectedKeys().iterator();
//轮询就绪的选择键
while (iterator.hasNext()){
SelectionKey selectionKey=iterator.next();
//对读就绪的做处理
if (selectionKey.isReadable()){
SocketChannel readChannel= (SocketChannel) selectionKey.channel();
//将通道里的数据写入到buffer
int readBytes = readChannel.read(byteBuffer);
if (readBytes > 0){
//将buffer改为读模式
byteBuffer.flip();
//将buffer中的数据打印出来
System.out.println(new String(byteBuffer.array(),0,readBytes));
}
}
iterator.remove();
}
}
}
}
运行即可,图片上传成功,并收到回复信息。
NIO中有三个关键概念:
channel: 通道,我理解相当于内核数据空间
buffer:缓冲区,我理解相当于用户数据空间。
selector:选择器,管理多个通道用的,实现多路复用的关键
我们只能操作buffer里的数据。
buffer有多个实现类,shortBuffer,CharBuffer,LongBuffer,ByteBuffer等,我们用的最多的还是ByteBuffer,使用方法都一样,通过allocate()获取一个缓冲区。
buffer中有三个核心概念,capacity,position,limit。
Capacity即是容量,这个容易理解,allocate(1024),capacity就是1024,容量不能被改变。
limit上限,在写模式下,limit跟capacity一致,即可写的最大空间。在读模式下,limit跟position一致,即可读的最大空间。
position,位置,在写模式下,写了多长位置就在哪里,即指向最后写的位置。在读模式下,指的是读到的位置。
那么我们知道,写模式转化为读模式是通过flip()方法,它其实就是将position的值赋给limit,并将position置为0。
而clear方法,是将position的值置为0,并将limit置为capacity,从而实现buffer中数据的“清除”,其实是遗忘,并没有清除实际数据。
ByteBuffer可以分配堆内存或直接内存,如下
ByteBuffer.allocate(1024);
ByteBuffer.allocateDirect(1024);
FileChannel中有多种IO模式可供使用,可以实现零拷贝。
1.使用buffer缓冲区的示例,上面已经展示过了。此时仍有一次拷贝动作。
2.使用内存映射。
public class NioMmap {
public static void main(String[] args) throws IOException {
FileChannel fromChannel=FileChannel.open(Paths.get("./WechatIMG2.jpeg"), StandardOpenOption.READ);
FileChannel toChannel=FileChannel.open(Paths.get("./to.jpeg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.READ);
MappedByteBuffer fromBuffer = fromChannel.map(FileChannel.MapMode.READ_ONLY,0,fromChannel.size());
MappedByteBuffer toBuffer = toChannel.map(FileChannel.MapMode.READ_WRITE,0,fromChannel.size());
byte[] dst = new byte[fromBuffer.limit()];
fromBuffer.get(dst);
toBuffer.put(dst);
}
}
通过内存映射可以实现零拷贝。
3.使用transfer()传输数据
FileChannel fromChannel=FileChannel.open(Paths.get("./WechatIMG2.jpeg"), StandardOpenOption.READ);
FileChannel toChannel=FileChannel.open(Paths.get("./to.jpeg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.READ);
该方法优先使用sendfile+DMA gather 实现零拷贝
其次是使用map映射,如果还不满足则改用传统IO
还有个几乎用不到的方法scatter和gather
分散读取(scatter):将一个通道中的数据分散读取到多个缓冲区中
聚集写入(gather):将多个缓冲区中的数据集中写入到一个通道中
这里不细说了。
NIO已经介绍完了,问题完美解决了吗,其实并没有。
TCP 粘包/拆包问题,接口不够好用的问题,NIO本身存在的bug。
怎么解决呢?
那就要请出我们不得不知道的netty了,下篇文章讲。