深入理解Netty以及为什么项目中要使用?(七)Netty中ByteBuf详解

news2024/11/20 15:30:16

在Netty中,还有另外一个比较常见的对象ByteBuf,它其实等同于Java Nio中的ByteBuffer,但是ByteBuf对Nio中的ByteBuffer的功能做了很作增强,下面我们来简单了解一下ByteBuf。

下面这段代码演示了ByteBuf的创建以及内容的打印,这里显示出了和普通ByteBuffer最大的区别之一,就是ByteBuf可以自动扩容,默认长度是256,如果内容长度超过阈值时,会自动触发扩容

public class ByteBufExample {

    public static void main(String[] args) {
        ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();//可自动扩容
        log(buf);
        StringBuilder sb=new StringBuilder();
        for (int i = 0; i < 32; i++) {  //演示的时候,可以把循环的值扩大,就能看到扩容效果
            sb.append(" - "+i);
        }
        buf.writeBytes(sb.toString().getBytes());
        log(buf);
    }

    private static void log(ByteBuf buf){
        StringBuilder builder=new StringBuilder()
            .append(" read index:").append(buf.readerIndex()) //获取读索引
            .append(" write index:").append(buf.writerIndex()) //获取写索引
            .append(" capacity:").append(buf.capacity()) //获取容量
            .append(StringUtil.NEWLINE);
        //把ByteBuf中的内容,dump到StringBuilder中
        ByteBufUtil.appendPrettyHexDump(builder,buf);
        System.out.println(builder.toString());
    }
}

ByteBuf创建的方法有两种

  • 第一种,创建基于堆内存的ByteBuf

    ByteBuf buffer=ByteBufAllocator.DEFAULT.heapBuffer(10);
    
  • 第二种,创建基于直接内存(堆外内存)的ByteBuf(默认情况下用的是这种

    Java中的内存分为两个部分,一部分是不需要jvm管理的直接内存,也被称为堆外内存。堆外内存就是把内存对象分配在JVM堆以外的内存区域,这部分内存不是虚拟机管理,而是由操作系统来管理,这样可以减少垃圾回收对应用程序的影响

    ByteBufAllocator.DEFAULT.directBuffer(10);
    

    直接内存的好处是读写性能会高一些,如果数据存放在堆中,此时需要把Java堆空间的数据发送到远程服务器,首先需要把堆内部的数据拷贝到直接内存(堆外内存),然后再发送。如果是把数据直接存储到堆外内存中,发送的时候就少了一个复制步骤。

    但是它也有缺点,由于缺少了JVM的内存管理,所以需要我们自己来维护堆外内存,防止内存溢出。

另外,需要注意的是,ByteBuf默认采用了池化技术来创建。关于池化技术在前面的课程中已经重复讲过,它的核心思想是实现对象的复用,从而减少对象频繁创建销毁带来的性能开销。

池化功能是否开启,可以通过下面的环境变量来控制,其中unpooled表示不开启。

-Dio.netty.allocator.type={unpooled|pooled}
public class NettyByteBufExample {
    public static void main(String[] args) {
        ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();
        System.out.println(buf);
    }
}

ByteBuf的存储结构

ByteBuf的存储结构如图所示,从这个图中可以看到ByteBuf其实是一个字节容器,该容器中包含三个部分

  • 已经丢弃的字节,这部分数据是无效的

  • 可读字节,这部分数据是ByteBuf的主体数据,从ByteBuf里面读取的数据都来自这部分;可写字节,所有写到ByteBuf的数据都会存储到这一段

  • 可扩容字节,表示ByteBuf最多还能扩容多少容量。

ByteBuf中常用的方法

对于ByteBuf来说,常见的方法就是写入和读取

Write相关方法

对于write方法来说,ByteBuf提供了针对各种不同数据类型的写入,比如

  • writeChar,写入char类型

  • writeInt,写入int类型

  • writeFloat,写入float类型

  • writeBytes, 写入nio的ByteBuffer

  • writeCharSequence, 写入字符串

public class ByteBufExample {

    public static void main(String[] args) {
        ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();//可自动扩容
        buf.writeBytes(new byte[]{1,2,3,4}); //写入四个字节
        log(buf);  
        buf.writeInt(5);  //写入一个int类型,也是4个字节
        log(buf);
    }
    private static void log(ByteBuf buf){
        System.out.println(buf);
        StringBuilder builder=new StringBuilder()
                .append(" read index:").append(buf.readerIndex())
                .append(" write index:").append(buf.writerIndex())
                .append(" capacity:").append(buf.capacity())
                .append(StringUtil.NEWLINE);
        //把ByteBuf中的内容,dump到StringBuilder中
        ByteBufUtil.appendPrettyHexDump(builder,buf);
        System.out.println(builder.toString());
    }
}

扩容

当向ByteBuf写入数据时,发现容量不足时,会触发扩容,而具体的扩容规则是

假设ByteBuf初始容量是10。

  • 如果写入后数据大小未超过512个字节,则选择下一个16的整数倍进行库容。比如写入数据后大小为12,则扩容后的capacity是16。

  • 如果写入后数据大小超过512个字节,则选择下一个2^n^。比如写入后大小是512字节,则扩容后的capacity是2^10^=1024 。(因为2^9^=512,长度已经不够了)

  • 扩容不能超过max capacity,否则会报错。

Reader相关方法

reader方法也同样针对不同数据类型提供了不同的操作方法,

  • readByte ,读取单个字节

  • readInt , 读取一个int类型

  • readFloat ,读取一个float类型

public class ByteBufExample {

    public static void main(String[] args) {
        ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();//可自动扩容
        buf.writeBytes(new byte[]{1,2,3,4});
        log(buf);
        System.out.println(buf.readByte());
        log(buf);
    }
    private static void log(ByteBuf buf){
        StringBuilder builder=new StringBuilder()
            .append(" read index:").append(buf.readerIndex())
            .append(" write index:").append(buf.writerIndex())
            .append(" capacity:").append(buf.capacity())
            .append(StringUtil.NEWLINE);
        //把ByteBuf中的内容,dump到StringBuilder中
        ByteBufUtil.appendPrettyHexDump(builder,buf);
        System.out.println(builder.toString());
    }
}

从下面结果中可以看到,读完一个字节后,这个字节就变成了废弃部分,再次读取的时候只能读取 未读取的部分数据。

read index:0 write index:7 capacity:256
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07                            |.......         |
+--------+-------------------------------------------------+----------------+
1
 read index:1 write index:7 capacity:256
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 02 03 04 05 06 07                               |......          |
+--------+-------------------------------------------------+----------------+

Process finished with exit code 0

另外,如果想重复读取哪些已经读完的数据,这里提供了两个方法来实现标记和重置

public static void main(String[] args) {
    ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();//可自动扩容
    buf.writeBytes(new byte[]{1,2,3,4,5,6,7});
    log(buf);
    buf.markReaderIndex(); //标记读取的索引位置
    System.out.println(buf.readInt());
    log(buf);
    buf.resetReaderIndex();//重置到标记位
    System.out.println(buf.readInt());
    log(buf);
}

另外,如果想不改变读指针位置来获得数据,在ByteBuf中提供了get开头的方法,这个方法基于索引位置读取,并且允许重复读取的功能。

ByteBuf的零拷贝机制

需要说明一下,ByteBuf的零拷贝机制和我们之前提到的操作系统层面的零拷贝不同,操作系统层面的零拷贝,是我们要把一个文件发送到远程服务器时,需要从内核空间拷贝到用户空间,再从用户空间拷贝到内核空间的网卡缓冲区发送,导致拷贝次数增加。

 而ByteBuf中的零拷贝思想也是相同,都是减少数据复制提升性能。假设有一个原始ByteBuf,我们想对这个ByteBuf其中的两个部分的数据进行操作。按照正常的思路,我们会创建两个新的ByteBuf,然后把原始ByteBuf中的部分数据拷贝到两个新的ByteBuf中,但是这种会涉及到数据拷贝,在并发量较大的情况下,会影响到性能。

ByteBuf中提供了一个slice方法,这个方法可以在不做数据拷贝的情况下对原始ByteBuf进行拆分,使用方法如下

public static void main(String[] args) {
    ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();//可自动扩容
    buf.writeBytes(new byte[]{1,2,3,4,5,6,7,8,9,10});
    log(buf);
    ByteBuf bb1=buf.slice(0,5);
    ByteBuf bb2=buf.slice(5,5);
    log(bb1);
    log(bb2);
    System.out.println("修改原始数据");
    buf.setByte(2, 5); //修改原始buf数据
    log(bb1);//再打印bb1的结果,发现数据发生了变化
}

在上面的代码中,通过slice对原始buf进行切片,每个分片是5个字节。

为了证明slice是没有数据拷贝,我们通过修改原始buf的索引2所在的值,然后再打印第一个分片bb1,可以发现bb1的结果发生了变化。说明两个分片和原始buf指向的数据是同一个。

Unpooled

在前面的案例中我们经常用到Unpooled工具类,它是通过非池化的ByteBuf的创建、组合、复制等操作。

假设有一个协议数据,它有头部和消息体组成,这两个部分分别放在两个ByteBuf中

ByteBuf header=...
ByteBuf body= ...

我们希望把header和body合并成一个ByteBuf,通常的做法是

ByteBuf allBuf=Unpooled.buffer(header.readableBytes()+body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);

在这个过程中,我们把header和body拷贝到了新的allBuf中,这个过程在无形中增加了两次数据拷贝操作。那有没有更高效的方法减少拷贝次数来达到相同目的呢?

在Netty中,提供了一个CompositeByteBuf组件,它提供了这个功能。

public class ByteBufExample {

    public static void main(String[] args) {
        ByteBuf header= ByteBufAllocator.DEFAULT.buffer();//可自动扩容
        header.writeCharSequence("header", CharsetUtil.UTF_8);
        ByteBuf body=ByteBufAllocator.DEFAULT.buffer();
        body.writeCharSequence("body", CharsetUtil.UTF_8);
        CompositeByteBuf compositeByteBuf=Unpooled.compositeBuffer();
        //其中第一个参数是 true, 表示当添加新的 ByteBuf 时, 自动递增 CompositeByteBuf 的 writeIndex.
        //默认是false,也就是writeIndex=0,这样的话我们不可能从compositeByteBuf中读取到数据。
        compositeByteBuf.addComponents(true,header,body);
        log(compositeByteBuf);
    }
    private static void log(ByteBuf buf){
        StringBuilder builder=new StringBuilder()
            .append(" read index:").append(buf.readerIndex())
            .append(" write index:").append(buf.writerIndex())
            .append(" capacity:").append(buf.capacity())
            .append(StringUtil.NEWLINE);
        //把ByteBuf中的内容,dump到StringBuilder中
        ByteBufUtil.appendPrettyHexDump(builder,buf);
        System.out.println(builder.toString());
    }
}

之所以CompositeByteBuf能够实现零拷贝,是因为在组合header和body时,并没有对这两个数据进行复制,而是通过CompositeByteBuf构建了一个逻辑整体,里面仍然是两个真实对象,也就是有一个指针指向了同一个对象,所以这里类似于浅拷贝的实现。

wrappedBuffer

在Unpooled工具类中,提供了一个wrappedBuffer方法,来实现CompositeByteBuf零拷贝功能。使用方法如下。

public static void main(String[] args) {
    ByteBuf header= ByteBufAllocator.DEFAULT.buffer();//可自动扩容
    header.writeCharSequence("header", CharsetUtil.UTF_8);
    ByteBuf body=ByteBufAllocator.DEFAULT.buffer();
    body.writeCharSequence("body", CharsetUtil.UTF_8);
    ByteBuf allBb=Unpooled.wrappedBuffer(header,body);
    log(allBb);
    //对于零拷贝机制,修改原始ByteBuf中的值,会影响到allBb
    header.setCharSequence(0,"Newer0",CharsetUtil.UTF_8);
    log(allBb); 
}

copiedBuffer

copiedBuffer,和wrappedBuffer最大的区别是,该方法会实现数据复制,下面代码演示了copiedBuffer和wrappedbuffer的区别,可以看到在case标注的位置中,修改了原始ByteBuf的值,并没有影响到allBb。

public static void main(String[] args) {
    ByteBuf header= ByteBufAllocator.DEFAULT.buffer();//可自动扩容
    header.writeCharSequence("header", CharsetUtil.UTF_8);
    ByteBuf body=ByteBufAllocator.DEFAULT.buffer();
    body.writeCharSequence("body", CharsetUtil.UTF_8);
    ByteBuf allBb=Unpooled.copiedBuffer(header,body);
    log(allBb);
    header.setCharSequence(0,"Newer0",CharsetUtil.UTF_8); //case
    log(allBb);
}

内存释放

针对不同的ByteBuf创建,内存释放的方法不同。

  • UnpooledHeapByteBuf,使用JVM内存,只需要等待GC回收即可

  • UnpooledDirectByteBuf,使用对外内存,需要特殊方法来回收内存

  • PooledByteBuf和它的之类使用了池化机制,需要更复杂的规则来回收内存

如果ByteBuf是使用堆外内存来创建,那么尽量手动释放内存,那怎么释放呢?

Netty采用了引用计数方法来控制内存回收,每个ByteBuf都实现了ReferenceCounted接口。

  • 每个ByteBuf对象的初始计数为1

  • 调用release方法时,计数器减一,如果计数器为0,ByteBuf被回收

  • 调用retain方法时,计数器加一,表示调用者没用完之前,其他handler即时调用了release也不会造成回收。

  • 当计数器为0时,底层内存会被回收,这时即使ByteBuf对象还存在,但是它的各个方法都无法正常使用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1539681.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

#Idea打包诺依 多模块项目遇到的问题

##诺依框架中遇到的问题 1. 打包部署出错 Please refer to /Users/zhang/code/giteeProjects/wms-ruoyi/ruoyi-generator/target/surefire-reports for the individual test results. Please refer to dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [dat…

【技术栈】Redis 删除策略

SueWakeup 个人主页&#xff1a;SueWakeup 系列专栏&#xff1a;学习技术栈 个性签名&#xff1a;保留赤子之心也许是种幸运吧 本文封面由 凯楠&#x1f4f8; 友情提供 目录 相关传送门 前言 1. 删除策略的目标 2. 数据删除策略 2.1 定时删除 2.2 惰性删除 2.3 定期删除…

智能财务新选择!Zoho Books入选福布斯榜单,助力中小企业!

放眼全球&#xff0c;中小企业始终是经济发展的重要组成部分。然而&#xff0c;由于中小企业的规模、流程规范和资源等方面受限较多&#xff0c;从而导致其在管理及运营上存在着诸多问题。其中包括财务管理不规范、成本控制不到位、运营效率低下等&#xff0c;这些问题则直接影…

freeRTOS动态内存heap4源码分析

1 前言 随着功能安全的推广&#xff0c;动态内存分配在RTOS领域的用武之地将越来越小。但heap4毕竟是为RTOS量身打造&#xff0c;相对简单&#xff0c;作为堆内存管理的入门学习&#xff0c;仍是很不错的选择。 1.1 标准c库动态内存函数的弊端 对于标准C库的malloc和free函数&…

2024年【安全员-A证】免费试题及安全员-A证作业考试题库

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年【安全员-A证】免费试题及安全员-A证作业考试题库&#xff0c;包含安全员-A证免费试题答案和解析及安全员-A证作业考试题库练习。安全生产模拟考试一点通结合国家安全员-A证考试最新大纲及安全员-A证考试真题汇…

动态规划——斐波那契问题(Java)

目录 什么是动态规划&#xff1f; 练习 练习1&#xff1a;斐波那契数 练习2&#xff1a;三步问题 练习3&#xff1a;使用最小花费爬楼梯 练习4&#xff1a;解码方法 什么是动态规划&#xff1f; 动态规划&#xff08;Dynamic Programming&#xff0c;DP&#xff09;&…

8个国产全能型AI写作神器,给个标题就能自动生成全文 #其他#知识分享

国外ChatGPT爆火&#xff0c;AI写作在国内也引起不小的瞩目&#xff0c;目前国内的AI写作工具少说也有几十上百个&#xff0c;要在这么多AI写作中找出适合自己的工具&#xff0c;一个一个尝试是不太现实的&#xff0c;所以今天就给大家推荐一些款AI写作工具。帮助你少走弯路&am…

递归和递推的区别

目录 1、递推 2、递归 3、结言 递归 递推 1、递推 递推就是说从初值出发后一直运算到所需的结果。 ——从已知到未知。&#xff08;从小到大&#xff09; 举一个简单的例子&#xff1a; 每天能学习一个小时的编程&#xff0c;那么一个月之后可以学到三十小时的编程知识。…

SSL加密:保护数据传输的安全盾牌

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

尝试Docker Dev Environments

无法从本地目录创建容器环境 创建的容器环境无法在VS Code打开 从官方仓库打开 结果vscode报错。fine&#xff0c;告辞。老老实实用本地环境开发。

2024公认口碑最好的洗地机有哪些?若看重清洁力,这四款最值得买

每当我们要清洁卫生时&#xff0c;是否总是感到腰酸背痛、疲劳不堪&#xff0c;甚至头昏眼花&#xff1f;地板是家中的重要门面&#xff0c;不容忽视的卫生焦点。如今&#xff0c;我们终于多了一位家务打扫的救星——家用洗地地机。一次操作&#xff0c;即可完成扫地除尘、地除…

鸿蒙ArkUI【开发移植Carbon】

项目介绍 本项目是基于开源项目[Carbon] 进行harmonyos化的移植和开发的。 移植版本&#xff1a;Branches/master 这不是单纯只是API和基本功能展示demo&#xff0c;它是最有用的自定义控件的实现&#xff0c;如设计规范中所示。 Carbon试图&#xff1a; 让事情变得更简单&…

飞桨ONNX推理部署初探

ONNX&#xff0c;全称Open Neural Network Exchange&#xff08;开放神经网络交换&#xff09;&#xff0c;是一个用于表示深度学习模型的标准&#xff0c;它定义了一组与环境、平台均无关的标准格式。这使得不同的人工智能框架&#xff0c;如飞桨、MXNet等&#xff0c;可以采用…

【API调用gpt-4 (vision-preview)】基于微软的Azure OpenAI API

微软的Azure页面 &#xff1a; https://learn.microsoft.com/zh-cn/azure/ai-services/openai/concepts/models 调用代码&#xff1a;https://learn.microsoft.com/zh-cn/azure/ai-services/openai/how-to/switching-endpoints openai说明: https://platform.openai.com/docs/g…

Kubernetes kafka系列 | Strimzi 快速部署kafka集群 (可外部通信)

一、Strimzi介绍 Strimzi 是一个用于 Apache Kafka 在 Kubernetes 上部署和管理的开源项目。它提供了一组 Kubernetes 自定义资源定义(Custom Resource Definitions,CRDs)、控制器和操作符,使得在 Kubernetes 环境中轻松地部署、管理和操作 Kafka 集群成为可能。Strimzi 项…

[AIGC] 主流工作流引擎对比与适用场景介绍

主流工作流引擎对比与适用场景介绍 工作流引擎在业务流程管理中扮演着重要的角色&#xff0c;它可以帮助组织将复杂的工作流程自动化&#xff0c;降低错误率&#xff0c;提高工作效率。目前市面上有许多优秀的工作流引擎&#xff0c;各自都有着独特的优点和适用的场景。本文将介…

C++:类的6大默认成员函数(拷贝构造函数篇)

文章目录 1、拷贝构造函数的概念const用途 2、拷贝构造函数的特性浅拷贝/值拷贝 前言:Hello,大家好&#xff0c;咱这篇博客继续默认成员函数&#xff0c;今天的笔记分享为拷贝构造函数~ 1、拷贝构造函数的概念 在创建对象时&#xff0c;我们能否创建一个与已存在对象一某一样的…

【JS】深度学习JavaScript

&#x1f493; 博客主页&#xff1a;从零开始的-CodeNinja之路 ⏩ 收录文章&#xff1a;【JS】深度学习JavaScript &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 一:JavaScript1.1 JavaScript是什么1.2 JS的引入方式1.3 JS变量1.4 数据类型1.5 …

(005)ssh Load key “./id_rsa“: invalid format (一直好好的,突然变心了)

文章目录 问题解决 问题 一直用的好好的&#xff0c;重装系统之后&#xff0c;使用 ssh命令&#xff0c;报了 &#xff1a; 解决 1.查看证书的换行符号&#xff1a; 2.证书的换行符需要是 “LF”&#xff1a;

鸿蒙ArkTS实战开发-Native XComponent组件的使用

介绍 本篇Codelab主要介绍如何使用XComponent组件调用NAPI来创建EGL/GLES环境&#xff0c;实现在主页面绘制一个正方形&#xff0c;并可以改变正方形的颜色。本篇CodeLab使用Native C模板创建。 如图所示&#xff0c;点击绘制矩形按钮&#xff0c;XComponent组件绘制区域中渲…