Netty 网络编程深入学习【一】:ByteBuffer 源码解析

ByteBuffer源码阅读

ByteBuffer是一个用于处理字节数据的缓冲区类。它是Java NIO 包的一部分,提供了一种高效的方式来处理原始字节数据。
ByteBuffer 可以用来读取、写入、修改和操作字节数据,它是一种直接操作字节的方式,比起传统的 InputStreamOutputStream ,它更加灵活和高效。

01. 阅读前准备

Buffer 一般配合另一个类 Channel 配合使用完成读写的操作,这里先来创建读取使用的 Channel 和缓冲使用的 Buffer。

FileChannel channel = new FileInputStream("data.txt").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(10); // 准备缓冲区
channel.read(buffer); // 将文件的内容读取到缓冲区

02. 基本构造解析

ByteBuffer 底层维护了一个存放数据的 Byte 数组,同时使用了多个属性来配合完成写入、读取的操作,底层的大致结构是这样的:

在这里插入图片描述

Position 是一个指向当前读或者写位置的指针(通过 filp 方法来切换读写模式)

Limit 是限制位置,根据读写模式的不同,限制的内容也不同

Capacity 就是底层数组的容量。

03. ByteBuffer 对象的构造

通过上面的案例不难看出,ByteBuffer 的获取方法并不是直接 new 的,而是调用了类中提供的静态方法 allocate(int capacity) 来构造对象,这个方法具体实现是这样的:

    public static ByteBuffer allocate(int capacity) {
    // 检测传入数据是否正确
        if (capacity < 0)
            throw new IllegalArgumentException();
            // 调用 HeapByteBuffer 的构造方法
        return new HeapByteBuffer(capacity, capacity);
    }

ByteBuffer 是一个抽象类,而 HeapByteBufferByteBuffer 的一个具体实现类之一,它是在堆内存中分配的 ByteBuffer。 HeapByteBuffer是通过**ByteBuffer.allocate(int capacity)**方法创建的,它将字节数据存储在Java虚拟机的堆内存中。

    HeapByteBuffer(int cap, int lim) {            // package-private
        super(-1, 0, lim, cap, new byte[cap], 0);
        /*
        hb = new byte[cap];
        offset = 0;
        */
    }
    
    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;
    }
    
    // 最终调用的,Buffer 抽象类中的构造方法
    Buffer(int mark, int pos, int lim, int cap) {       // package-private
    // 检查 cap
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);
        this.capacity = cap; // 设置容量
        limit(lim); // 设置 limit
        position(pos); // 设置 position
        // 检测 mark 的值,如果 mark > position
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("mark > position: ("
                                                   + mark + " > " + pos + ")");
            this.mark = mark;
        }
    }

方法中调用了父类,也就是 ByteBuffer 的构造方法来构造对象,最终调用的是 Buffer 中的构造方法,其中出现了一个之前没有提到的参数**mark ,这个**参数用于设置初始标记,这个标记是一个可选的位置,它允许你在某个位置上设置一个标记,然后稍后可以通过调用 reset() 方法将位置恢复到这个标记处;这样就能理解上面的检测 mark 的值,这个值是不应该放到 position 的后面的,因为它代表的含义是回退。

所以在初始化的时候一般是将其设置为 -1,也就是约定的未设置值的状态,在 Buffer 抽象类中也是这样定义的:

    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;

将文件中的内容读取到 Buffer 需要调用 Channel 中的 read(ByteBuffer dst) 方法,下面来看一下方法具体的实现,因为本文是 Buffer 的源码解析,所以将重点聚焦在 Buffer 类上,关于 Channel 以注释的方式说明:

public int read(ByteBuffer dst) throws IOException {
				// 检测 Channel 是否开启,若未开启会抛出 ClosedChannelException 异常
        ensureOpen(); 
        // 检测文件权限,如果不可读,抛出 NonReadableChannelException 异常
        if (!readable)
            throw new NonReadableChannelException();
        // 锁,类型为 Object
        synchronized (positionLock) {
            int n = 0; // 用于存储实际读取的字节数目
            int ti = -1; // 用于存储当前线程的标识符
            try {
                begin();
                ti = threads.add();
                // 再次检测 Channel 是否开启
                if (!isOpen())
                    return 0;
                    
                // 执行读取的操作    
                do {
                    n = IOUtil.read(fd, dst, -1, nd);
                } while ((n == IOStatus.INTERRUPTED) && isOpen());
                
                // 返回读取的字节数
                return IOStatus.normalize(n);
            } finally {
                threads.remove(ti);
                end(n > 0);
                assert IOStatus.check(n);
            }
        }
    }

04. 将 Channel 中的内容读取到 Buffer

其中最重要的方法就是 IOUtil 类的静态 read 方法,来看一下它的具体实现:

    static int read(FileDescriptor fd, ByteBuffer dst, long position,
                    NativeDispatcher nd)
        throws IOException
    {
    // 检查 ByteBuffer 是否为只读的
        if (dst.isReadOnly())
            throw new IllegalArgumentException("Read-only buffer");
	      // 检查是否为直接缓冲区
        if (dst instanceof DirectBuffer)
            return readIntoNativeBuffer(fd, dst, position, nd);

        // 创建一个临时的直接缓冲区,大小为 dst 的剩余大小
        ByteBuffer bb = Util.getTemporaryDirectBuffer(dst.remaining());
        try {
        // 将数据读取到临时的直接缓冲区
            int n = readIntoNativeBuffer(fd, bb, position, nd);
            bb.flip();
            if (n > 0)
                dst.put(bb); // 将数据放入传入的 Buffer 中
            return n;
        } finally {
        // 释放临时直接缓冲区
            Util.offerFirstTemporaryDirectBuffer(bb);
        }
    }
    
		// 计算剩余大小的方法
    public final int remaining() {
        int rem = limit - position;
        return rem > 0 ? rem : 0;
    }

上面的方法将数据读取到直接缓冲区中,直接缓冲区的优势是在于其数据存储在堆外(off-heap memory)而不是堆内存中。这种特性使得直接缓冲区在进行I/O操作时能够更有效地与操作系统进行交互,从而提高了I/O操作的性能和效率。

上面方法使用了 put 方法将存储在直接缓冲区的内容读取到了传入的 Buffer 中:

    public ByteBuffer put(ByteBuffer src) {
    // 如果是传入的类型堆缓冲区,可以通过数组拷贝的方式复制
        if (src instanceof HeapByteBuffer) {
        // 自己读取自己
            if (src == this)
                throw new IllegalArgumentException();
            HeapByteBuffer sb = (HeapByteBuffer)src;
            int pos = position();
            int sbpos = sb.position();
            
            // 源 Buffer 中内容大于当前 Buffer 的剩余容量
            int n = sb.limit() - sbpos;
            if (n > limit() - pos)
                throw new BufferOverflowException();
                
            // 数组拷贝复制
            System.arraycopy(sb.hb, sb.ix(sbpos),
                             hb, ix(pos), n);
            sb.position(sbpos + n);
            position(pos + n);
            
            // 传入的类型是直接缓冲区
        } else if (src.isDirect()) {
            int n = src.remaining(); // 源 Buffer 的内容容量
            int pos = position(); // 写指针
            // 源 Buffer 中内容大于当前 Buffer 的剩余容量
            if (n > limit() - pos)
                throw new BufferOverflowException();
    // 该方法的作用是从当前 ByteBuffer 中读取数据,
    // 并将其存储到指定的字节数组 dst 中的指定位置 offset 开始的 length 个位置中。
            src.get(hb, ix(pos), n);
            position(pos + n);
        } else {
            super.put(src);
        }
        return this;
    }

这样就完成了将直接缓冲区中的内容复制到新的 Buffer 中。

总结一下,read 方法的原理就是将文件中的内容读取到更有效与系统进行 IO 交互的直接缓冲区中,然后将直接缓冲区中的内容复制到传入的堆缓冲区中。

05. flip 转换方法

为什么在读取和写入数据之前需要调用 flip() 方法来转换模式呢?上面我们提到,Buffer 的读取和存储都依赖指针进行的,flip 方法实质上就是对指针进行操作来实现模式的转换的。

  public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

无论是读模式还是写模式的转换,position 都是位于 0 的位置,也就是读取也要从头开始读,写也要从头开始写(覆盖,后面提到的 compact 方法可以将未读的内容保留下来)

limit 即或者写的限制,如果是从读模式转到写模式,将 limit 设置为 position,也就是最后写入的位置,但是当从读取切换到写入模式的时候,因为 limit 会直接切换成 position 的值,所以有可能会导致写入量大于 limit 而抛出 BufferOverflowException 异常,比如下面的操作:

public class ByteBufferFlipTest {
    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.flip(); // 切换到读模式
        buffer.flip(); // 切换到写模式
        buffer.put((byte) 1);
    }
}

运行后就会这样:

Exception in thread "main" java.nio.BufferOverflowException
	at java.nio.Buffer.nextPutIndex(Buffer.java:525)
	at java.nio.HeapByteBuffer.put(HeapByteBuffer.java:173)
	at org.example.ByteBufferFlipTest.main(ByteBufferFlipTest.java:10)

所以切换的时候一定要注意避免这种情况,因为对 Buffer 的操作比较底层,需要根据合理的设计来实现需求,比如当我们明确的知道读取的是多少字节的时候,可以通过 Buffer 类提供的
limit(int newLimit) 方法来设置限制的值。

06. compact() 保留未读内容的切换方法

上面提到,如果使用 filp 方法来转换读取的模式,实质上是不会保留未读取的内容的,这里要将的 compact 方法则会保留未读取的内容,下面来看一下具体的实现。

在这里插入图片描述

    public ByteBuffer compact() {
        int pos = position(); // 获取 position
        int lim = limit(); // 获取 limit(读限制)
        assert (pos <= lim); // 断言
        int rem = (pos <= lim ? lim - pos : 0); // 剩余未读的内容
        
        // 从 hb 中读取从 pos 长度为 rem 的内容到 hb 从 0 开始长度为 rem 中
        System.arraycopy(hb, ix(pos), hb, ix(0), rem);
        position(rem); // 设置新的指针(写指针)
        limit(capacity()); // 将 limit 设置为容量
        discardMark(); // 将 mark 设置为 -1
        return this;
    }

方法中使用 void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); 方法将未读取的内容保留到 byte 数组中,实现了上面图片中的效果。

07. put 方法和 clear 方法

通过上面的阅读,再来看两个比较轻松的方法,put 方法用于将 byte 存入 Buffer 中,clear 用于将 Buffer 重置到初始状态。

    public ByteBuffer put(byte x) {
        hb[ix(nextPutIndex())] = x; // 获取下一个存放数据的位置
        return this;
    }
    
    final int nextPutIndex() {
        int p = position;
        if (p >= limit)
            throw new BufferOverflowException();
        position = p + 1; // 自增操作
        return p;
    }
    
    protected int ix(int i) {
        return i + offset;
    }

put 中使用到的 ix 方法是 ByteBuffer 类中的一个受保护的辅助方法,用于计算指定索引位置在底层数组中的实际位置;offset 是一个成员变量,表示底层数组中的偏移量。

    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

clear 方法则是将指针直接重置到初始位置(数据并未删除)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何高速下载,百度 阿里 天翼 等网盘内的内容

如何高速下载&#xff0c;百度 阿里 天翼 等网盘内的内容&#x1f3c5; 前言教程下期更新预报&#x1f3c5; 前言 近段时间经常给大家分享各种视频教程&#xff0c;由于分享的资料是用迅雷网盘存的&#xff0c;但是绝大部分用户都是使用的某度&#xff0c;阿某的这些网盘&…

AI工具大揭秘:如何改变我们的工作和生活

文章目录 &#x1f4d1;前言一、常用AI工具&#xff1a;便利与高效的结合1.1 语音助手1.2 智能推荐系统1.3 自然语言处理工具 二、创新AI应用&#xff1a;不断突破与发展2.1 医疗诊断AI2.2 智能家居2.3 无人驾驶技术 三、AI工具在人们生活中的应用和影响3.1 生活方式的变化3.2 …

旅游系列之:庐山美景

旅游系列之&#xff1a;庐山美景 一、路线二、住宿二、庐山美景 一、路线 庐山北门乘坐大巴上山&#xff0c;住在上山的酒店东线大巴游览三叠泉&#xff0c;不需要乘坐缆车&#xff0c;步行上下三叠泉即可&#xff0c;线路很短 二、住宿 长江宾馆庐山分部 二、庐山美景

Ubuntu 20.04安装桌面XFCE

1.安装Xfce软件包 $ sudo apt update $ sudo apt install xfce42.选择gdm3和lightdm 我这里选择的是lightdm LightDM&#xff0c;即&#xff1a;Light Display Manager&#xff0c;是一个全新的、轻量的Linux桌面的桌面显示管理器&#xff0c;而传统的Ubuntu用的是GNOME桌面…

HR面试测评,招聘行政部门主管的人才测评方案

把合适的人放入到合适的岗位中&#xff0c;可以实现双赢&#xff08;企业效益和个人成就&#xff09;&#xff0c;人力资源管理者HR又该如何去发掘行政主管岗位的人才&#xff1f; 行政部门主管属于管理层岗位&#xff0c;在企业发展中有重要的作用&#xff0c;可以协助企业…

Docker私有镜像仓库搭建 带图形化界面的

搭建镜像仓库可以基于Docker官方提供的DockerRegistry来实现。 官网地址&#xff1a;https://hub.docker.com/_/registry 先配置私服的信任地址: # 打开要修改的文件 vi /etc/docker/daemon.json # 添加内容&#xff1a; "insecure-registries":["http://192.…

FastAPI - Pydantic相关应用

参考链接&#xff1a;Pydantic官方文档 文章目录 定义数据模型创建模型实例数据验证数据转换模型转换模型更新模型配置辅助类Fieldvalidator Pydantic 是一个 Python 库&#xff0c;主要用于数据验证和管理。数据验证是指检查数据是否符合预定的规则和格式&#xff0c;比如检查…

xss注入漏洞解析(下)

DOM型XSS 概念 DOM全称Document Object Model&#xff0c;使用DOM可以使程序和脚本能够动态访问和更新文档的内容、结 构及样式。DOM型XSS其实是一种特殊类型的反射型XSS&#xff0c;它是基于DOM文档对象模型的一种漏洞。 HTML的标签都是节点&#xff0c;而这些节点组成了DOM的…

TeXCount failed. Please refer to LaTeX Utilities Output for details.

写LaTeX的时候总是报这个错、看了下网上也没有什么好的解决方法、就是单词计数器无法使用 我的解决方法: 看下驱动器是否还在&#xff0c;不在的话重新安装一下、不要把驱动器给删除了

开关门机关

根物体创建动画 子物体录制动画 ctrl6&#xff1a;调用动画窗口 添加关键帧&#xff1a;输入添加关键帧到第几帧&#xff0c;然后点击录制&#xff0c;最后在该物体的面板上修改其位置等&#xff0c;记得添加完要结束录制 搞个父物体是为了让动画的可移植性变高 设置触发器方…

基于OpenCv的图像金字塔

⚠申明&#xff1a; 未经许可&#xff0c;禁止以任何形式转载&#xff0c;若要引用&#xff0c;请标注链接地址。 全文共计3077字&#xff0c;阅读大概需要3分钟 &#x1f308;更多学习内容&#xff0c; 欢迎&#x1f44f;关注&#x1f440;【文末】我的个人微信公众号&#xf…

人工智能大模型应用指南

大家好&#xff0c;我是爱编程的喵喵。双985硕士毕业&#xff0c;现担任全栈工程师一职&#xff0c;热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。…

【Flask 系统教程 2】路由的使用

Flask 是一个轻量级的 Python Web 框架&#xff0c;其简洁的设计使得构建 Web 应用变得轻而易举。其中&#xff0c;路由是 Flask 中至关重要的一部分&#xff0c;它定义了 URL 与视图函数之间的映射关系&#xff0c;决定了用户请求的处理方式。在本文中&#xff0c;我们将深入探…

设计模式——行为型模式——策略模式

策略模式 定义 策略模式定义了一系列算法&#xff0c;并将每个算法封装起来&#xff0c;使它们可以相互替换&#xff0c;且算法的变化不会影响使用算法的客户。 策略模式属于对象行为模式&#xff0c;它通过对算法进行封装&#xff0c;把使用算法的责任和算法的实现分割开来&a…

【ARM Cortex-M3指南】3:Cortex-M3基础

文章目录 三、Cortex-M3基础3.1 寄存器3.1.1 通用目的寄存器 R0~R73.1.2 通用目的寄存器 R8~R123.1.3 栈指针 R133.1.4 链接寄存器 R143.1.5 程序计数器 R15 3.2 特殊寄存器3.2.1 程序状态寄存器3.2.2 PRIMASK、FAULTMASK和BASEPRI寄存器3.2.3 控制寄存器 3.3 操作模式3.4 异常…

# 在 Windows 命令提示符(cmd)中,可以通过以下方法设置长命令自动换行

在 Windows 命令提示符&#xff08;cmd&#xff09;中&#xff0c;可以通过以下方法设置长命令自动换行 1、点击 cmd 窗口左上角标题栏&#xff0c;选择【属性】。 2、在【属性】菜单中&#xff0c;依次点击【选项】&#xff0c;找到【编辑选项】下面的【自动换行】&#xff…

经纬度聚类:聚类算法比较

需求&#xff1a; 将经纬度数据&#xff0c;根据经纬度进行聚类 初始数据 data.csv K均值聚类 简介 K均值&#xff08;K-means&#xff09;聚类是一种常用的无监督学习算法&#xff0c;用于将数据集中的样本分成K个不同的簇&#xff08;cluster&#xff09;。其基本思想是…

OpenCV | 入门

OpenCV | 入门 安装 参考教程 基础知识 V G A 640 480 VGA 640 \times 480 VGA640480 H D 1280 720 HD 1280 \times 720 HD1280720 F H D 1920 1080 FHD 1920 \times 1080 FHD19201080 4 K 3840 2160 4K 3840 \times 2160 4K38402160 这些都表示了固定的像素…

AI-数学-高中52-离散型随机变量概念及其分布列、两点分布

原作者视频&#xff1a;【随机变量】【一数辞典】2离散型随机变量及其分布列_哔哩哔哩_bilibili 离散型随机变量分布列&#xff1a;X表示离散型随机变量可能在取值&#xff0c;P:对应分布在概率&#xff0c;P括号里X1表示事件的名称。 示例&#xff1a;