• 【Netty】第3章-Java NIO 编程


    【Netty】系列文章目录

    待补充



    1. Java NIO 基本介绍

    1)Java NIO 全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的。

    2)NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。【基本案例】

    3)NIO 有三大核心部分:Channel(通道)Buffer(缓冲区)Selector(选择器)

    4)NIO 是面向缓冲区,或者面向 块 编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。

    5)Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他事情。非阻塞写也是如此,一个线程写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。【后面有案例说明】

    6)通俗理解:NIO 是可以做到用一个线程来处理多个操作。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个线程处理连接。

    7)HTTP2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP1.0 大了好几个数量级。

    8)案例说明 NIO 的 Buffer:

    package com.min.NIO;
    
    import java.nio.IntBuffer;
    
    /**
     * 举例说明Buffer 的使用 (简单说明)
     */
    public class BasicBuffer {
        public static void main(String[] args) {
    
            //1.创建一个Buffer, 大小为 5, 即可以存放5个int类型数据
            IntBuffer intBuffer = IntBuffer.allocate(5);
    
            //2.向buffer 存放数据
            // 2.1 直接put方式存放数据
    //        intBuffer.put(10);
    //        intBuffer.put(11);
    //        intBuffer.put(12);
    //        intBuffer.put(13);
    //        intBuffer.put(14);
    
            // 2.2 通过for循环put存放数据
            for(int i = 0; i < intBuffer.capacity(); i++) {
                intBuffer.put( i * 2);
            }
    
            //3.从buffer 读取数据
            /** flip() 将buffer转换,读写切换(!!!)
             *
             * flip()源码:
             * public final Buffer flip() {
             *      limit = position; //读数据不能超过5
             *      position = 0;
             *      mark = -1;
             *      return this;
             * }
             */
            intBuffer.flip();
    //        intBuffer.position(1);//1,2
    //        System.out.println(intBuffer.get());
    //        intBuffer.limit(3);
            while (intBuffer.hasRemaining()) {
                System.out.println(intBuffer.get());
            }
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    2. NIO 和 BIO 的比较

    1)BIO 以的方式处理数据;而 NIO 以的方式处理数据。块 I/O 的效率比 流 I/O 高很多。

    2)BIO 是阻塞的;而 NIO 是非阻塞的。

    3)BIO 基于字节流和字符流进行操作;而 NIO 基于 Channel(通道) 和 Buffer(缓冲区) 进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器) 用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。


    3. NIO 三大核心原理示意图

    3.1 Selector、Channel 和 Buffer 的关系图(简单版)

    在这里插入图片描述

    3.2 关系图说明

    Channel:通道;Buffer:缓冲区;Selector:选择器;Event:事件

    1)每个 Channel 都会对应一个 Buffer;
    2)Selector 对应一个线程,一个线程对应多个 Channel(Channel可以理解为 连接);
    3)该图反映了有三个 Channel 注册到该 Selector;
    4)程序切换到哪个 Channel 是由事件决定的,Event 就是一个重要的概念;
    5)Selector 会根据不同的事件,在各个通道上切换;
    6)Buffer 就是一个内存块,底层是有一个数组;(所以说 Netty 是面向块编程的,块其实就是指Buffer)
    7)数据的读取和写入是通过 Buffer。这点和BIO有本质区别,BIO中要么是输入流,要么是输出流,不能双向。但是 NIO 的 Buffer 是可以读可以写,需要 filp()方法切换;Channel是双向的,可以返回底层操作系统的情况;比如Linux,底层的操作系统通道就是双向的。


    4. 缓冲区(Buffer)

    4.1 基本介绍

    缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成就是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够追踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer,如图:
    在这里插入图片描述

    4.2 Buffer类及其子类

    1)在NIO中,Buffer是一个顶层父类,它是一个抽象类,常用Buffer子类有:

    常用Buffer子类作用
    ByteBuffer存储字节数据到缓冲区
    ShortBuffer存储字符串数据到缓冲区
    CharBuffer存储字符数据到缓存区
    IntBuffer存储整数数据到缓存区
    LongBuffer存储长整型数据到缓存区
    DoubleBuffer存储小数到缓存区
    FloatBuffer存储小数到缓存区

    2)Buffer类定义了所有的缓冲区都具有的四个属性,用来提供关于其所包含的数据元素的信息:

    属性描述
    capacity容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
    limit表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的
    position位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变该值,为下次读写做准备
    mark标记,定义为 -1

    3)Buffer类相关方法有:
    在这里插入图片描述

    4.3 ByteBuffer

    从前面可以看出对于 Java 中的基本数据类型(boolean除外),都有一个 Buffer 类型与之相对应,最常用的自然是 ByteBuffer类(二进制数据),该类的主要方法如下:
    在这里插入图片描述


    5. 通道(Channel)

    5.1 基本介绍

    1)NIO 的通道类似于流,但有些区别,如下:

    • 通道可以同时进行读写;而流只能读或者只能写;
    • 通道可以实现异步读写数据;
    • 通道可以从缓冲区读数据,也可以写数据到缓冲区。

    2)BIO 中的 流(stream )是单向的,例如 FileInputStream 对象只能进行读取数据的操作;而 NIO 中的通道(Channel)是双向的,可以读操作,也可以写操作。

    Channel
    Buffer

    3)Channel 在NIO中是一个接口

    public interface Channel extends Closeable{}
    
    • 1

    4)常用的 Channel类有:

    • FileChannel:用于文件的数据读写
    • DatagramChannel:用于UDP的数据读写
    • ServerSocketChannel(类似 ServerChannel):用于TCP的数据读写
    • SocketChannel(Socket):用于TCP的数据读写

    5.2 FileChannel类

    FileChannel 主要用来对本地文件进行 IO 操作,常见的方法有:

    • public int read(ByteBuffer dst):从通道读取数据并放到缓冲区中
    • public int write(ByteBuffer src):把缓冲区的数据写到通道中
    • public long transferFrom(ReadableByteChannel src,long position,long count):从目标通道中复制数据到当前通道
    • public long transferTo(long position,long count,WritableByteChannel target):把数据从当前通道复制给目标通道

    5.3 应用案例1-本地文件写数据

    1)实例要求:使用前面学习后的ByteBuffer(缓冲)和FileChannel(通道),将"hello,尚硅谷"写入到file01.txt文件中。

    2)业务流程如下图左半部分:
    在这里插入图片描述
    3)代码展示:(运行程序之后D盘就会创建一个file01.txt文件,并有写入的字符串数据展示)

    package com.min.NIO;
    
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    
    /**
     * FileChannel 应用案例1-本地文件写数据
     *
     * 1.创建文件 file01.txt
     * 2.将“hello,尚硅谷”写入到 file01.txt中
     */
    public class NIOFileChannel01 {
        public static void main(String[] args) throws IOException {
    
            String str = "hello,尚硅谷";
            //创建一个输出流->channel
            FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");
    
            //通过 fileOutputStream 获取对应的 FileChannel
            //这个 fileChannel 真实类型是  FileChannelImpl
            FileChannel fileChannel = fileOutputStream.getChannel();
    
            //创建一个缓冲区 ByteBuffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    
            //将 str 放入 byteBuffer
            byteBuffer.put(str.getBytes());
    
    
            //对byteBuffer 进行flip(反转,由写模式转为读模式)
            byteBuffer.flip();
    
            //将byteBuffer 数据写入到 fileChannel
            fileChannel.write(byteBuffer);
            fileOutputStream.close();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    5.4 应用案例2-本地文件读数据

    1)实例要求:使用前面学习后的ByteBuffer(缓冲)和FileChannel(通道),将file01.txt文件中的数据读入到程序,并显示在控制台屏幕。(因为刚进行案例1创建出来了file01.txt文件,本案例将读取其中的数据信息,若无文件需自己创建)

    2)业务流程如下图右部分:
    在这里插入图片描述
    3)代码展示

    package com.min.NIO;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    
    /**
     * FileChannel 应用案例2-本地文件读数据
     */
    public class NIOFileChannel02 {
        public static void main(String[] args) throws Exception {
    
            //创建文件的输入流
            File file = new File("d:\\file01.txt");
            FileInputStream fileInputStream = new FileInputStream(file);
    
            //通过fileInputStream 获取对应的FileChannel -> 实际类型  FileChannelImpl
            FileChannel fileChannel = fileInputStream.getChannel();
    
            //创建缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
    
            //将 通道的数据读入到Buffer
            fileChannel.read(byteBuffer);
    
            //将byteBuffer 的 字节数据 转成String
            System.out.println(new String(byteBuffer.array()));
            fileInputStream.close();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    5.5 应用案例3-使用一个Buffer完成文件读取、写入

    1)实例要求:使用FileChannel(通道)和read/write方法,用一个Buffer完成文件的拷贝,拷贝一个文本文件1.txt到2.txt。(此时需要在本项目录下手动创建文件1.txt,文件2.txt会自动创建)

    2)业务流程图:
    在这里插入图片描述

    3)代码展示:

    package com.min.NIO;
    
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    
    /**
     * 应用案例3-使用一个Buffer完成文件读取、写入
     */
    public class NIOFileChannel03 {
        public static void main(String[] args) throws Exception {
    
            FileInputStream fileInputStream = new FileInputStream("1.txt");
            FileChannel fileChannel01 = fileInputStream.getChannel();
    
            FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
            FileChannel fileChannel02 = fileOutputStream.getChannel();
    
            ByteBuffer byteBuffer = ByteBuffer.allocate(512);
    
            while (true) { //循环读取
    
                //这里有一个重要的操作,一定不要忘了
                /*
                 public final Buffer clear() {
                    position = 0;
                    limit = capacity;
                    mark = -1;
                    return this;
                }
                 */
                byteBuffer.clear(); //清空buffer,重置标志位
                
                int read = fileChannel01.read(byteBuffer);
                System.out.println("read =" + read);
                if(read == -1) { //表示读完
                    break;
                }
                //将buffer 中的数据写入到 fileChannel02 -- 2.txt
                byteBuffer.flip();
                fileChannel02.write(byteBuffer);
            }
    
            //关闭相关的流
            fileInputStream.close();
            fileOutputStream.close();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    5.6 应用案例4-拷贝文件transferFrom方法

    1)实例要求:使用FileChannel(通道)和transferFrom方法,完成文件的拷贝,拷贝一张图片。(在D盘中放置一张图片a.jpg,完成拷贝后a2.jpg图片会自动创建出来)

    2)代码演示:

    package com.atguigu.nio;
    
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.nio.channels.FileChannel;
    
    public class NIOFileChannel04 {
        public static void main(String[] args)  throws Exception {
            //创建相关流
            FileInputStream fileInputStream = new FileInputStream("d:\\a.jpg");
            FileOutputStream fileOutputStream = new FileOutputStream("d:\\a2.jpg");
    
            //获取各个流对应的filechannel
            FileChannel sourceCh = fileInputStream.getChannel();
            FileChannel destCh = fileOutputStream.getChannel();
    
            //使用transferForm完成拷贝
            destCh.transferFrom(sourceCh,0,sourceCh.size());
            
            //关闭相关通道和流
            sourceCh.close();
            destCh.close();
            fileInputStream.close();
            fileOutputStream.close();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    5.7 关于Buffer和Channel的注意事项和细节

    1)ByteBuffer 支持类型化的 put 和 get,put放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有 BufferUnderflowException 异常。【举例说明】

    2)可以将一个普通Buffer 转成只读Buffer。【举例说明】

    3)NIO 还提供了 MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由 NIO 来完成。【举例说明】

    4)前面我们讲的读写操作,都是通过一个Buffer完成的,NIO还支持通过多个Buffer(即Buffer数组)完成读写操作,即 ScatteringGatering。【举例说明】


    6. Selector(选择器)

    6.1 基本介绍

    1)Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)。

    2)Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。【示意图如6.2所示】

    3)只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。

    4)避免了多线程之间的上下文切换导致的开销。

    6.2 Selector 示意图和特点说明

    在这里插入图片描述
    说明如下:
    1)Netty 的 IO线程 NioEventLoop 聚合了 Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
    2)当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
    3)线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。
    4)由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。
    5)一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

  • 相关阅读:
    kibana 基础操作
    matlab学习笔记(七)
    Unity 基础 之 目录结构解析
    2021年的一封情书:指针
    大数据(5q)ClickHouse客户端
    如何利用 Selenium 对已打开的浏览器进行爬虫
    基于JAVA疫情物质管理系统计算机毕业设计源码+系统+mysql数据库+lw文档+部署
    新人学习笔记之(变量)
    关系抽取(三)实体关系联合抽取:CasRel
    Python面向对象(三)
  • 原文地址:https://blog.csdn.net/weixin_44768683/article/details/125884958