【Netty篇】ByteBuf 详解 (下)

在这里插入图片描述

🌟我的其他文章也讲解的比较有趣😁,如果喜欢博主的讲解方式,可以多多支持一下,感谢🤗!

🌟了解 Netty 的 线程模型 请看 : 【Netty篇】Netty的线程模型

其他优质专栏: 【🎇SpringBoot】【🎉多线程】【🎨Redis】【✨设计模式专栏已完结)】…等

如果喜欢作者的讲解方式,可以点赞收藏加关注,你的支持就是我的动力
✨更多文章请看个人主页: 码熔burning

各位观众老爷,今天咱们来聊聊 Netty 里的“管道工”—— Channel。幽默风趣的讲解方式,让您听得懂,记得住!🤣

看之前可以先看看Netty的入门:【Netty篇】幽默的讲解带你入门 Netty !建议收藏

精彩继续!各位观众,欢迎回到“Netty奇妙夜”之“ByteBuf探秘”下半场!🕵️‍♂️ 今晚,我们要继续深入挖掘ByteBuf的各种神奇技能!首先,让我们聚焦它的“读心术”——读取数据!📖

一、ByteBuf的“读心术”——读取数据!

写入数据就像我们往ByteBuf这个“百宝箱”里塞东西,而读取数据呢,就是我们从这个“百宝箱”里往外掏东西。ByteBuf提供了一系列的读取方法,让我们能够按照不同的数据类型和方式,将箱子里的宝贝取出来。

与写入方法类似,ByteBuf的读取方法也分为按字节、按基本数据类型以及按块读取等。读取操作的关键在于Reader Index(读指针),每次读取成功后,读指针都会自动向后移动,指向下一个待读取的字节。这就像我们从一个装满零食的罐子里拿东西,拿走一个,手就会伸向下一个。🍬➡️😋

下面我们来演示一下读取过程,看看ByteBuf是如何把我们之前写入的数据“吐”出来的!👅

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import java.nio.charset.StandardCharsets;

public class ByteBufReadExample {
    public static void main(String[] args) {
        // 创建一个堆缓冲区,并写入一些数据,就像往罐子里塞各种零食
        ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(32);
        buffer.writeByte(100); // 'd' - 一块巧克力 🍫
        buffer.writeBoolean(true); // true - 一颗软糖 
        buffer.writeShort(256); // 一个小饼干 🍪
        buffer.writeInt(65535); // 一根棒棒糖 🍭
        buffer.writeCharSequence("Netty", StandardCharsets.UTF_8); // 一包印着 "Netty" 的薯片 🍟
        buffer.writeBytes(new byte[]{'b', 'u', 'f'}); // 三颗花生豆 🥜🥜🥜

        System.out.println("写入数据后的 ByteBuf 状态:");
        printBufferDetails(buffer);

        System.out.print("\n开始读取数据,看看我们掏出了什么:");

        // 读取一个字节,掏出一块巧克力
        byte readByte = buffer.readByte();
        System.out.println("\n读取一个字节: " + readByte + " (char: " + (char) readByte + ")");
        printBufferDetails(buffer); // 读指针向后移动了一位

        // 读取一个布尔值,掏出一颗软糖
        boolean readBoolean = buffer.readBoolean();
        System.out.println("\n读取一个布尔值: " + readBoolean);
        printBufferDetails(buffer); // 读指针又向后移动了一位

        // 读取一个短整型,掏出一个小饼干
        short readShort = buffer.readShort();
        System.out.println("\n读取一个短整型: " + readShort);
        printBufferDetails(buffer); // 读指针向后移动了两

        // 读取一个整型,掏出一根棒棒糖
        int readInt = buffer.readInt();
        System.out.println("\n读取一个整型: " + readInt);
        printBufferDetails(buffer); // 读指针向后移动了四位

        // 读取一个字符串,掏出一包 "Netty" 薯片
        CharSequence readCharSequence = buffer.readCharSequence(5, StandardCharsets.UTF_8);
        System.out.println("\n读取一个字符串 (5 字节): " + readCharSequence);
        printBufferDetails(buffer); // 读指针向后移动了五位

        // 读取剩余的字节数组,掏出三颗花生豆
        byte[] remainingBytes = new byte[buffer.readableBytes()];
        buffer.readBytes(remainingBytes);
        System.out.print("\n读取剩余的字节数组: [");
        for (int i = 0; i < remainingBytes.length; i++) {
            System.out.print((char) remainingBytes[i] + (i == remainingBytes.length - 1 ? "" : ", "));
        }
        System.out.println("]");
        printBufferDetails(buffer); // 读指针移动到了写指针的位置

        // 读取完毕,readerIndex 等于 writerIndex,罐子空了!
        System.out.println("\n读取完毕后的 ByteBuf 状态:");
        printBufferDetails(buffer);

        buffer.release(); // 吃完零食,记得清理罐子(释放 ByteBuf)!🧹
    }

    private static void printBufferDetails(ByteBuf buffer) {
        System.out.print("  Capacity:     " + buffer.capacity());
        System.out.print("  \tReader Index: " + buffer.readerIndex());
        System.out.print("  \tWriter Index: " + buffer.writerIndex());
        System.out.print("  \tReadable Bytes: " + buffer.readableBytes());
        System.out.print("  \tWritable Bytes: " + buffer.writableBytes());
    }
}

运行结果

在这里插入图片描述

你会看到随着我们使用 readByte(), readBoolean(), readShort(), readInt(), readCharSequence(), readBytes() 等方法读取数据,readerIndex 会不断向后移动,而 readableBytes 则相应减少。就像我们从“百宝箱”里一件件地掏出宝贝,箱子里剩下的东西就越来越少啦!📦➡️

接下来,我们将学习ByteBuf的“生命魔法”和各种“分身术”!😉

二、ByteBuf的“生命魔法”—— retain & release!🧙‍♂️✨

在Netty的世界里,ByteBuf是一个需要我们手动管理生命周期的资源,特别是对于使用了内存池的 ByteBuf。retain()release() 就是管理ByteBuf引用计数的两个关键方法,它们就像给ByteBuf施加了“生命魔法”!

  • retain() 这个方法会增加 ByteBuf 的引用计数。想象一下,你手里拿着一个珍贵的魔法卷轴📜(ByteBuf),如果你想把这个卷轴借给多个魔法师🧙‍♂️🧙‍♀️一起研习,但又不想让它消失,你就可以复制几份“引用”(增加引用计数)。每个魔法师都持有一份“引用”,表示他们正在使用这个卷轴。

  • release() 这个方法会减少 ByteBuf 的引用计数。当魔法师研习完毕,不再需要这个卷轴时,他们就会归还他们的“引用”(减少引用计数)。只有当所有魔法师都归还了他们的“引用”,即引用计数降为 0 时,这个卷轴才会被送回魔法图书馆(内存池)或者被销毁(非池化)。如果你忘记 release(),就相当于魔法卷轴被某个魔法师永远占用了,其他魔法师就无法使用,最终导致魔法世界的资源枯竭(内存泄漏!😱)

代码演示:retain & release

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;

public class ByteBufRetainReleaseExample {
    public static void main(String[] args) {
        // 从魔法泉水(ByteBufAllocator)中获取一个魔法卷轴(directBuffer)
        ByteBuf scroll = ByteBufAllocator.DEFAULT.directBuffer();
        System.out.println("初始引用计数: " + scroll.refCnt()); // 初始引用计数为 1,表示我们拥有这个卷轴

        // 第一个魔法师(main 方法)想要继续持有这个卷轴,增加引用计数
        ByteBuf scrollForMagician1 = scroll.retain();
        System.out.println("retain() 后 scroll 的引用计数: " + scroll.refCnt()); // 变为 2
        System.out.println("retain() 后 scrollForMagician1 的引用计数: " + scrollForMagician1.refCnt()); // 也为 2,它们指向同一个卷轴

        // 第二个魔法师也想看看这个卷轴,再次增加引用计数
        ByteBuf scrollForMagician2 = scroll.retain();
        System.out.println("再次 retain() 后 scroll 的引用计数: " + scroll.refCnt()); // 变为 3
        System.out.println("再次 retain() 后 scrollForMagician2 的引用计数: " + scrollForMagician2.refCnt()); // 也为 3

        // 第一个魔法师研习完毕,归还他的“引用”
        scrollForMagician1.release();
        System.out.println("release() 后 scrollForMagician1 的引用计数: " + scrollForMagician1.refCnt()); // 变为 2
        System.out.println("release() 后 scroll 的引用计数: " + scroll.refCnt()); // 仍然是 2

        // 第二个魔法师也研习完毕,归还他的“引用”
        scrollForMagician2.release();
        System.out.println("再次 release() 后 scrollForMagician2 的引用计数: " + scrollForMagician2.refCnt()); // 变为 1
        System.out.println("再次 release() 后 scroll 的引用计数: " + scroll.refCnt()); // 仍然是 1

        // 最后,我们(main 方法)也用完了,归还我们的“引用”
        scroll.release();
        System.out.println("最后 release() 后 scroll 的引用计数: " + scroll.refCnt()); // 变为 0,卷轴被送回魔法图书馆或销毁

        // 尝试操作引用计数为 0 的卷轴通常会引发异常
        // try {
        //     scrollForMagician1.writeByte(0);
        // } catch (io.netty.util.IllegalReferenceCountException e) {
        //     System.err.println("\n尝试操作已释放的卷轴: " + e.getMessage());
        // }
    }
}

运行结果:

在这里插入图片描述

三、ByteBuf的“分身术”—— slice!🔪👯

想象一下,我们有一个大披萨(原始 ByteBuf),上面有各种美味的馅料。slice() 操作就像从这个大披萨上切下几块小披萨。每一小块都共享着大披萨的馅料,但每一块都有自己的“起点”和“可食用”范围。
在这里插入图片描述

代码演示:slice

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import java.nio.charset.StandardCharsets;

public class ByteBufSlicePizzaExample {
    public static void main(String[] args) {
        // 我们的原始大披萨,上面写着 "DeliciousPizza"
        ByteBuf bigPizza = ByteBufAllocator.DEFAULT.heapBuffer(15);
        bigPizza.writeCharSequence("DeliciousPizza", StandardCharsets.UTF_8);
        System.out.println("原始大披萨: " + bigPizza.toString(StandardCharsets.UTF_8));
        printBufferDetails("大披萨", bigPizza);

        // 切下第一块:从索引 0 开始,长度为 5,内容是 "Delic"
        ByteBuf slice1 = bigPizza.slice(0, 5);
        System.out.println("\n切下的第一块 (0, 5): " + slice1.toString(StandardCharsets.UTF_8));
        printBufferDetails("第一块披萨", slice1);

        // 切下第二块:从索引 5 开始,长度为 7,内容是 "iousPiz"
        ByteBuf slice2 = bigPizza.slice(5, 7);
        System.out.println("\n切下的第二块 (5, 7): " + slice2.toString(StandardCharsets.UTF_8));
        printBufferDetails("第二块披萨", slice2);

        // 切下第三块:从索引 12 开始,到末尾,内容是 "za"
        ByteBuf slice3 = bigPizza.slice(12, bigPizza.writerIndex() - 12);
        System.out.println("\n切下的第三块 (12, end): " + slice3.toString(StandardCharsets.UTF_8));
        printBufferDetails("第三块披萨", slice3);

        // 在第一块披萨上“加点辣” (修改数据)
        slice1.setByte(0, 'J'); // 将 'D' 改为 'J'
        System.out.println("\n在第一块披萨上加辣后,第一块披萨的内容: " + slice1.toString(StandardCharsets.UTF_8));
        System.out.println("加辣后,原始大披萨的内容: " + bigPizza.toString(StandardCharsets.UTF_8)); // 大披萨也变“辣”了!
        printBufferDetails("加辣后的第一块披萨", slice1);
        System.out.println();
        printBufferDetails("加辣后的大披萨", bigPizza);
        System.out.println();
        printBufferDetails("第二块披萨 (未受影响)", slice2);
        System.out.println();
        printBufferDetails("第三块披萨 (未受影响)", slice3);

        bigPizza.release();
    }

    private static void printBufferDetails(String name, ByteBuf buffer) {
        System.out.print("  --- " + name + " ---");
        System.out.print("  容量:     " + buffer.capacity());
        System.out.print("  \t读指针: " + buffer.readerIndex());
        System.out.print("  \t写指针: " + buffer.writerIndex());
        System.out.print("  \t可读字节数: " + buffer.readableBytes());
        System.out.print("  \t可写字节数: " + buffer.writableBytes());
        System.out.print("  \t字符串的值:   " + buffer.toString(StandardCharsets.UTF_8));
    }
}

运行结果:

在这里插入图片描述

四、ByteBuf的“克隆术”—— duplicate!👯‍♂️👯‍♀️

想象一下,我们有一份重要的原始文件(原始 ByteBuf),上面记录了一些关键信息,并且我们已经阅读到了一部分,也记录了一部分(移动了 readerIndexwriterIndex)。现在,我们想要创建一个这份文件的副本,让另一个人也从我们阅读和记录的位置开始查看。duplicate() 操作就像复印了这份文件,副本的内容和当前的阅读/记录状态都与原件保持一致,但它们是两个独立的文件对象。
在这里插入图片描述

代码演示:duplicate

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import java.nio.charset.StandardCharsets;

public class ByteBufDuplicateFileExample {
    public static void main(String[] args) {
        // 原始的重要文件,内容是 "ConfidentialData"
        ByteBuf originalFile = ByteBufAllocator.DEFAULT.heapBuffer(16);
        originalFile.writeCharSequence("ConfidentialData", StandardCharsets.UTF_8);
        originalFile.readerIndex(5); // 假设我们已经阅读到 "ential" 的位置
        originalFile.writerIndex(12); // 假设我们已经记录了 "Confidential" 这部分
        System.out.println("原始文件内容: " + originalFile.toString(StandardCharsets.UTF_8));
        printBufferDetails("原始文件", originalFile);

        // 复印一份文件
        ByteBuf duplicateFile = originalFile.duplicate();
        System.out.println("\n复印的文件内容: " + duplicateFile.toString(StandardCharsets.UTF_8));
        printBufferDetails("复印的文件", duplicateFile); // 注意 readerIndex 和 writerIndex 也被复制了

        // 在复印件上“高亮”一部分内容 (修改数据)
        duplicateFile.setByte(duplicateFile.readerIndex(), '*'); // 在复印件当前阅读位置添加高亮
        System.out.println("\n在复印件上高亮后,复印件的内容: " + duplicateFile.toString(StandardCharsets.UTF_8));
        System.out.println("在复印件上高亮后,原始文件的内容: " + originalFile.toString(StandardCharsets.UTF_8)); // 原始文件也被修改了!
        printBufferDetails("高亮后的复印文件", duplicateFile);
        System.out.println();
        printBufferDetails("高亮后的原始文件", originalFile);

        originalFile.release();
    }

    private static void printBufferDetails(String name, ByteBuf buffer) {
        System.out.print("  --- " + name + " ---");
        System.out.print("  容量:     " + buffer.capacity());
        System.out.print("  \t读指针: " + buffer.readerIndex());
        System.out.print("  \t写指针: " + buffer.writerIndex());
        System.out.print("  \t可读字节数: " + buffer.readableBytes());
        System.out.print("  \t可写字节数: " + buffer.writableBytes());
        System.out.print("  \tbuffer的值:   " + buffer.toString(StandardCharsets.UTF_8));
    }
}

运行结果:

在这里插入图片描述

五、ByteBuf的“影分身之术”—— copy!👯‍♂️

想象一下,我们有一张珍贵的照片(原始 ByteBuf),记录着一段美好的回忆。我们想要复制一张完全一样的照片留作纪念,即使在复制的照片上涂鸦或修改,也不会影响到原始的照片。copy() 操作就像拍摄了一张照片的副本,副本拥有与原件完全相同的内容和状态,但它们是两张独立的照片。

代码演示:copy

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class ByteBufCopyPhotoExample {
    public static void main(String[] args) {
        // 原始的珍贵照片,记录着 "HappyMemory"
        ByteBuf originalPhoto = ByteBufAllocator.DEFAULT.heapBuffer(12);
        originalPhoto.writeCharSequence("HappyMemory", StandardCharsets.UTF_8);
        originalPhoto.readerIndex(2); // 假设我们正在查看从 'p' 开始的部分
        originalPhoto.writerIndex(10); // 假设照片的有效信息到 'r' 结束
        System.out.println("原始照片内容: " + originalPhoto.toString(StandardCharsets.UTF_8));
        printBufferDetails("原始照片", originalPhoto);

        // 拍摄一张照片的副本
        ByteBuf copiedPhoto = originalPhoto.copy();
        System.out.println("\n复制的照片内容: " + copiedPhoto.toString(StandardCharsets.UTF_8));
        printBufferDetails("复制的照片", copiedPhoto); // readerIndex 和 writerIndex 也被复制了

        // 在复制的照片上“涂鸦” (修改数据)
        copiedPhoto.setByte(copiedPhoto.readerIndex(), 'X'); // 在复制的照片当前查看位置涂鸦
        System.out.println("\n在复制的照片上涂鸦后,复制的照片内容: " + copiedPhoto.toString(StandardCharsets.UTF_8));
        System.out.println("在复制的照片上涂鸦后,原始照片的内容: " + originalPhoto.toString(StandardCharsets.UTF_8)); // 原始照片完好无损!
        printBufferDetails("涂鸦后的复制照片", copiedPhoto);
        System.out.println();
        printBufferDetails("原始照片", originalPhoto);

        originalPhoto.release();
    }

    private static void printBufferDetails(String name, ByteBuf buffer) {
        System.out.print("  --- " + name + " ---");
        System.out.print("  容量:     " + buffer.capacity());
        System.out.print("  \t读指针: " + buffer.readerIndex());
        System.out.print("  \t写指针: " + buffer.writerIndex());
        System.out.print("  \t可读字节数: " + buffer.readableBytes());
        System.out.print("  \t可写字节数: " + buffer.writableBytes());
        System.out.print("  \tbuffer的值的值:   " + buffer.toString(StandardCharsets.UTF_8));
    }
}

运行结果:

在这里插入图片描述

六、ByteBuf的“合体术”—— CompositeByteBuf!🤝🔗

CompositeByteBuf 就像乐高积木一样,它允许我们将多个不同的 ByteBuf 逻辑上组合成一个单一的 ByteBuf。这样做的好处是可以避免不必要的数据拷贝,特别是在处理由多个独立的数据块组成的消息时(比如消息头和消息体)。CompositeByteBuf 对外表现得像一个连续的缓冲区,但其内部是由多个小的 ByteBuf 组成的。这就像我们把几段绳子连接起来,虽然每段绳子是独立的,但连接后可以当作一根更长的绳子使用。

代码演示:CompositeByteBuf

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.CompositeByteBuf;
import java.nio.charset.StandardCharsets;

public class CompositeByteBufExample {
    public static void main(String[] args) {
        // 创建消息头
        ByteBuf headerBuf = ByteBufAllocator.DEFAULT.heapBuffer(10);
        headerBuf.writeCharSequence("Header", StandardCharsets.UTF_8);

        // 创建消息体
        ByteBuf bodyBuf = ByteBufAllocator.DEFAULT.heapBuffer(10);
        bodyBuf.writeCharSequence("Body", StandardCharsets.UTF_8);

        // 将消息头和消息体“连接”成一个 CompositeByteBuf
        CompositeByteBuf messageBuf = ByteBufAllocator.DEFAULT.compositeBuffer();
        messageBuf.addComponents(true, headerBuf, bodyBuf); // true 表示在 release() messageBuf 时也 release() 这些组件

        System.out.println("组合后的消息内容: " + messageBuf.toString(StandardCharsets.UTF_8));
        System.out.println("组合后的消息可读字节数: " + messageBuf.readableBytes());

        // 像操作一个普通的 ByteBuf 一样读取组合后的消息
        CharSequence header = messageBuf.readCharSequence(6, StandardCharsets.UTF_8);
        CharSequence body = messageBuf.readCharSequence(4, StandardCharsets.UTF_8);
        System.out.println("读取到的 Header: " + header);
        System.out.println("读取到的 Body: " + body);

        messageBuf.release(); // 释放组合缓冲区会自动释放其组件 (因为 addComponents 的第一个参数是 true)
    }
}

运行结果:

在这里插入图片描述

七、ByteBuf的“独门秘籍”—— Unpooled!📜

我们之前已经接触过 Unpooled 类,它提供了一些静态方法来创建非池化的 ByteBuf 实例。这些方法通常用于一些特殊的场景或者简单的测试,因为非池化的 ByteBuf 每次都会分配新的内存,在高并发环境下可能会影响性能。这就像我们每次需要用笔都买一支新笔,而不是用完墨水就换笔芯,有点浪费资源。

代码演示:Unpooled

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class UnpooledExample {
    public static void main(String[] args) {
        // 创建一个非池化的堆缓冲区
        ByteBuf unpooledHeapBuf = Unpooled.buffer(16);
        unpooledHeapBuf.writeCharSequence("Unpooled Heap", StandardCharsets.UTF_8);
        System.out.println("Unpooled Heap Buffer 内容: " + unpooledHeapBuf.toString(StandardCharsets.UTF_8));
        unpooledHeapBuf.release(); // 用完也要记得 release()

        // 创建一个包含已有数据的非池化堆缓冲区 (wrapped buffer)
        byte[] data = "Wrapped Data".getBytes(StandardCharsets.UTF_8);
        ByteBuf wrappedBuffer = Unpooled.wrappedBuffer(data);
        System.out.println("Wrapped Buffer 内容: " + wrappedBuffer.toString(StandardCharsets.UTF_8));
        System.out.println("Wrapped Buffer 是否是堆缓冲区: " + wrappedBuffer.hasArray());
        wrappedBuffer.release();

        // 创建一个直接包含字节数组的非池化堆缓冲区 (copied buffer)
        ByteBuf copiedBuffer = Unpooled.copiedBuffer("Copied Data", StandardCharsets.UTF_8);
        System.out.println("Copied Buffer 内容: " + copiedBuffer.toString(StandardCharsets.UTF_8));
        System.out.println("Copied Buffer 底层数据: " + Arrays.toString(copiedBuffer.array()));
        copiedBuffer.release();

        // 创建一个空的非池化直接缓冲区
        ByteBuf unpooledDirectBuf = Unpooled.directBuffer(8);
        unpooledDirectBuf.writeCharSequence("Direct", StandardCharsets.UTF_8);
        System.out.println("Unpooled Direct Buffer 内容: " + unpooledDirectBuf.toString(StandardCharsets.UTF_8));
        unpooledDirectBuf.release();
    }
}

运行结果::

在这里插入图片描述

好了,各位观众!今晚的“ByteBuf变形记”就到

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值