Netty应用(二) 之 ByteBuffer

目录

4.ByteBuffer详解

4.1 ByteBuffer为什么做成一个抽象类?

4.2 ByteBuffer是抽象类,他的主要实现类为

4.3 ByteBuffer的获取方式

4.4 核心结构(NIO的ByteBuffer底层是啥结构,以及读写模式都是根据这些核心结构进行维护的)

4.4 核心API

4.5 字符串操作

4.6 粘包与半包


4.ByteBuffer详解

4.1 ByteBuffer为什么做成一个抽象类?

回答这个问题之前,先说一下:啥时候设计成抽象类,啥时候设计成接口?

1.针对抽象的概念(名词)设计成抽象类。eg:动物,形状,汽车这些类设计成抽象类

2.针对抽象的动作,功能(动词)设计成接口。eg:DAO设计成接口,Service设计成接口

对于ByteBuffer的设计也是符合这项规定的,ByteBuffer是缓冲区,是一个名词,所以设计成抽象类。

但是同样存在特例,如InputStream,OutputStream,流是动词,应该设计成接口,但是java设计成了抽象类,因为早期java设计不是很通透。

为什么接口可以多继承?

打个比方,一个人有多少功能?一个人可以拥有很多个功能,对吧。所以一个类(一个人)可以继承(可以拥有)很多接口(很多功能)

4.2 ByteBuffer是抽象类,他的主要实现类为

1.HeapBuffer 堆ByteBuffer ----》占用的是JVM内的堆内存 ----》读写操作 效率低 会收到GC影响

2. MappedByteBuffer(DirectByteBuffer) ----》占用的是OS内存----》读写操作 效率高 不会收到GC影响 。 不主动析构,会造成内存的泄露

  • 分析一下HeapBuffer与MappedByteBuffer的区别

HeapBuffer:占用的是JVM内的堆内存 ----》读写操作 相对效率低 会收到GC影响

Netty应用(二) 之 ByteBuffer_第1张图片

MappedByteBuffer: 占用的是OS内存----》读写操作 相对效率高 不会收到GC影响。但是如果不主动去释放OS的内存,会造成内存泄漏!啥是内存泄漏?内存泄漏就是前一个用户所开辟的内存空间由于忘记释放,所以无法再被之后的其他用户正常使用了

Netty应用(二) 之 ByteBuffer_第2张图片

为什么MappedByteBuffer相对效率要高?

很明显,通过图中可以看出,MappedByteBuffer是在OS中直接申请的内存空间,而HeapByteBuffer是在JVM进程中申请的堆内存空间,HeapByteBuffer操作的时候,中间还隔着OS操作系统,自然效率低一些。

补充:

JVM其实就相当于操作系统启动的一个进程,进程占用着一块内存空间,进程中有栈,堆,静态代码区域等,堆空间主要存储的就是new的对象,栈存储一些局部变量,参数等,静态代码区域存储的就是代码。操作系统本质上也是由很多进程组成的一个系统,来管理整个硬件。

  • 内存溢出和内存泄漏有什么区别?

内存泄漏:

举个例子:明明具有100M内存,但是实际只处理了80M内存就不能再继续增加处理了。剩余的20M内存就内存泄漏了。

内存泄漏的原因:

1.不主动析构(没有主动释放内存)

分析:前一个用户申请了20M内存空间但是没有释放并且这一个用户的使用已经结束了,后一个用户去使用的时候,100M内存只能正常使用80M内存,按理说应该可以正常使用100M内存,所以20M内存发生内存泄漏

2.内存碎片

Netty应用(二) 之 ByteBuffer_第3张图片

假设说内存碎片1的大小为20M,内存碎片2的大小为10M。假设说内存空间就是剩余30M(内存碎片1和内存碎片2的内存总大小),但此时我们申请一块30M大小的空间,申请失败!!因为我们目前只有20M和10M的内存空间大小,没有一块连续的30M空间大小。这也造成了内存泄漏!

现在有很多优秀的内存管理器,来减少内存碎片的大小。但我们不能保证内存中不存在碎片,内存碎片(缝隙一定存在)一定存在!而是可以做到让碎片足够的小,这样才能提升空间利用效率。

内存管理器哪里使用的多?redis中用的多,redis是一个基于内存的nosql产品,它对内存的管理要求很高。它的内存管理整合第三方的内存管理器,如gemalloc,tcmalloc等。强大的内存管理器的一个指标就是:内存碎片的管理。

内存溢出:

举个例子:具有100M内存,但实际操作的过程中,把120M的真实数据一次性加入到内存中,由于超过了100M内存的真实大小,所以内存溢出。

总结:

内存溢出就是最后的一个表现,造成内存溢出的一个很大的原因是因为过程中存在内存泄漏。

想想是不是这个道理,在造成一个内存溢出结果的过程中,因为一定会有内存碎片----》所以一定会有内存泄漏。

4.3 ByteBuffer的获取方式

1.ByteBuffer.allocate(10);//一旦分配空间,不可以动态调整。但是在Netty中的ByteBuffer类就是具有动态调整的功能,可以进行动态修改调整所分配的空间大小。因为Netty对ByteBuffer进行了一系列的优化封装,使其可以动态调整空间大小

2.encode(); 注释:encode()方法会让字符串类型的数据转换成ByteBuffer类型

4.4 核心结构(NIO的ByteBuffer底层是啥结构,以及读写模式都是根据这些核心结构进行维护的)

ByteBuffer是一个类似数组的结构,整个结构中包含以下三个主要的指针状态:

以下这三个状态维护出了ByteBuffer的读写模式,读写模式的切换背后实际上都是由这三个状态的不断变化而实现的。

1.Capacity

buffer的容量,类似于数组的size,指向ByteBuffer最后一个位置

2.Position

buffer当前缓存的下标。读取操作时记录下一个待读取的位置下标。写操作时记录下一个待写入的位置下标。位置下标是从0开始,每读取一次,下标+1

3.Limit

读写操作时的一个限制下标。在读操作时,Limit设置你还能再读取多少个字节的数据。在写操作时,设置你还能再写入多少个字节的数据。

  • 画图

Netty应用(二) 之 ByteBuffer_第4张图片

Netty应用(二) 之 ByteBuffer_第5张图片

  • 为什么clear方法和compact方法都是把Buffer转换成写模式,为什么还需要compact?画图来说明:

单独演示compact方法底层三个指针是如何变化的:

Netty应用(二) 之 ByteBuffer_第6张图片

  • 代码演示
public class TestNIO4 {

    public static void main(String[] args) {
        TestNIO4 test = new TestNIO4();
        test.testState05();
    }

    public void testState01() {
        //创建完Buffer,默认是写模式
        ByteBuffer buffer = ByteBuffer.allocate(10);

        System.out.println("buffer.capacity() = " + buffer.capacity());//10
        System.out.println("buffer.position() = " + buffer.position());//0
        System.out.println("buffer.limit() = " + buffer.limit());//10

    }

    public void testState02() {
        //创建完Buffer,默认是写模式
        ByteBuffer buffer = ByteBuffer.allocate(10);
        //写入四个字节的数据
        buffer.put(new byte[]{'a','b','c','d'}) ;
        //
        System.out.println("buffer.capacity() = " + buffer.capacity());//10
        System.out.println("buffer.position() = " + buffer.position());//4
        System.out.println("buffer.limit() = " + buffer.limit());//10
    }

    public void testState03() {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put(new byte[]{'a','b','c','d'}) ;

        //改为读模式
        buffer.flip();

        //
        System.out.println("buffer.capacity() = " + buffer.capacity());//10
        System.out.println("buffer.position() = " + buffer.position());//0
        System.out.println("buffer.limit() = " + buffer.limit());//4
    }
    public void testState04() {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put(new byte[]{'a','b','c','d'}) ;

        //调用clear,让position指向第一个索引位置
        buffer.clear();

        //
        System.out.println("buffer.capacity() = " + buffer.capacity());//10
        System.out.println("buffer.position() = " + buffer.position());//0
        System.out.println("buffer.limit() = " + buffer.limit());//10
    }
    public void testState05() {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put(new byte[]{'a','b','c','d'}) ;

        buffer.flip();
        System.out.println("(char) buffer.get() = " + (char) buffer.get());//a
        System.out.println("(char) buffer.get() = " + (char) buffer.get());//b

        System.out.println("buffer.capacity() = " + buffer.capacity());//10
        System.out.println("buffer.position() = " + buffer.position());//2
        System.out.println("buffer.limit() = " + buffer.limit());//4

        System.out.println("----------------------------------");

        //把未读取的c和d移到最前面索引位置保存起来 position指向下一个d后面的一个索引位置(未读取的索引位置)开始写入
        buffer.compact();

        System.out.println("buffer.capacity() = " + buffer.capacity());//10
        System.out.println("buffer.position() = " + buffer.position());//2
        System.out.println("buffer.limit() = " + buffer.limit());//10

        //转换为读模式
        buffer.flip();
        System.out.println("(char) buffer.get() = " + (char) buffer.get());//c
        System.out.println("(char) buffer.get() = " + (char) buffer.get());//d


    }


}

全部测试成功,主要是对读写模式切换的理解即可,以及对Buffer读写模式切换底层核心结构的理解!

  • 总结

最后总结一下:其实在日常开发过程中,无需知道在读写模式切换时底层核心结构标记是如何切换的,但是对于一些复杂开发,我们需要理解底层核心结构标记的切换

写入Buffer数据之前要设置写模式

1. 写模式

1. 新创建的Buffer,默认是写模式

2. 调用了clear,compact方法

读取Buffer数据之前要设置读模式

2. 读模式

1. 调用flip方法

4.4 核心API

Netty应用(二) 之 ByteBuffer_第7张图片

  • 向Buffer缓冲区写入数据(写模式,创建一个ByteBuffer时默认为写模式 或 clear方法调用 或 compact方法调用)

1.channel的read方法

channel.read(buffer) --->向ByteBuffer这一缓冲区中写入数据

2.buffer的put方法

buffer.put(byte) ---->一个字节一个字节的写入数据到ByteBuffer 如:buffer.put((byte) 'a')

buffer.put(byte[]) ---->向ByteBuffer缓冲区中写入一个字节数组

  • 从Buffer缓冲区中读出数据(读模式)

1.channel的write方法 ---》把ByteBuffer的数据读出到文件中

2.buffer的get()方法调用 ----》每调用一次get(),position位置向后移动一位

3.rewind方法(像手风琴一样来回拉扯)----》将position指向重置为0,用于重新读取数据

4.mark & reset方法 ---》这两个方法结合使用,通过mark方法进行标记一个 position位置,当调用reset方法时,会跳转到上一次mark标记的position的位置坐标,并且从这个position指向的位置坐标开始重新执行。记住一点:mark和reset方法都是结合使用的!!!!

5.get(i)方法 -----》获取特定索引位置上的数据,与get()的不同之处在于:get(i)可以特定获取某一索引位置的数据值 并且 get(i)方法不会改变position的指向!

  • 代码演示
public class TestNIO5 {

    public static void main(String[] args) {

        ByteBuffer buffer = ByteBuffer.allocate(10) ;
        buffer.put(new byte[]{'a','b','c','d'}) ;

        buffer.flip();

        while (buffer.hasRemaining()) {
            System.out.println("(char) = " + (char) buffer.get()) ;
        }
        //此时此刻
        //position = 4 limit=4 capacity=10
        System.out.println("--------------------------");

        //把position置为0
        buffer.rewind();

        while (buffer.hasRemaining()) {
            System.out.println("(char) buffer.get() = " + (char) buffer.get());
        }

    }

}
public class TestNIO6 {

    public static void main(String[] args) {

        ByteBuffer buffer = ByteBuffer.allocate(10);

        buffer.put(new byte[]{'a','b','c','d'}) ;

        buffer.flip();
        System.out.println("buffer.get() = " + (char) buffer.get());//a
        System.out.println("buffer.get() = " + (char) buffer.get());//b

        //记录此时position的位置:2
        buffer.mark();
        System.out.println("buffer.get() = " + (char) buffer.get());//c
        System.out.println("buffer.get() = " + (char) buffer.get());//d

        //恢复成上一次记录的position所指向的位置:2
        buffer.reset();
        System.out.println("buffer.get() = " + (char) buffer.get());//c
        System.out.println("buffer.get() = " + (char) buffer.get());//d
    }

}
public class TestNIO7 {

    public static void main(String[] args) {

        ByteBuffer buffer = ByteBuffer.allocate(10) ;
        buffer.put(new byte[]{'a','b','c','d'}) ;

        buffer.flip();
        System.out.println("(char)buffer.get() = " + (char) buffer.get());//a
        //虽然此时position指向1,但是由于调用的是get(0),所以输出的还是第一个数据值
        System.out.println("(char)buffer.get(0) = " + (char) buffer.get(0));//a

        System.out.println(buffer.position());//1
    }

}

4.5 字符串操作

  • 字符串存储到Buffer缓冲区中

1.使用allocate方法创建ByteBuffer

Netty应用(二) 之 ByteBuffer_第8张图片

2.使用encode方法创建ByteBuffer缓冲区

Netty应用(二) 之 ByteBuffer_第9张图片

flip方法源码:

Netty应用(二) 之 ByteBuffer_第10张图片

3.使用新的方法创建ByteBuffer缓冲区

Netty应用(二) 之 ByteBuffer_第11张图片

4.使用wrap方法创建ByteBuffer缓冲区

Netty应用(二) 之 ByteBuffer_第12张图片

  • Buffer缓冲区的数据转换成字符串

Netty应用(二) 之 ByteBuffer_第13张图片

补充演示:

Netty应用(二) 之 ByteBuffer_第14张图片

4.6 粘包与半包

粘包:下一句话不完整的粘到上一句话的后面,这就是粘包

半包:不完整的一句话就是半包

eg:

在客户端-服务端网络通信的过程中,客户端原本想要发送三句话给服务端:

1.Hi leomessi\n

2.I love you\n

3.Do you love me?\n

但是由于服务端开辟的ByteBuffer缓冲区大小问题,假设服务端ByteBuffer设为20个字节大小,那么最终服务端接收的时候:第一次ByteBuffer接收读取到的是:Hi leomessi\nI love y,第二次ByteBuffer接收读取到的是:ou\nDo you love me?\n。

服务端本来应该接收到的是完整无缺的三句话:

1.Hi leomessi\n

2.I love you\n

3.Do you love me?\n

结果现在造成割裂,其中第一次接收到的"I love y"就是粘包,第二次接收到的"ou\n"就是半包

补充:

又有新的疑惑了,既然由于ByteBuffer过小造成粘包,半包,那为什么不增大ByteBuffer的大小呢??当然是可以增大的,但是极限法思考一下,可以无限大吗?当然不行!ByteBuffer占用的是内存,无论是JVM进程的内存还是操作系统的内存都是有限的,所以一味的增大ByteBuffer的大小而保证一次可以接收读取客户端所有的数据,这一操作改善是不靠谱的!

所以后续我们需要引出其他解决方法,代码如下:

  • 解决半包粘包
public class TestNIO10 {

    public static void main(String[] args) {
        //难点2:合理开辟ByteBuffer的大小,如果太小,可能i love y+追加的空间 可能造成Buffer缓冲区的溢出(后续Netty动态扩容会解决,这里手动设置)
        ByteBuffer buffer = ByteBuffer.allocate(50) ;
        //难点3:假设说put一行的数据中没有\n,那么就会不断的追加,不断的追加就很难控制ByteBuffer缓冲区的大小。
        // 那么该如何设置ByteBuffer的大小,同理难点2,后续Netty会自动调整容量大小
        buffer.put("Hello leomessi\ni love y".getBytes());
        doLineSplit(buffer);
        //i love you\nDo you like me?\n
        buffer.put("ou\nDo you like me\n?".getBytes());
        doLineSplit(buffer);
        buffer.put("ceshi\n".getBytes());
        doLineSplit(buffer);
    }

    //ByteBuffer接收的数据 \n
    private static void doLineSplit(ByteBuffer buffer) {
        //写模式转换成读模式
        buffer.flip();
        for (int i = 0; i < buffer.limit(); i++) {
            if(buffer.get(i) == '\n') {
                //难点1:为什么要减去buffer.position()? 为了在当前buffer数据中存在多个\n,此时为了节省开辟的target空间大小
                int length = i+1-buffer.position();
                ByteBuffer target = ByteBuffer.allocate(length) ;
                for (int j = 0; j < length; j++) {
                    target.put(buffer.get());
                }
                //写入工作完成,从写模式转换成读模式
                target.flip();
                System.out.println("StandardCharsets.UTF_8.decode(target).toString() = " + StandardCharsets.UTF_8.decode(target).toString());
            }
        }
        buffer.compact();
    }

}
  • 上述代码未解决的问题

1.

代码依旧存在问题,当读取行数据没有\n,我们需要读取完第一行后接着继续读取第二行的数据,直到第3,4,5........第n行的数据。这个过程需要ByteBuffer缓冲区的大小不断扩容,使用NIO就很麻烦,后续Netty帮我们都做好了,并且Netty可以动态调整ByteBuffer缓冲区的大小

2.

ByteBuffer buffer = ByteBuffer.allocate(50),50这个值不可以设置的太小。

假如设为20,设为20后。

第一次读取Hi sunshuai\n,第一次读取后,未读取的l love y被移到前面,后面接着追加写入ou\nDo you like me\n? 但是ByteBuffer的空间(20)不够用了,所以会Buffer缓冲区溢出!

你可能感兴趣的:(Netty应用,java,Netty,netty,后端)