Netty原理:ByteBuf对Nio bytebuffer做了什么导致效率提升?(1)

简介: Netty原理:ByteBuf对Nio bytebuffer做了什么导致效率提升?(1)

ByteBuf

NIO中ByteBuffer的缺点:


A 长度固定,无法动态的扩容和缩容,缺乏灵活性

B 使用一个position记录读写的索引位置,在读写模式切换时需手动调用flip方法,增加了使用的复杂度。

C 功能有限,使用过程中往往需要自行封装


1)分类

按照内存的位置,分为堆内存缓冲区 heap buffer、直接内存缓冲区direct buffer、复合内存缓冲区composite buffer。


A heap buffer

将数据存储到JVM的堆空间中,实际使用字节数组byte[]来存放。

优点:数据可以快速的创建和释放,并且能够直接访问内部数组

缺点:在读写数据时,需要将数据复制到直接缓冲区 再进行网络传输。


B direct buffer

不在堆中,而是使用了操作系统的本地内存。

优点:在使用Socket进行数据传输过程中,减少一次拷贝,性能更高。

缺点:释放和分配的空间更昂贵,使用时需要更谨慎。


C composite buffer

将两个或多个不同内存的缓冲区合并

优点:可以统一进行操作


应用场景:在通信线程使用缓冲区时,往往使用direct buffer,而业务消息使用缓冲区时,往往使用heap buffer,在解决http包,请求头+请求体特性不同而选择不同位置存储时,可以将两者拼接使用


通过迭代器遍历就可以看到类型


  public static void main(String[] args) { 
//    堆内存 其他的方式
        ByteBuf byteBuf = Unpooled.buffer();
        //直接内存缓冲区
        ByteBuf dBuf = Unpooled.directBuffer();
        //这个类型是复合缓冲区 堆内存 和 直接内存 两个缓冲区可以作为参数复合起来
        CompositeByteBuf csbuf = Unpooled.compositeBuffer();
        csbuf.addComponents(byteBuf, dBuf);
        //    这个缓冲区可以用迭代器遍历
        Iterator<ByteBuf> iterator = csbuf.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
  }

1.png


可以看到 这里遍历出来的就是 我们先后放进去的 堆内存缓冲区和直接内存缓冲区


对于直接内存缓冲区对于空间和释放相对复杂,速度也比堆内存稍微慢一些,如何有跟好的办法去解决这个问题呢?


D 池化的概念

对于内存空间分配和释放的复杂度和效率,netty通过内存池的方式来解决。

内存池,可以循环利用ByteBuf,提高使用率。但是管理和维护较复杂。


Unpooled正是非池化缓冲区的工具类。


主要区别在于,池化的内存由netty管理,非池化的内存由GC回收。


E 回收方式

回收方式为引用计数,具体规则为,通过记录被引用的次数,判断当前对象是否还会被使用。

当对象被调用时,引用计为+1,当对象被释放时,引用计为-1,当引用次数为0时,对象可以回收。


弊端:可能引发内存泄漏。

当对象不可达,JVM会通过GC回收掉,但此时引用计数可能不为0,对象无法归还内存池,会导致内存泄漏。netty只能通过对内存缓冲区进行采样,来检查。


GC回收方式的代码效果


public static void main(String[] args) {
        ByteBuf buf = Unpooled.buffer(10);
        System.out.println(buf);
        //引用计数的值
        System.out.println(buf.refCnt());
        //保持的意思 将计数 +1
        buf.retain();
        System.out.println(buf.refCnt());
        //释放的意思 计数-1
        buf.release();
        System.out.println(buf.refCnt());
    }

2.png

2)工作原理

和ByteBuffer不同在于,增加了一个指针,通过两个指针记录读模式和写模式时的索引位置,读指针叫做readerIndex,写指针叫做writerIndex。


A 读写分离

1.png

2.png


当执行clear()方法时,索引位置清空回初始位置,但数据保持不变。


mark和reset方法在ByteBuf中同样适用,如markReaderIndex和resetReaderIndex。


用代码来验证效果


先查看一下初始值 以下代码都在main方法中。

    public static void main(String[] args) {
        ByteBuf buf = Unpooled.buffer();
        //默认的初始化是 256
        System.out.println(buf.capacity());
        //查看读写索引
        System.out.println(buf.readerIndex());
        System.out.println(buf.writerIndex());
        //可写大小
        System.out.println(buf.writableBytes());
    }

1.png


不同的参数都有不同的方法来展示


接下来写入数据

    buf.writeBytes("hello index".getBytes());
        //默认的初始化是 256
        System.out.println(buf.capacity());
        //查看读写索引
        System.out.println(buf.readerIndex());
        System.out.println(buf.writerIndex());
        //可写大小
        System.out.println(buf.writableBytes());

 1.png



读取数据

1.png

 

        System.out.println("--------只读取5个字节 hello");
        for (int i = 0; i < 5; i++) {
            System.out.print((char) buf.readByte());
        }
        System.out.println();
        //默认的初始化是 256
        System.out.println(buf.capacity());
        //查看读写索引
        System.out.println(buf.readerIndex());
        System.out.println(buf.writerIndex());
        //可写大小
        System.out.println("可写入区域:" + buf.writableBytes());
        System.out.println("可读写的字节:" + buf.readableBytes());

可回收区域代码验证


读取完之后我们读取过的那五个字节就变成了可回收区域,我们可以调用方法来回收


回收之后方法 可写位置会变成 当前大小+回收大小


 

      //回收可废弃空间
        buf.discardReadBytes();
        System.out.println("-------回收废弃空间");
        //默认的初始化是 256
        System.out.println(buf.capacity());
        //查看读写索引
        System.out.println(buf.readerIndex());
        System.out.println(buf.writerIndex());
        //可写大小
        System.out.println("可写入区域:" + buf.writableBytes());
        System.out.println("可读写的字节:" + buf.readableBytes());

可以看到 我们读索引归零 可写入区域变成了 原有加上回收的可废弃区域

1.png



标记回滚 同之前的charbuffer是一样的 只不过这里是两个索引


 

   System.out.println("--------读取index 并且回退");
        //这里读写索引都适用
        buf.markReaderIndex();
        //buf.markWriterIndex();
        int end = buf.writerIndex();
        for (int i = buf.readerIndex(); i < end; i++) {
            System.out.print((char) buf.readByte());
        }
        System.out.println();
        //撤回到mark地方
        buf.resetReaderIndex();
        //默认的初始化是 256
        System.out.println(buf.capacity());
        //查看读写索引
        System.out.println(buf.readerIndex());
        System.out.println(buf.writerIndex());
        //可写大小
        System.out.println("可写入区域:" + buf.writableBytes());
        System.out.println("可读写的字节:" + buf.readableBytes());

1.png


B 深浅拷贝

浅拷贝,拷贝的是对对象的引用,并没有创建新对象,新对象和原对象之间互相影响。


浅拷贝 代码验证实现


浅拷贝方法 证明是引用

切片拷贝 只可读不可写 区间是readerindex - writerindex

 

    public static void main(String[] args) {
        ByteBuf buf = Unpooled.buffer();
        buf.writeBytes("hello bytebuf copy".getBytes());
        System.out.println("capacity:" + buf.capacity());
        //查看读写索引
        System.out.println("readerIndex:" + buf.readerIndex());
        System.out.println("writerIndex:" + buf.writerIndex());
        //可写大小
        System.out.println("可写入区域:" + buf.writableBytes());
        System.out.println("可读写的字节:" + buf.readableBytes());
        //复制  浅拷贝
        ByteBuf newbuf = buf.duplicate();
        System.out.println("-------------duplicate newbuf");
        System.out.println("capacity:" + newbuf.capacity());
        //查看读写索引
        System.out.println("readerIndex:" + newbuf.readerIndex());
        System.out.println("writerIndex:" + newbuf.writerIndex());
        //可写大小
        System.out.println("可写入区域:" + newbuf.writableBytes());
        System.out.println("可读写的字节:" + newbuf.readableBytes());
        //写入新的数据 在新的buf中
        System.out.println("-------------duplicate newbuf add data");
        newbuf.writeBytes("from newbuf".getBytes());
        //之后读取两个 buf 查看不同
        //读取大小超过写 索引会报错 我们需要设置一下
        buf.writerIndex(30);
        for (int i = 0; i < 13; i++) {
            System.out.print((char) buf.readByte());
        }
        System.out.println();
        System.out.println("capacity:" + buf.capacity());
        //查看读写索引
        System.out.println("readerIndex:" + buf.readerIndex());
        System.out.println("writerIndex:" + buf.writerIndex());
        //可写大小
        System.out.println("可写入区域:" + buf.writableBytes());
        System.out.println("可读写的字节:" + buf.readableBytes());
        //有时候 我们的只需要拿到最重要的部分 未读取部分 netty同时也给我们准备了方法
        //slice() 部分浅拷贝 拷贝区间 readerindex - writerindex之间的区域
        //这部分 只可读 不可写 切片的容量 就是原可读区域的大小
        ByteBuf sliceBuf = buf.slice();
        //写入数据 会导致 异常
        System.out.println("---------sliceBuf");
        System.out.println("capacity:" + sliceBuf.capacity());
        //查看读写索引
        System.out.println("readerIndex:" + sliceBuf.readerIndex());
        System.out.println("writerIndex:" + sliceBuf.writerIndex());
        //可写大小
        System.out.println("可写入区域:" + sliceBuf.writableBytes());
        System.out.println("可读写的字节:" + sliceBuf.readableBytes());
    }

效果输出 分析

1.png



深拷贝,拷贝的是整个对象,和原对象之间完全独立。


// 深复制
ByteBuf copyBuf = buf.copy();
System.out.println("------copyBuf");
System.out.println("capacity:" + copyBuf.capacity());
//查看读写索引
System.out.println("readerIndex:" + copyBuf.readerIndex());
System.out.println("writerIndex:" + copyBuf.writerIndex());
//可写大小
System.out.println("可写入区域:" + copyBuf.writableBytes());
System.out.println("可读写的字节:" + copyBuf.readableBytes());
System.out.println("------- add data copybuf");
copyBuf.writeBytes("from copyBuf".getBytes());
copyBuf.writerIndex(43);
for (int i = copyBuf.readerIndex(); i < 43; i++) {
    System.out.print((char) copyBuf.readByte());
}
System.out.println();
System.out.println("----------原 buf");
buf.writerIndex(43);
for (int i = buf.readerIndex(); i < 43; i++) {
    System.out.print((char) buf.readByte());
}
System.out.println();

分析结果

1.png



小结


duplicate和slice方法,达成全部浅拷贝和部分浅拷贝。

copy,部分深拷贝,部分代表的是可读空间。


相关文章
|
2月前
|
Java 调度
Netty运行原理问题之ChannelHandler在Netty中扮演什么角色
Netty运行原理问题之ChannelHandler在Netty中扮演什么角色
|
2月前
|
编解码 网络协议 API
Netty运行原理问题之Netty的主次Reactor多线程模型工作的问题如何解决
Netty运行原理问题之Netty的主次Reactor多线程模型工作的问题如何解决
|
2月前
|
编解码 网络协议 开发者
Netty运行原理问题之NettyTCP的粘包和拆包的问题如何解决
Netty运行原理问题之NettyTCP的粘包和拆包的问题如何解决
|
2月前
|
调度
Netty运行原理问题之事件调度工作的问题如何解决
Netty运行原理问题之事件调度工作的问题如何解决
|
2月前
|
设计模式
Lettuce的特性和内部实现问题之Netty NIO的性能优于BIO的问题如何解决
Lettuce的特性和内部实现问题之Netty NIO的性能优于BIO的问题如何解决
|
7天前
|
Java
Netty BIO/NIO/AIO介绍
Netty BIO/NIO/AIO介绍
|
2月前
|
开发者
Netty运行原理问题之Netty高性能实现的问题如何解决
Netty运行原理问题之Netty高性能实现的问题如何解决
|
2月前
|
API 开发者
Netty运行原理问题之Netty实现低开发门槛的问题如何解决
Netty运行原理问题之Netty实现低开发门槛的问题如何解决
|
2月前
|
网络协议 C# 开发者
WPF与Socket编程的完美邂逅:打造流畅网络通信体验——从客户端到服务器端,手把手教你实现基于Socket的实时数据交换
【8月更文挑战第31天】网络通信在现代应用中至关重要,Socket编程作为其实现基础,即便在主要用于桌面应用的Windows Presentation Foundation(WPF)中也发挥着重要作用。本文通过最佳实践,详细介绍如何在WPF应用中利用Socket实现网络通信,包括创建WPF项目、设计用户界面、实现Socket通信逻辑及搭建简单服务器端的全过程。具体步骤涵盖从UI设计到前后端交互的各个环节,并附有详尽示例代码,助力WPF开发者掌握这一关键技术,拓展应用程序的功能与实用性。
68 0
|
2月前
|
存储 网络协议 Java
【Netty 神奇之旅】Java NIO 基础全解析:从零开始玩转高效网络编程!
【8月更文挑战第24天】本文介绍了Java NIO,一种非阻塞I/O模型,极大提升了Java应用程序在网络通信中的性能。核心组件包括Buffer、Channel、Selector和SocketChannel。通过示例代码展示了如何使用Java NIO进行服务器与客户端通信。此外,还介绍了基于Java NIO的高性能网络框架Netty,以及如何用Netty构建TCP服务器和客户端。熟悉这些技术和概念对于开发高并发网络应用至关重要。
50 0