zoukankan      html  css  js  c++  java
  • 01 Java的NIO三大组件以及buffer的原理以及应用

    一、Java的NIO(non-blocking IO:非阻塞IO)的常识

    1-1 NIO相关的三大组件

    三大组件:channel,buffer以及selector

    • channel:读写数据的双向通道,配合buffer一起使用
    • buffer: 就是存储数据的缓冲区,配合buffer一起使用,即从 channel 将数据读入 buffer,也可以将 buffer 的数据写入 channel

    • selector:选择器,配合线程来管理多个 channel,获取这些 channel 上发生的事件

    channel的类型

    名称 作用 备注
    FileChannel 文件数据传输通道
    DatagramChannel UDP网络编程网络传输通道
    SocketChannel TCP网络传输通道 客户端/服务端都可以用
    ServerSocketChannel TCP网络传输通道 服务器专用

    buffer的类型

    名称 作用
    ByteBuffer(抽象类) 字节为单位缓冲数据 MappedByteBuffer, DirectByteBuffer, HeapByteBuffer是bytebuffer的实现类 最常用
    其他:ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer,CharBuffer
    

    1-2 三大组件之selector的概述

    服务器设计架构的发展历史(selector的由来)

    发展1:早期服务器处理客户端连接

    特点

    • 每个客户端的socket连接都会分配一个线程进行处理

    缺陷

    • 内存占用高(每个线程占用几M内存)
    • 线程上下文切换成本高(对CPU要求高)
    • 只适合连接数少的场景

    发展2:采用线程池处理客户端连接(线程池可以控制最大线程数,从而控制线程资源开销)

    特点

    • 每个线程同一时间只能处理一个客户端连接,

    缺陷

    • 阻塞模式下,线程只能处理一个sokcet连接,即使这个连接没有任何数据读取,也要干等,线程利用率低
    • 仅适合短连接场景(短连接的目的是为了充分利用线程资源,每个线程单位时间段能够处理多个请求)

    发展3:selector 版的服务器设计处理客户端连接请求

    特点:selector 的作用就是配合线程来管理多个 channel,获取这些 channel 上发生的事件

    • 这些 channel 工作在非阻塞模式下,线程不会死等channel的数据。适合连接数特别多,但流量低的场景(low traffic)
    • 当有读写事件发生的时候,selector就去处理并分配线程去处理,读写事件结束,线程会被释放去处理其他连接,这样让线程能够只处理读写,不会出现空等的现象

    二、缓冲区(buffer)的原理与使用方法

    重点理解bytebuffer读写模式的设计思路

    2-1 bytebuffer使用的简单实例

    代码

    package part1;
    import lombok.extern.slf4j.Slf4j;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    @Slf4j
    public class test1 {
        public static void main(String[] args){
    
            try(FileChannel channel = new FileInputStream("data.txt").getChannel())          {
                ByteBuffer buffer = ByteBuffer.allocate(10);
                int num = 1;
                while(true){
                    /*channel与buffer配合使用:将channel中的数据读取到buffer当中*/
                    int len = channel.read(buffer);
                    log.warn("the {} time to access and read {} bytes!",num++,len);
                    /*通过返回结果是否为-1判断buffer是否读取完毕*/
                    if(len == -1)
                        break;
                    buffer.flip();     /*切换读模式*/
                    while(buffer.hasRemaining()){
                        byte b = buffer.get();
                        System.out.printf("%c ",b);
                    }
                    System.out.printf("
    ");
                    buffer.clear();   /*切换写模式*/
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    执行结果

    设置buffer的大小为10个字节,那么从channel中获取的数据最多为10个字节。上面的实例中,

    14个字节分2次读取。当数据读取完毕,channel.read(buffer)会返回-1.

    10:01:57.776 [main] WARN part1.test1 - the 1 time to access and read 10 bytes!
    w o y a o s h a n g 
    10:01:57.785 [main] WARN part1.test1 - the 2 time to access and read 4 bytes!
    g a n g 
    10:01:57.785 [main] WARN part1.test1 - the 3 time to access and read -1 bytes!
    
    bytebuffer的使用规范
    • bytebuffer同一时刻要么读要么写,不能读写同时进行(半双工模式,类似于管道)
    • 特别注意读写模式的切换
    1. 向 buffer 写入数据,例如调用 channel.read(buffer)
    2. 调用 flip() 切换至读模式
    3. 从 buffer 读取数据,例如调用 buffer.get()
    4. 调用 clear() 或 compact() 切换至写模式
    5. 重复 1~4 步骤
    

    2-2 bytebuffer的内部结构刨析

    bytebuffer的部分源码

    public abstract class ByteBuffer
        extends Buffer
        implements Comparable<ByteBuffer>
    {
    
        // These fields are declared here rather than in Heap-X-Buffer in order to
        // reduce the number of virtual method invocations needed to access these
        // values, which is especially costly when coding small buffers.
        //
        final byte[] hb;                  // Non-null only for heap buffers
        final int offset;
        boolean isReadOnly;                 // Valid only for heap buffers
        // Creates a new buffer with the given mark, position, limit, capacity,
        // backing array, and array offset
        //
        ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
                     byte[] hb, int offset)
        {
            super(mark, pos, lim, cap);
            this.hb = hb;
            this.offset = offset;
        }
        ......
    }
    

    重要的属性

    capacity      // 容量
    position      // 位置
    limit         // 限制
    
    bytebuffer的读写模式实现原理图解(重要)

    基本思想:双指针策略


    2-3 bytebuffer的常用API使用方法

    为什们buffer分配为直接内存相比堆内存读写效率高

    2-3-1 空间分配的API
    Bytebuffer buf = ByteBuffer.allocate(16);         // 分配堆内存用于buffer
    Bytebuffer buf = ByteBuffer.allocateDirect(16)    // 分配直接内存用于buffer
    

    Java的buffer的空间分配注意点

    • 堆内存受垃圾回收器管理,数据的读写效率不如直接内存,需要拷贝2次数据
    • 直接内存由操作系统分配,分配的代价大,但是读写效率比较高。只需要拷贝一次数据,不受GC管理。
    import java.nio.ByteBuffer;
    public class test2 {
        public static void main(String[] args) {
            System.out.println(ByteBuffer.allocate(16).getClass());       // class java.nio.HeapByteBuffer
            System.out.println(ByteBuffer.allocateDirect(16).getClass()); // class java.nio.DirectByteBuffer
        }
    }
    
    2-3-2 buffer的写入数据API

    方式1:利用channel的方法写入数据,从channel中读取数据写入buffer

    int readBytes = channel.read(buf);
    

    方式2:使用put方法写入数据

    buf.put((byte)127);
    
    2-3-3 buffer 的读取数据的API

    方式1:利用channel的方法读取数据,利用channel将数据从buffer中读出然后放入channel

    int writeBytes = channel.write(buf);
    

    方式2:使用buffer.get方法读取数据

    byte b = buf.get();
    

    get 方法会让 position 读指针向后走,如果想重复读取数据

    • 可以调用 rewind 方法将 position 重新置为 0
    • 或者调用 get(int i) 方法获取索引 i 的内容,它不会移动读指针
    2-4-4 buffer的mark和 reset方法(成对使用)

    使用场景:mark 在读取时做一个标记,即使 position 改变,只要调用 reset 就能回到 mark 的位置

    注意点:rewind 和 flip 都会清除 mark 位置

    2-4-5 字符串与 ByteBuffer 转化三种方式

    实例

    import java.nio.ByteBuffer;
    import java.nio.charset.StandardCharsets;
    
    public class test3 {
        public static void main(String[] args) {
            /*将字符串写入到bytebuffer中*/
            // ========================方式1:使用getBytes然后通过put写入===================================
            ByteBuffer buffer = ByteBuffer.allocate(16);
            // getBytes将字符串转换为byte数组写入到bytebuffer中
            buffer.put("hello".getBytes());                                    // 注意当前buffer模式还是写模式
            buffer.flip();
            String str = StandardCharsets.UTF_8.decode(buffer).toString();    // 将bytebuffer中的数据再转换为字符串
            System.out.println(str);
    
            // ========================方式2:使用nio的charset直接进行转换===================================
            ByteBuffer buffer2 = StandardCharsets.UTF_8.encode("hello");  //当前buffer模式是读模式
            String str1 = StandardCharsets.UTF_8.decode(buffer2).toString();
            System.out.println(str1);
    
            // ========================方式3:使用wrap方法结合byte[]数组=====================================
            ByteBuffer buffer3 = ByteBuffer.wrap("hello".getBytes());         // 当前buffer是读模式
            String str2 = StandardCharsets.UTF_8.decode(buffer3).toString();
            System.out.println(str2);
        }
    }
    

    执行结果

    hello
    hello
    hello
    
    2-4-6 Scatter Reads/Writes(分散读取,集中写入的思想理解)

    作用:将数据分区间读取到不同的buffer/将不同buffer的数组合并放入到同一个文件中。

    好处:分散读,集中写有助于减少数据在buffer之间的拷贝

    文本文件:words.txt

    onetwothree
    

    分散读取测试代码:将数据读取到多个buffer

    package part1;
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    import java.nio.charset.StandardCharsets;
    public class test4 {
        public static void main(String[] args) {
            try (RandomAccessFile file = new RandomAccessFile("words.txt", "rw")) {
                FileChannel channel = file.getChannel();
                /*分配3个byte buffer,并默认设置为写模式,同一个数据源中将数据分配到3个byte buffer*/
                ByteBuffer a = ByteBuffer.allocate(3);
                ByteBuffer b = ByteBuffer.allocate(3);
                ByteBuffer c = ByteBuffer.allocate(5);
                /*关键:传入bytebuffer数组进行分散读取,将数据读取到多个bytebuffer中*/
                channel.read(new ByteBuffer[]{a, b, c});
                a.flip();
                printBytebuffer(a);
                b.flip();
                printBytebuffer(b);
                c.flip();
                printBytebuffer(c);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        static void printBytebuffer(ByteBuffer tmp){      // 注意:传入的bytebuffer必须时写模式
            System.out.println(StandardCharsets.UTF_8.decode(tmp).toString());
        }
    }
    

    执行结果

    one
    two
    three
    

    分散写入测试代码:将数据从多个buffer写入到同一个文件中(避免多余的拷贝)

    package part1;
    import java.io.FileNotFoundException;
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    import java.nio.charset.StandardCharsets;
    public class test5 {
        public static void main(String[] args){
            try (RandomAccessFile file = new RandomAccessFile("words.txt", "rw")) {
                FileChannel channel = file.getChannel();
                ByteBuffer d = ByteBuffer.allocate(4);
                d.put(new byte[]{'f', 'o', 'u', 'r'});
                d.flip();
                ByteBuffer e = ByteBuffer.allocate(4);
                e.put(new byte[]{'f', 'i', 'v', 'e'});
                e.flip();
                channel.position(11);
                channel.write(new ByteBuffer[]{d, e});
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        static void printBytebuffer(ByteBuffer tmp){           // 打印buffer内容,注意buffer必须是读模式
            System.out.println(StandardCharsets.UTF_8.decode(tmp).toString());
        }
    }
    

    words.txt结果

    onetwothreefourfive
    

    三 应用bytebuffer的API简单处理黏包与半包问题

    3-1 黏包与半包问题概述

    情景:网络上客户端有多条数据发送给服务端,数据之间使用 进行分隔,比如传输前可能分三次调用传输:

    第一次:Hello,world
    
    第二次:I'm zhangsan
    
    第三次:How are you?
    
    

    实际服务端可能收到了2个数据包

    数据包1:Hello,world
    I'm zhangsan
    Ho
    数据包2:w are you?
    
    

    黏包(数据包1就是黏包):应用层数据在传输过程中多条记录合并到一个数据包中。

    • 黏包的原因主要是为了提高传输效率,将多条小的记录直接放入一个包发送。

    半包(数据包2就是半包):应用层单条数据记录在传输过程中被分到多个包中。

    • 半包的主要是由于接受方buffer的大小有限或者应用层单条数据记录确实比较大

    3-2 使用buffer的API模拟解决这个问题

    实例代码

    package part1;
    import java.nio.ByteBuffer;
    import java.nio.charset.StandardCharsets;
    
    public class test6 {
        public static void main(String[] args) {
            ByteBuffer source = ByteBuffer.allocate(32);
    
            source.put("Hello,world
    I'm zhangsan
    Ho".getBytes());    // 数据包1
            split(source);
            source.put("w are you?
    haha!
    ".getBytes());              // 数据包2
            split(source);
        }
        // 基本思想:按照应用层分割符读取读取数据,如果buffer中最终数据不是以
    结尾,
        // 保留这个数据直到倒数第一个
    不进行读取,将其compact后,再一个数据包数据读入之后再处理。
        private static void split(ByteBuffer source) {
            source.flip();                                 // step1: 转换为读模式
            int oldLimit = source.limit();                
            for (int i = 0; i < oldLimit; i++) {           // step2:按照分隔符读取数据记录,没有分隔符的数据留在buffer中
                if (source.get(i) == '
    ') {
                    //分配用于读取的bytebuffer接受传输过来的一条数据记录,当前记录大小 = 当前位置+1-position
                    ByteBuffer target = ByteBuffer.allocate(i + 1 - source.position());
                    source.limit(i + 1);                   // 0 ~ limit
                    target.put(source);                    // 从source 读,向 target 写
                    target.flip();
                    printBytebuffer(target);
                    source.limit(oldLimit);
                }
            }
            source.compact();                             // step3: 对buffer中没有读完的数据进行处理,移动到开头,转换为写模式
        }
        static void printBytebuffer(ByteBuffer tmp){  
            // 打印buffer内容,注意buffer必须是读模式
            System.out.print(StandardCharsets.UTF_8.decode(tmp).toString());
        }
    }
    
    

    执行结果实现数据记录的分隔

    Hello,world
    I'm zhangsan
    How are you?
    haha!
    

    注意点

    • 上面代码中逐个找分隔符,数据的处理效率不高,有其他更加高效的方式
    • 黏包/半包问题属于较为底层的问题,实际开发中,netty会帮助我们处理黏包/半包问题

    参考资料

    01 Netty基础课程

  • 相关阅读:
    10-padding(内边距)
    09-盒模型
    07-css的继承性和层叠性
    Python之路【第09章】:Python模块和包的详细说明
    Python之路【第10章】:程序异常处理
    Python之路【第09章】:Python模块
    排序算法—冒泡排序算法
    算法总结
    递归函数与二分法
    练习题
  • 原文地址:https://www.cnblogs.com/kfcuj/p/14706856.html
Copyright © 2011-2022 走看看