18.Netty源码之ByteBuf 详解


highlight: arduino-light

ByteBuf 是 Netty 的数据容器,所有网络通信中字节流的传输都是通过 ByteBuf 完成的。

然而 JDK NIO 包中已经提供了类似的 ByteBuffer 类,为什么 Netty 还要去重复造轮子呢?本节课我会详细地讲解 ByteBuf。

JDK NIO的ByteBuffer

我们首先介绍下 JDK NIO 的 ByteBuffer,才能知道 ByteBuffer 有哪些缺陷和痛点。下图展示了 ByteBuffer 的内部结构:

image.png

从图中可知,ByteBuffer 包含以下四个基本属性:

  • mark:为某个读取过的关键位置做标记,方便回退到该位置;
  • position:当前读取的位置;
  • limit:buffer 中有效的数据长度大小;
  • capacity:初始化时的空间容量。

以上四个基本属性的关系是:mark <= position <= limit <= capacity。结合 ByteBuffer 的基本属性,不难理解它在使用上的一些缺陷。

第一,ByteBuffer 分配的长度是固定的,无法动态扩缩容,所以很难控制需要分配多大的容量。如果分配太大容量,容易造成内存浪费;如果分配太小,存放太大的数据会抛出 BufferOverflowException 异常。在使用 ByteBuffer 时,为了避免容量不足问题,你必须每次在存放数据的时候对容量大小做校验,如果超出 ByteBuffer 最大容量,那么需要重新开辟一个更大容量的 ByteBuffer,将已有的数据迁移过去。整个过程相对烦琐,对开发者而言是非常不友好的。

第二,ByteBuffer 只能通过 position 获取当前可操作的位置,因为读写共用的 position 指针,所以需要频繁调用 flip、rewind 方法切换读写状态,开发者必须很小心处理 ByteBuffer 的数据读写,稍不留意就会出错。

ByteBuffer 作为网络通信中高频使用的数据载体,显然不能够满足 Netty 的需求,Netty 重新实现了一个性能更高、易用性更强的 ByteBuf,相比于 ByteBuffer 它提供了很多非常酷的特性:

  • 容量可以按需动态扩展,类似于 StringBuffer;
  • 读写采用了不同的指针,读写模式可以随意切换,不需要调用 flip 方法;
  • 通过内置的复合缓冲类型可以实现零拷贝;
  • 支持引用计数;
  • 支持缓存池。

这里我们只是对 ByteBuf 有一个简单的了解,接下来我们就一起看下 ByteBuf 是如何实现的吧。

痛点:

1.readIndex 和 writeIndex没有分开

2.需要调用flip和clear方法

3.api命名区分度低

Netty ByteBuf 内部结构

同样我们看下 ByteBuf 的内部结构,与 ByteBuffer 做一个对比。

Netty11(2).png

从图中可以看出,ByteBuf 包含三个指针:读指针 readerIndex写指针 writeIndex最大容量 maxCapacity,根据指针的位置又可以将 ByteBuf 内部结构可以分为四个部分:

第一部分是废弃字节,表示已经丢弃的无效字节数据。

第二部分是可读字节,表示 ByteBuf 中可以被读取的字节内容,可以通过 writeIndex - readerIndex 计算得出。从 ByteBuf 读取 N 个字节,readerIndex 就会自增 N,readerIndex 不会大于 writeIndex,当 readerIndex == writeIndex 时,表示 ByteBuf 已经不可读。

第三部分是可写字节,向 ByteBuf 中写入数据都会存储到可写字节区域。向 ByteBuf 写入 N 字节数据,writeIndex 就会自增 N,当 writeIndex 超过 capacity,表示 ByteBuf 容量不足,需要扩容。

第四部分是可扩容字节,表示 ByteBuf 最多还可以扩容多少字节,当 writeIndex 超过 capacity 时,会触发 ByteBuf 扩容,最多扩容到 maxCapacity 为止,超过 maxCapacity 再写入就会出错。

由此可见,Netty 重新设计的 ByteBuf 有效地区分了可读、可写以及可扩容数据,解决了 ByteBuffer 无法扩容以及读写模式切换烦琐的缺陷。

接下来,我们一起学习下 ByteBuf 的核心 API,你可以把它当作 ByteBuffer 的替代品单独使用。

引用计数

ByteBuf 是基于引用计数设计的,它实现了 ReferenceCounted 接口,ByteBuf 的生命周期是由引用计数所管理。

只要引用计数大于 0,表示 ByteBuf 还在被使用;

当 ByteBuf 不再被其他对象所引用时,引用计数为 0,那么代表该对象可以被释放。

当新创建一个 ByteBuf 对象时,它的初始引用计数为 1,当 ByteBuf 调用 release() 后,引用计数减 1。

所以不要误以为调用了 release() 就会保证 ByteBuf 对象一定会被回收。因为可能计数是2。

你可以结合以下的代码示例做验证:

md ByteBuf buffer = ctx.alloc().directBuffer(); assert buffer.refCnt() == 1; buffer.release(); assert buffer.refCnt() == 0;

引用计数对于 Netty 设计缓存池化有非常大的帮助,当引用计数为 0,该 ByteBuf 可以被放入到对象池中,避免每次使用 ByteBuf 都重复创建,对于实现高性能的内存管理有着很大的意义。

此外 Netty 可以利用引用计数的特点实现内存泄漏检测工具。

JVM 并不知道 Netty 的引用计数是如何实现的,当 ByteBuf 对象不可达时,一样会被 GC 回收掉,但是如果此时 ByteBuf 的引用计数不为 0,那么该对象就不会释放或者被放入对象池,从而发生了内存泄漏。

Netty 会对分配的 ByteBuf 进行抽样分析,检测 ByteBuf 是否已经不可达且引用计数大于 0,判定内存泄漏的位置并输出到日志中,你需要关注日志中 LEAK 关键字。

ByteBuf 分类

ByteBuf 有多种实现类,每种都有不同的特性,下图是 ByteBuf 的家族图谱,可以划分为三个不同的维度:Heap/DirectPooled/UnpooledUnsafe/非 Unsafe,我逐一介绍这三个维度的不同特性。

image (3).png

Heap/Direct

Heap/Direct 就是堆内和堆外内存

Heap 指的是在 JVM 堆内分配,底层依赖的是字节数据;

Direct 则是堆外内存,不受 JVM 限制,分配方式依赖 JDK 底层的 ByteBuffer。

Pooled/Unpooled

Pooled/Unpooled 表示池化还是非池化内存

Pooled 是从预先分配好的内存中取出,使用完可以放回 ByteBuf 内存池,等待下一次分配。

而 Unpooled 是直接调用系统 API 去申请内存,确保能够被 JVM GC 管理回收。

Unsafe/非 Unsafe

Unsafe/非 Unsafe 的区别在于操作方式是否安全。

Unsafe 表示每次调用 JDK 的 Unsafe 对象操作物理内存,依赖 offset + index 的方式操作数据。

非 Unsafe 则不需要依赖 JDK 的 Unsafe 对象,直接通过数组下标的方式操作数据。

ByteBuf 核心 API

我会分为指针操作数据读写内存管理三个方面介绍 ByteBuf 的核心 API。在开始讲解 API 的使用方法之前,先回顾下之前我们实现的自定义解码器,以便于加深对 ByteBuf API 的理解。

java public final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // 判断 ByteBuf 可读取字节 if (in.readableBytes() < 14) { return; } in.markReaderIndex(); // 标记 ByteBuf 读指针位置 in.skipBytes(2); // 跳过魔数 in.skipBytes(1); // 跳过协议版本号 //获取序列化方式 byte serializeType = in.readByte(); in.skipBytes(1); // 跳过报文类型 in.skipBytes(1); // 跳过状态字段 in.skipBytes(4); // 跳过保留字段 //获取数据长度 int dataLength = in.readInt(); if (in.readableBytes() < dataLength) { in.resetReaderIndex(); // 重置 ByteBuf 读指针位置 return; } //根据数据长度构造 byte[] byte[] data = new byte[dataLength]; in.readBytes(data); //根据序列化方式 反序列化 SerializeService serializeService = getSerializeServiceByType(serializeType); //反序列化为对象 Object obj = serializeService.deserialize(data); if (obj != null) { out.add(obj); } }

指针操作 API

readerIndex()

readerIndex() 返回的是当前的读指针的 readerIndex 位置

writeIndex()

writeIndex() 返回的当前写指针 writeIndex 位置。

markReaderIndex()

markReaderIndex() 用于保存当前readerIndex 的位置。

resetReaderIndex()

resetReaderIndex() 则将当前 readerIndex 重置为之前markReaderIndex保存的位置。

markReaderIndex和resetReaderIndex这对 API 在实现协议解码时最为常用,例如在上述自定义解码器的源码中,在读取协议内容长度字段之前,先使用 markReaderIndex() 保存了 readerIndex 的位置,如果 ByteBuf 中可读字节数小于长度字段的值,则表示 ByteBuf 还没有一个完整的数据包,此时直接使用 resetReaderIndex() 重置readerIndex 的位置。

此外对应的写指针操作还有 markWriterIndex() 和 resetWriterIndex(),与读指针的操作类似,我就不再一一赘述了。

数据读写 API

isReadable()

isReadable() 用于判断 ByteBuf 是否可读,如果 writerIndex 大于 readerIndex,那么 ByteBuf 是可读的,否则是不可读状态。

readableBytes()

readableBytes() 可以获取 ByteBuf 当前可读取的字节数,可以通过 writerIndex - readerIndex 计算得到

readBytes(byte[] dst)

writeBytes(byte[] src)

readBytes() 和 writeBytes() 是两个最为常用的方法。

readBytes() 是将 ByteBuf 的数据读取相应的字节到字节数组dst 中,readBytes() 经常结合 readableBytes() 一起使用,

dst 字节数组的大小通常等于 readableBytes() 的大小。

java //收到的消息 ByteBuf bytebuf = (ByteBuf) msg; //构建字节数组 byte[] req = new byte[bytebuf.readableBytes()]; //将ByteBuf中的数据读取到字节数组中 bytebuf.readBytes(req); String body = new String(req, "UTF-8");

readByte()

writeByte(int value)

readByte() 是从 ByteBuf 中读取一个字节,相应的 readerIndex + 1;同理 writeByte 是向 ByteBuf 写入一个字节,相应的 writerIndex + 1。

类似的 Netty 提供了 8 种基础数据类型的读取和写入,例如 readChar()、readShort()、readInt()、readLong()、writeChar()、writeShort()、writeInt()、writeLong() 等,在这里就不详细展开了。

getByte(int index)

setByte(int index, int value)

readByte() 是从 ByteBuf 中读取一个字节,相应的 readerIndex + 1;

同理 writeByte 是向 ByteBuf 写入一个字节,相应的 writerIndex + 1。

与 readByte() 和 writeByte() 相对应的还有 getByte() 和 setByte(),get/set 系列方法也提供了 8 种基础类型的读写,那么这两个系列的方法有什么区别呢?

read/write 方法在读写时会改变readerIndex 和 writerIndex 指针,而 get/set 方法则不会改变指针位置。

release() & retain()

之前已经介绍了引用计数的基本概念,每调用一次 release() 引用计数减 1,每调用一次 retain() 引用计数加 1。

slice()

返回ByteBuf可读字节的一部分。 修改返回的ByteBuf或当前ByteBuf会影响彼此的内容, 同时它们维护单独的索引和标记,此方法不会修改当前ByteBuf的readerIndex或writerIndex *另请注意,此方法不会调用{@link #retain()},因此不会增加引用计数

slice() 等同于 slice(buffer.readerIndex(), buffer.readableBytes()),默认截取 readerIndex 到 writerIndex 之间的数据,最大容量 maxCapacity 为原始 ByteBuf 的可读取字节数,底层分配的内存、引用计数都与原始的 ByteBuf 共享。

duplicate()

duplicate() 与 slice() 不同的是,duplicate()截取的是整个原始 ByteBuf 信息,底层分配的内存、引用计数也是共享的。如果向 duplicate() 分配出来的 ByteBuf 写入数据,那么都会影响到原始的 ByteBuf 底层数据。

返回共享当前ByteBuf信息的新ByteBuf,他们使用独立的readIndex writeIndex markIndex *修改返回的ByteBuf或当前ByteBuf会影响彼此的内容,同时它们维护单独的索引和标记, *此方法不会修改当前ByteBuf的readerIndex或writerIndex, 另请注意,此方法不会调用{@link #retain()},因此不会增加引用计数

copy()

会从原始的 ByteBuf 中拷贝所有信息,所有数据都是独立的,向 copy() 分配的 ByteBuf 中写数据不会影响原始的 ByteBuf。

返回ByteBuf的可读字节的拷贝。修改返回的ByteBuf内容与当前ByteBuf完全不会相互影响。

此方法不会修改当前ByteBuf的readerIndex或writerIndex

到底为止,ByteBuf 的核心 API 我们基本已经介绍完了,ByteBuf 读写指针分离的小设计,确实带来了很多实用和便利的功能,在开发的过程中不必再去想着 flip、rewind 这种头疼的操作了。

内存管理 API

未池化堆内存

java ByteBuf heapByteBuf = Unpooled.buffer(10);

未池化直接内存

java ByteBuf directByteBuf = Unpooled.directBuffer(10);

池化堆内存

java PooledByteBufAllocator allocator = new PooledByteBufAllocator(false); ByteBuf pHeapByteBuf = allocator.buffer();

池化直接内存

java PooledByteBufAllocator allocator2 = new PooledByteBufAllocator(true);

ByteBuf 实战演练

学习完 ByteBuf 的内部构造以及核心 API 之后,我们下面通过一个简单的示例演示一下 ByteBuf 应该如何使用,代码如下所示。

java public class ByteBufTest { public static void main(String[] args) { // static final ByteBufAllocator DEFAULT_ALLOCATOR; // 根据allocType创建不同的分配器 如果没有值默认使用池化技术 // alloc = UnpooledByteBufAllocator.DEFAULT; // alloc = PooledByteBufAllocator.DEFAULT; // 初始是6 最大是10 ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(6, 10); printByteBufInfo("ByteBufAllocator.buffer(5, 10)", buffer); buffer.writeBytes(new byte[]{1, 2}); printByteBufInfo("write 2 Bytes", buffer); buffer.writeInt(100); printByteBufInfo("write Int 100", buffer); buffer.writeBytes(new byte[]{3, 4, 5}); printByteBufInfo("write 3 Bytes", buffer); byte[] read = new byte[buffer.readableBytes()]; buffer.readBytes(read); printByteBufInfo("readBytes(" + buffer.readableBytes() + ")", buffer); printByteBufInfo("BeforeGetAndSet", buffer); System.out.println("getInt(2): " + buffer.getInt(2)); buffer.setByte(1, 0); System.out.println("getByte(1): " + buffer.getByte(1)); printByteBufInfo("AfterGetAndSet", buffer); } private static void printByteBufInfo(String step, ByteBuf buffer) { System.out.println("------" + step + "-----"); System.out.println("readerIndex(): " + buffer.readerIndex()); System.out.println("writerIndex(): " + buffer.writerIndex()); System.out.println("isReadable(): " + buffer.isReadable()); System.out.println("isWritable(): " + buffer.isWritable()); System.out.println("readableBytes(): " + buffer.readableBytes()); System.out.println("writableBytes(): " + buffer.writableBytes()); System.out.println("maxWritableBytes(): " + buffer.maxWritableBytes()); System.out.println("capacity(): " + buffer.capacity()); System.out.println("maxCapacity(): " + buffer.maxCapacity()); } }

java ------ByteBufAllocator.buffer(5, 10)----- readerIndex(): 0 writerIndex(): 0 isReadable(): false isWritable(): true readableBytes(): 0 writableBytes(): 6 maxWritableBytes(): 10 capacity(): 6 maxCapacity(): 10 ------write 2 Bytes----- readerIndex(): 0 writerIndex(): 2 isReadable(): true isWritable(): true readableBytes(): 2 writableBytes(): 4 maxWritableBytes(): 8 capacity(): 6 maxCapacity(): 10 ------write Int 100----- readerIndex(): 0 writerIndex(): 6 isReadable(): true isWritable(): false readableBytes(): 6 writableBytes(): 0 maxWritableBytes(): 4 capacity(): 6 maxCapacity(): 10 ------write 3 Bytes----- readerIndex(): 0 writerIndex(): 9 isReadable(): true isWritable(): true readableBytes(): 9 writableBytes(): 1 maxWritableBytes(): 1 capacity(): 10 maxCapacity(): 10 ------readBytes(0)----- readerIndex(): 9 writerIndex(): 9 isReadable(): false isWritable(): true readableBytes(): 0 writableBytes(): 1 maxWritableBytes(): 1 capacity(): 10 maxCapacity(): 10 ------BeforeGetAndSet----- readerIndex(): 9 writerIndex(): 9 isReadable(): false isWritable(): true readableBytes(): 0 writableBytes(): 1 maxWritableBytes(): 1 capacity(): 10 maxCapacity(): 10 getInt(2): 100 getByte(1): 0 ------AfterGetAndSet----- readerIndex(): 9 writerIndex(): 9 isReadable(): false isWritable(): true readableBytes(): 0 writableBytes(): 1 maxWritableBytes(): 1 capacity(): 10 maxCapacity(): 10

结合代码示例,我们总结一下 ByteBuf API 使用时的注意点:

  • write 系列方法会改变 writerIndex 位置,当 writerIndex 等于 capacity 的时候,Buffer 置为不可写状态;
  • 向不可写 Buffer 写入数据时,Buffer 会尝试扩容,但是扩容后 capacity 最大不能超过 maxCapacity,如果写入的数据超过 maxCapacity,程序会直接抛出异常;
  • read 系列方法会改变 readerIndex 位置,get/set 系列方法不会改变 readerIndex/writerIndex 位置。

总结

Netty 强大的数据容器 ByteBuf,它不仅解决了 JDK NIO 中 ByteBuffer 的缺陷,而且提供了易用性更强的接口。很多开发者已经使用 ByteBuf 代替 ByteBuffer,即便他没有在写一个网络应用,也会单独使用 ByteBuf。ByteBuf 作为 Netty 中最基础的数据结构。

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

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

相关文章

VMware搭建Hadoop集群 for Windows(完整详细,实测可用)

目录 一、VMware 虚拟机安装 &#xff08;1&#xff09;虚拟机创建及配置 &#xff08;2&#xff09;创建工作文件夹 二、克隆虚拟机 三、配置虚拟机的网络 &#xff08;1&#xff09;虚拟网络配置 &#xff08;2&#xff09;配置虚拟机 主机名 &#xff08;3&#xf…

【LLM】大语言模型学习之LLAMA 2:Open Foundation and Fine-Tuned Chat Model

大语言模型学习之LLAMA 2:Open Foundation and Fine-Tuned Chat Model 快速了解预训练预训练模型评估微调有监督微调(SFT)人类反馈的强化学习(RLHF)RLHF结果局限性安全性预训练的安全性安全微调上手就干使用登记代码下载获取模型转换模型搭建Text-Generation-WebUI分发模型…

高效率,38V最大输入单电感同步升/降稳压器SYV939C

SYV939是一种高压同步降压-升压转换器。该器件工作在4V至28V的宽输入电压范围内&#xff0c;具有10max平均电感电流能力。四个集成的低RDS(ON)开关最大限度地减少了传导损耗。 SYV939c包括完整的保护功能&#xff0c;如输出过流/短路保护&#xff0c;过压保护和热停机&#xff…

了解Unity编辑器之组件篇Playables和Rendering(十)

Playables 一、Playable Director&#xff1a;是一种用于控制和管理剧情、动画和音频的工具。它作为一个中央控制器&#xff0c;可以管理播放动画剧情、视频剧情和音频剧情&#xff0c;以及它们之间的时间、顺序和交互。 Playable Director组件具有以下作用&#xff1a; 剧情控…

【MATLAB第61期】基于MATLAB的GMM高斯混合模型回归数据预测

【MATLAB第61期】基于MATLAB的GMM高斯混合模型回归数据预测 高斯混合模型GMM广泛应用于数据挖掘、模式识别、机器学习和统计分析。其中&#xff0c;它们的参数通常由最大似然和EM算法确定。关键思想是使用高斯混合模型对数据&#xff08;包括输入和输出&#xff09;的联合概率…

最新版Onenet云平台HTTP协议接入上传数据

2023年最新版Onenet更新后&#xff0c;原来的多协议接口已经找不到&#xff0c;由于需要用HTTP接入&#xff0c;就研究了一下新版Onenet云平台&#xff0c;搞清楚Onenet云平台的鉴权信息&#xff0c;就知道怎么上传数据了&#xff0c;包括后续上传实际数据&#xff0c;其实只需…

JVM简述

JDK&JRE&JVMJVM运行时内存结构图方法区堆区栈区程序计数器本地方法栈 JVM 的主要组成部分及其作用 JDK&JRE&JVM JVM就是java虚拟机&#xff0c;一台虚拟的机器&#xff0c;用来运行java代码 但并不是只有这台机器就可以的&#xff0c;java程序在运行时需要依赖…

【图论】kruskal算法

一.介绍 Kruskal&#xff08;克鲁斯卡尔&#xff09;算法是一种用于解决最小生成树问题的贪心算法。最小生成树是指在一个连通无向图中&#xff0c;选择一棵包含所有顶点且边权重之和最小的树。 下面是Kruskal算法的基本步骤&#xff1a; 将图中的所有边按照权重从小到大进行…

C# 快速写入日志 不卡线程 生产者 消费者模式

有这样一种场景需求&#xff0c;就是某个方法&#xff0c;对耗时要求很高&#xff0c;但是又要记录日志到数据库便于分析&#xff0c;由于访问数据库基本都要几十毫秒&#xff0c;可在方法里写入BlockingCollection&#xff0c;由另外的线程写入数据库。 可以看到&#xff0c;在…

2023河南萌新联赛第(三)场:郑州大学 A - 发工资咯

2023河南萌新联赛第&#xff08;三&#xff09;场&#xff1a;郑州大学 A - 发工资咯 时间限制&#xff1a;C/C 2秒&#xff0c;其他语言4秒 空间限制&#xff1a;C/C 262144K&#xff0c;其他语言524288K 64bit IO Format: %lld 题目描述 一个公司有n个人&#xff0c;每个月都…

TCP如何保证服务的可靠性

TCP如何保证服务的可靠性 确认应答超时重传流量控制滑动窗口机制概述发送窗口和接收窗口的工作原理几种滑动窗口协议1比特滑动窗口协议&#xff08;停等协议&#xff09;后退n协议选择重传协议 采用滑动窗口的问题&#xff08;死锁可能&#xff0c;糊涂窗口综合征&#xff09;死…

iostat工具使用

文章目录 iostat命令简介iostat命令参数 iostat输出信息CPU利用率输出信息磁盘利用率输出信息更详细的磁盘利用率输出信息 iostat命令使用示例iostat -kdx 1 iostat数据来源相关参考 iostat命令简介 iostat工具可用于CPU使用统计信息和设备的输入输出统计信息。iostat能支持显…

数据结构—数组和广义表

4.2数组 数组&#xff1a;按一定格式排列起来的&#xff0c;具有相同类型的数据元素的集合。 **一维数组&#xff1a;**若线性表中的数据元素为非结果的简单元素&#xff0c;则称为一维数组。 **一维数组的逻辑结构&#xff1a;**线性结构&#xff0c;定长的线性表。 **声明…

Vue通过指令 命令将打包好的dist静态文件上传到腾讯云存储桶 (保存原有存储目录结构)

1、在项目根目录创建uploadToCOS.js文件 (建议起简单的名字 方便以后上传输入命令方便) 2、uploadToCOS.js文件代码编写 const path = require(path); const fs = require(fs); const COS = require(cos-nodejs-sdk-v5);// 配置腾讯云COS参数 const cos = new COS({SecretI…

基于Docker-compose创建LNMP环境并运行Wordpress网站平台

基于Docker-compose创建LNMP环境并运行Wordpress网站平台 1.Docker-Compose概述2.YAML文件格式及编写注意事项3.Docker-Compose配置常用字段4.Docker Compose常用命令5.使用Docker-compose创建LNMP环境&#xff0c;并运行Wordpress网站平台1. Docker Compose 环境安装下载安装查…

《入门级-Cocos2d 4.0塔防游戏开发》---第二课:游戏加载界面开发

目录 一、开发环境介绍 二、开发内容 2.1 修改窗口的大小。 2.2 添加加载场景相关代码 2.3 添加资源 三、显示效果 四、知识点 4.1 Sprite 4.2 定时器 一、开发环境介绍 操作系统&#xff1a;UOS1060专业版本。 cocos2dx:版本 环境搭建教程&#xff1a; 统信UOS下配…

cURL error 1: Protocol “https“ not supported or disabled in libcurl

1、php项目composer update报错 2、curl -V检查 发现curl已经支持了https了 3、php版本检查 4、php插件检查 插件也已经含有openssl组件了 5、phpinfo检查 curl是否开启ssl 定位到问题所在&#xff0c;php7.4的 curl扩展不支持 https 需要重装 php7.4的curl扩展 6、curl下载 下…

大学生活题解

样例输入&#xff1a; 3 .xA ... Bx.样例输出&#xff1a; 6思路分析&#xff1a; 这道题只需要在正常的广搜模板上多维护一个— —方向&#xff0c;如果当前改变方向&#xff0c;就坐标不变&#xff0c;方向变&#xff0c;步数加一&#xff1b;否则坐标变&#xff0c;方向不…

微信小程序radio单选按钮选中与取消

wxml <view bindtapcheckedTap><radio checked"{{checked}}">设为默认</radio> </view> wxss <style lang"less" > radio .wx-radio-input {border-radius: 50%; /* 圆角 */width: 24rpx;border: 2rpx solid #5e5e5f;hei…

centos7安装tomcat

安装tomcat 必须依赖 JDK 环境&#xff0c;一定要提前装好JDK保证可以使用 一、下载安装包 到官网下载 上传到linux 服务器 二、安装tomcat 创建tomcat 文件夹 mkdir -p /usr/local/tomcat设置文件夹权限 chmod 757 tomcat将安装包上传至 新建文件夹 解压安装包 tar zx…