NIO详解以及BIO、NIO的区别

一、JavaIO流

  1. Reader:字符格式操作,基于字符的输入操作
  2. Writer:字符格式操作,基于字符的输出操作
  3. InputStream:字节格式操作,基于字节的输入操作
  4. OutputStream:字节格式操作,基于字节的输出操作

按照流的功能可以分为输入流和输出流。
按照操作单元可以分为字节流和字符流。

  1. 使用字节流操作字符时容易出现乱码问题。所以字符流适合对字符处理的IO操作。
  2. 使用字节流适合对图片、音频等文件的处理。

对于一个IO类,要看属于什么类型则看后半部分,要看他的作用则看前半部分。Java Io 流有很多不同功能的类,都是基于输入流、输出流、字节流、字符流变化而来。主要规则:

  1. InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
  2. OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

按照操作方式分类结构图:
在这里插入图片描述
按操作对象分类结构图:
在这里插入图片描述

二、同步阻塞I/O(BIO)

在jdk1.4以前,服务器实现模式为一个连接一个线程,数据的读取写入必须阻塞在一个线程内等待其完成。即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接没有对应的线程,则会一直等待。如果这个连接不做任何事情,则会造成线程浪费。这种模型能够让一个连接专注于自己的I/O,模型比较简单。当并发量较大时,BIO无法创建大量线程处理。

三、同步非阻塞I/O(NIO)

在 Java 1.4 中引入了 NIO 框架,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,它支持面向缓冲的,基于通道的 I/O 操作方法。 服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。也就是NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,由于有复用存在,可以分配50或者100个线程来处理。不用分配10000个线程。对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

1.缓冲区Buffer

缓冲区负责数据存取。本质上是一个数组,用于存储不同数据类型的数据。根据数据类型不同(boolean 除外),提供了相应类型的缓冲区:

  • ByteBuffer(最常用)、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、 FloatBuffer、DoubleBuffer

缓冲区常用操作:

  • allocate():获取非缓冲区。
  • allocateDirect() : 获取直接缓冲区
  • put() : 存入数据到缓冲区中
  • get() : 获取缓冲区中的数据
  • flip() : 切换读取数据模式
  • rewind() : 使缓冲区可重复度
  • clear() : 清空缓冲区,但是缓冲区中的数据依然存在,但是处于“被遗忘”状态

缓冲区中核心属性: 0 <= mark <= position <= limit <= capacity

  1. capacity : buffer的容量,表示缓冲区中最大存储数据的容量。一旦声明不能改变。
  2. limit : 界限,表示缓冲区中可以操作数据的大小。(limit 后数据不能进行读写),写模式下limit等于capacity。读模式下limit等于当前写入信息的末尾位置。
  3. position : 当前读/写位置,表示缓冲区中正在操作数据的位置。写模式下
  4. mark : 标记,表示记录当前 position 的位置。可以通过 reset() 恢复到 mark 的位置
    在这里插入图片描述

缓冲区常用方法示例:

    public static void main(String[] args) {
        String str = "abcde";
        //1. 分配一个指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        System.out.println("-----------------allocate()----------------");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());
        //2. 利用 put() 存入数据到缓冲区中
        buf.put(str.getBytes());
        System.out.println("-----------------put()----------------");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());
        //3. 切换读取数据模式
        buf.flip();
        System.out.println("-----------------flip()----------------");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());
        //4. 利用 get() 读取缓冲区中的数据
        byte[] dst = new byte[buf.limit()];
        buf.get(dst);
        System.out.println("-----------------get()----------------");
        System.out.println(new String(dst, 0, dst.length));
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());
        //5. rewind() : 可重复读
        buf.rewind();
        System.out.println("-----------------rewind()----------------");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());
        //6. mark/reset() : 标记回退
        System.out.println("-----------------reset()----------------");
        buf.get(dst, 0, 2);
        System.out.println(new String(dst, 0, 2));
        System.out.println(buf.position());
        //mark() : 标记
        buf.mark();
        buf.get(dst, 2, 2);
        System.out.println(new String(dst, 2, 2));
        System.out.println(buf.position());
        //reset() : 恢复到 mark 的位置
        buf.reset();
        System.out.println(buf.position());
        //判断缓冲区中是否还有剩余数据
        if(buf.hasRemaining()){
            //获取缓冲区中可以操作的数量
            System.out.println(buf.remaining());
        }
        //7. clear() : 清空缓冲区. 但是缓冲区中的数据依然存在,但是处于“被遗忘”状态
        buf.clear();
        System.out.println("-----------------clear()----------------");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());
        System.out.println((char)buf.get());
    }

输出结果:

-----------------allocate()----------------
0
1024
1024
-----------------put()----------------
5
1024
1024
-----------------flip()----------------
0
5
1024
-----------------get()----------------
abcde
5
5
1024
-----------------rewind()----------------
0
5
1024
-----------------reset()----------------
ab
2
cd
4
2
3
-----------------clear()----------------
0
1024
1024
a

非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中。

在这里插入图片描述
直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率。

在这里插入图片描述
直接缓冲区于非直接缓冲区的区别:

  1. 字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
  2. 直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
  3. 直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。 Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
  4. 字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理
2.通道Channel

通道用于源节点与目标节点的连接。负责缓冲区中数据的传输。Channle本身不存储数据,因此需要配合缓冲区进行传输。通道的概念于流相似,但是通道比流的功能更强大。

  1. 通道可以同时进行读写,流只能进行读或写
  2. 通道可以实现异步读取数据
  3. 通道可以从缓冲区读数据,也可以写数据到缓冲区

通道的主要实现类

  • java.nio.channels.Channel 接口:
    |–FileChannel :用于操作本地文件数据传输
    |–SocketChannel : 用于网络TCP数据传输
    |–ServerSocketChannel :用于网络TCP数据传输
    |–DatagramChannel :用于网络UDP数据传输
    获取通道三种方式
  1. Java 针对支持通道的类提供了 getChannel() 方法
    本地 IO:
    FileInputStream/FileOutputStream
    RandomAccessFile
    网络IO:
    Socket
    ServerSocket
    DatagramSocket

  2. 在 JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法 open()

  3. 在 JDK 1.7 中的 NIO.2 的 Files 工具类的 newByteChannel()

通道之间的数据传输

  1. transferFrom()
  2. transferTo()

使用通道和缓冲区进行文件复制的示例:

    public static void main(String[] args) throws IOException {
        System.out.println("------使用getChannel()获取通道完成文件复制------");
        FileInputStream fis = new FileInputStream("e:/1.gif");;
        FileOutputStream fos = new FileOutputStream("e:/copy.gif");;
        //①获取通道
        FileChannel inChannel = fis.getChannel();;
        FileChannel outChannel = fos.getChannel();;
        //②分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        //③将通道中的数据存入缓冲区中
        while(inChannel.read(buf) != -1){
            buf.flip(); //切换读取数据的模式
            //④将缓冲区中的数据写入通道中
            outChannel.write(buf);
            buf.clear(); //清空缓冲区
        }
        fis.close();fos.close();inChannel.close();outChannel.close();
        System.out.println("------使用open()获取通道完成文件复制-------");

        FileChannel inChannelo = FileChannel.open(Paths.get("e:/1.gif"), StandardOpenOption.READ);
        FileChannel outChannelo = FileChannel.open(Paths.get("e:/openCopy.gif"), StandardOpenOption.WRITE,
                StandardOpenOption.READ, StandardOpenOption.CREATE);

        //内存映射文件
        MappedByteBuffer inMappedBuf = inChannelo.map(FileChannel.MapMode.READ_ONLY, 0, inChannelo.size());
        MappedByteBuffer outMappedBuf = outChannelo.map(FileChannel.MapMode.READ_WRITE, 0, inChannelo.size());

        //直接对缓冲区进行数据的读写操作
        byte[] dst = new byte[inMappedBuf.limit()];
        inMappedBuf.get(dst);
        outMappedBuf.put(dst);
        inChannelo.close();
        outChannelo.close();
        System.out.println("------使用transferFrom/transferTo完成文件复制-------");
        FileChannel inChannelt = FileChannel.open(Paths.get("e:/1.gif"), StandardOpenOption.READ);
        FileChannel outChannelt = FileChannel.open(Paths.get("e:/tansCopy.gif"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
//		inChannel.transferTo(0, inChannel.size(), outChannel);
        outChannelt.transferFrom(inChannelt, 0, inChannelt.size());
        inChannelt.close();
        outChannelt.close();
    }

复制结果:
在这里插入图片描述
分散(Scatter)与聚集(Gather)

  1. 分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区中
  2. 聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中

分散读取与聚集写入示例:

    public static void main(String[] args) throws IOException {
        RandomAccessFile raf1 = new RandomAccessFile("1.txt", "rw");
        //1. 获取通道
        FileChannel channel1 = raf1.getChannel();
        //2. 分配指定大小的缓冲区
        ByteBuffer buf1 = ByteBuffer.allocate(100);
        ByteBuffer buf2 = ByteBuffer.allocate(1024);
        //3. 分散读取
        ByteBuffer[] bufs = {buf1, buf2};
        channel1.read(bufs);
        for (ByteBuffer byteBuffer : bufs) {
            byteBuffer.flip();
        }
        System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
        System.out.println("-----------------");
        System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
        //4. 聚集写入
        RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
        FileChannel channel2 = raf2.getChannel();
        channel2.write(bufs);
    }

字符集:Charset

  1. 编码:字符串 -> 字节数组
  2. 解码:字节数组 -> 字符串

编码与解码示例:

    public static void main(String[] args) throws IOException {
        Charset cs1 = Charset.forName("GBK");
        //获取编码器
        CharsetEncoder ce = cs1.newEncoder();
        //获取解码器
        CharsetDecoder cd = cs1.newDecoder();
        CharBuffer cBuf = CharBuffer.allocate(1024);
        cBuf.put("tcc");

        cBuf.flip();
        //编码
        ByteBuffer bBuf = ce.encode(cBuf);
        for (int i = 0; i < 3; i++) {
            System.out.println(bBuf.get());
        }
        //解码
        bBuf.flip();
        CharBuffer cBuf2 = cd.decode(bBuf);
        System.out.println(cBuf2.toString());
        System.out.println("------------------------------------------------------");
        Charset cs2 = Charset.forName("UTF-8");
        bBuf.flip();
        CharBuffer cBuf3 = cs2.decode(bBuf);
        System.out.println(cBuf3.toString());
    }
3.选择器Selector

阻塞与非阻塞:

  1. 阻塞:IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
  2. 非阻塞:NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

选择器Selectorf正是实现NIO非阻塞模式的核心存在,选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用同一个选择器。

在这里插入图片描述
模拟客户端发送数据:

    public static void main(String[] args) throws IOException {
        //1. 获取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        //2. 切换非阻塞模式
        sChannel.configureBlocking(false);
        //3. 分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        //4. 发送数据给服务端
        Scanner scan = new Scanner(System.in);
        while(scan.hasNext()){
            String str = scan.next();
            buf.put((new Date().toString() + "\n" + str).getBytes());
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
        //5. 关闭通道
        sChannel.close();
    }

模拟服务端接收数据:

    public static void main(String[] args) throws IOException {
            //1. 获取通道
            ServerSocketChannel ssChannel = ServerSocketChannel.open();

            //2. 切换非阻塞模式
            ssChannel.configureBlocking(false);

            //3. 绑定连接
            ssChannel.bind(new InetSocketAddress(9898));

            //4. 获取选择器
            Selector selector = Selector.open();

            //5. 将通道注册到选择器上, 并且指定“监听接收事件”
            ssChannel.register(selector, SelectionKey.OP_ACCEPT);

            //6. 轮询式的获取选择器上已经“准备就绪”的事件
            while(selector.select() > 0){

                //7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();

                while(it.hasNext()){
                    //8. 获取准备“就绪”的是事件
                    SelectionKey sk = it.next();

                    //9. 判断具体是什么事件准备就绪
                    if(sk.isAcceptable()){
                        //10. 若“接收就绪”,获取客户端连接
                        SocketChannel sChannel = ssChannel.accept();

                        //11. 切换非阻塞模式
                        sChannel.configureBlocking(false);

                        //12. 将该通道注册到选择器上
                        sChannel.register(selector, SelectionKey.OP_READ);
                    }else if(sk.isReadable()){
                        //13. 获取当前选择器上“读就绪”状态的通道
                        SocketChannel sChannel = (SocketChannel) sk.channel();

                        //14. 读取数据
                        ByteBuffer buf = ByteBuffer.allocate(1024);

                        int len = 0;
                        while((len = sChannel.read(buf)) > 0 ){
                            buf.flip();
                            System.out.println(new String(buf.array(), 0, len));
                            buf.clear();
                        }
                    }

                    //15. 取消选择键 SelectionKey
                    it.remove();
                }
            }
    }
原创文章 49 获赞 55 访问量 8388

猜你喜欢

转载自blog.csdn.net/tc979907461/article/details/105832865