NIO是什么?怎么使用?存在什么问题?

NIO是什么?

NIO是JDK1.4 java.nio.*包中引入的新的IO库,用来提高速度。

有什么优势,为什么要用NIO?

通过我的这篇文章5种IO模型的原理,我们知道非阻塞IO可以避免硬盘到内核空间的数据复制的阻塞,从而将CPU空闲出来用于其他操作。而IO多路复用可以减少线程数,使用一个线程管理多个IO操作。这明显可以提高CPU的利用率。
而NIO就是利用以上亮点,提高性能的。

怎么使用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正是为了解决这样的问题而生的。

下面使用NIO做一个socket服务器

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();
            }
        }
    }
}

我们再来一个client

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

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已经介绍完了,问题完美解决了吗,其实并没有。

JAVA NIO存在什么问题?

TCP 粘包/拆包问题,接口不够好用的问题,NIO本身存在的bug。
怎么解决呢?
那就要请出我们不得不知道的netty了,下篇文章讲。

你可能感兴趣的:(NIO,java,网络)