Netty笔记03-组件Channel
文章目录
- Channel概述
- Channel 的概念
- Channel 的主要功能
- Channel 的生命周期
- Channel 的状态
- Channel 的类型
- channel 的主要方法
- ChannelFuture
- CloseFuture
- 💡 netty异步提升的是什么
- 要点
- 总结
Channel概述
Channel 的概念
在 Netty 中,Channel 是一个非常重要的概念,它代表了网络连接的抽象(一个网络连接),用于进行数据的读取和写入操作。Channel 是 Netty 中处理网络 I/O 的核心组件之一,它将网络 I/O 操作封装在一个统一的接口下,使得开发人员可以更容易地编写高性能的网络应用程序。
Channel 是 Netty 中的接口,它定义了一组基本的操作,如打开、关闭、读取、写入等。Netty 为不同的网络通信协议提供了不同的Channel 实现,比如 TCP 的 SocketChannel,UDP 的 DatagramChannel 等。
Channel 的主要功能
- 读取数据:从网络中读取数据。
- 写入数据:将数据写入到网络中。
- 关闭连接:关闭当前的 Channel。
- 绑定地址:将 Channel 绑定到特定的本地地址(IP 地址和端口号)。
注册到 EventLoop:将 Channel 注册到 EventLoop,以便处理 I/O 事件。
获取ChannelPipeline:每个 Channel 都有一个 ChannelPipeline,用于处理入站和出站的数据流。
Channel 的生命周期
一个 Channel 的生命周期通常包括以下几个阶段:
- 创建:当一个新连接建立时,Netty 会创建一个新的 Channel。
- 注册:新创建的 Channel 会注册到一个 EventLoop 上。
- 激活:当 Channel 被绑定到一个 Socket 地址时,它会变为激活状态。
- 读写操作:Channel 可以进行读取和写入操作。
- 关闭:当连接关闭时,Channel 也会被关闭。
Channel 的状态
Channel 有几个重要的状态,包括但不限于:
- 注册状态:Channel 是否已经被注册到 EventLoop。
channel.isRegistered() // 检查是否已注册
- 活跃状态:Channel 是否已经绑定到一个 Socket 地址。
channel.isActive() // 检查是否活跃
- 打开状态:Channel 是否处于打开状态。
channel.isOpen() // 检查是否打开
Channel 的类型
Netty 为不同的网络通信协议提供了不同的 Channel 实现:
- TCP 通信:NioServerSocketChannel 和 NioSocketChannel。
- UDP 通信:NioDatagramChannel。
- 文件传输:FileRegion。
channel 的主要方法
- close() 可以用来关闭 channel
- closeFuture() 用来处理 channel 的关闭
- sync 方法作用是同步等待 channel 关闭
- 而 addListener 方法是异步等待 channel 关闭
- pipeline() 方法添加处理器
- write() 方法将数据写入(只会将数据写入到channel的缓冲区中,具体什么时候发送数据则有很多条件,如在执行flush()或缓冲区达到一定大小)
- writeAndFlush() 方法将数据写入并刷出(将数据写入到channel的缓冲区中并立刻从缓冲区中发出)
ChannelFuture
使用上一章的服务器端和客户端代码
服务器端
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import jdk.internal.org.objectweb.asm.Handle;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.Charset;@Slf4j
public class EventLoopServer {public static void main(String[] args) {// 创建一个独立的 EventLoopGroup,将耗时的代码放到一个额外的组中线程处理EventLoopGroup group = new DefaultEventLoopGroup();new ServerBootstrap()// boss 和 worker// boss 只负责 ServerSocketChannel 上 accept 事件// worker 只负责 socketChannel 上的读写//group参数1:boss不需要指定线程数,因为ServerSocketChannel只会跟一个EventLoop进行绑定,// 又因为服务器只有一个,所以只会占用一个线程,不用指定线程数。//group参数1:work线程指定为两个.group(new NioEventLoopGroup(), new NioEventLoopGroup(2)).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {//如果读操作耗费的时间很上,会影响其他客户端的读写操作,// 一个work管理多个channel,如果其中一个耗时过长则会影响其他channel的读写操作ch.pipeline().addLast("handler1", new ChannelInboundHandlerAdapter() {/*** @param ctx* @param msg ByteBuf类型* @throws Exception*/@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;log.debug(buf.toString(Charset.defaultCharset()));ctx.fireChannelRead(msg); // 让消息传递给下一个handler,如果不添加则消息不会传递给handler2中}}).addLast(group, "handler2", new ChannelInboundHandlerAdapter() {@Override // ByteBufpublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;log.debug(buf.toString(Charset.defaultCharset()));}});}}).bind(8080);}
}
客户端代码
new Bootstrap().group(new NioEventLoopGroup()).channel(NioSocketChannel.class).handler(new ChannelInitializer<Channel>() {@Overrideprotected void initChannel(Channel ch) {ch.pipeline().addLast(new StringEncoder());}}).connect("127.0.0.1", 8080).sync().channel().writeAndFlush(new Date() + ": hello world!");
拆分上面的代码
ChannelFuture channelFuture = new Bootstrap().group(new NioEventLoopGroup()).channel(NioSocketChannel.class).handler(new ChannelInitializer<Channel>() {@Overrideprotected void initChannel(Channel ch) {ch.pipeline().addLast(new StringEncoder());}}).connect("127.0.0.1", 8080); // 标记1channelFuture.sync()
.channel()
.writeAndFlush(new Date() + ": hello world!");
- 1 处返回的是 ChannelFuture 对象,它的作用是利用 channel() 方法来获取 Channel 对象
注意 connect 方法是异步的,意味着不等连接建立,方法执行就返回了。因此 channelFuture 对象中不能【立刻】获得到正确的 Channel 对象。
实验如下:
// 2. 类中带有 Future,Promise 的类型都是和异步方法配套使用,用来处理结果
ChannelFuture channelFuture = new Bootstrap().group(new NioEventLoopGroup()).channel(NioSocketChannel.class).handler(new ChannelInitializer<NioSocketChannel>() {@Override // 在连接建立后被调用protected void initChannel(NioSocketChannel ch) throws Exception {ch.pipeline().addLast(new StringEncoder());}})// 1. 连接到服务器// 异步非阻塞, main 发起了调用,真正执行 connect 是 nio 线程.connect(new InetSocketAddress("localhost", 8080)); // 1s 秒后// 2.1 使用 sync 方法同步处理结果
//1.
System.out.println("sync前:"+channelFuture.channel());//[id: 0xe3489549]
//2.
channelFuture.sync(); // 阻塞住当前线程,直到nio线程连接建立完毕
//3.
System.out.println("sync后:"+channelFuture.channel());//[id: 0xe3489549, L:/127.0.0.1:54398 - R:localhost/127.0.0.1:8080]
Channel channel = channelFuture.channel();
log.debug("{}", channel);
//[DEBUG] [main] c.i.n.c.EventLoopClient - [id: 0xf540a105, L:/127.0.0.1:20434 - R:localhost/127.0.0.1:8080]
channel.writeAndFlush("hello, world");
channel.writeAndFlush("hello, world");
channel.writeAndFlush("hello, world");
System.out.println();
- 执行到 1 时,连接未建立,打印
[id: 0xe3489549]
- 执行到 2 时,sync 方法是同步等待连接建立完成
- 执行到 3 时,连接肯定建立了,打印
[id: 0xe3489549, L:/127.0.0.1:54398 - R:localhost/127.0.0.1:8080]
除了用 sync 方法可以让异步操作同步以外,还可以使用回调的方式:
// 2. 类中带有 Future,Promise 的类型都是和异步方法配套使用,用来处理结果ChannelFuture channelFuture = new Bootstrap().group(new NioEventLoopGroup()).channel(NioSocketChannel.class).handler(new ChannelInitializer<NioSocketChannel>() {@Override // 在连接建立后被调用protected void initChannel(NioSocketChannel ch) throws Exception {ch.pipeline().addLast(new StringEncoder());}})// 1. 连接到服务器// 异步非阻塞, main 发起了调用,真正执行 connect 是 nio 线程.connect(new InetSocketAddress("localhost", 8080)); // 1s 秒后// 2.2 使用 addListener(回调对象) 方法异步(将main线程处理的活交给其他线程)处理结果// 将等待连接建立、连接成功后处理结果全部交给其他的线程处理//1.System.out.println("sync前:"+channelFuture.channel());//[id: 0x3baac75f]channelFuture.addListener(new ChannelFutureListener() {
/** ↑* ↑ ← ← ↑* ↑* @param future 该future对象就是channelFuture对象* @throws Exception*/@Override// 在 nio 线程连接建立好之后,会调用 operationCompletepublic void operationComplete(ChannelFuture future) throws Exception {//2.System.out.println("sync后:"+channelFuture.channel());//[id: 0x3baac75f, L:/127.0.0.1:54744 - R:localhost/127.0.0.1:8080]Channel channel = future.channel();log.debug("{}", channel);//[DEBUG] [nioEventLoopGroup-2-1] c.i.n.c.EventLoopClient - [id: 0x2ac4448f, L:/127.0.0.1:20346 - R:localhost/127.0.0.1:8080]channel.writeAndFlush("hello, world");//调用以上三行代码的还是nio线程}});
- 执行到 1 时,连接未建立,打印
[id: 0x3baac75f]
- ChannelFutureListener 会在连接建立时被调用(其中 operationComplete 方法),因此执行到 2 时,连接肯定建立了,打印
[id: 0x3baac75f, L:/127.0.0.1:54744 - R:localhost/127.0.0.1:8080]
CloseFuture
需求:客户端的控制台不断的接受用户的输入,将用户输入的信息发给服务器端, 当不想发送信息时,输入q退出。
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.util.Scanner;@Slf4j
public class CloseFutureClient {public static void main(String[] args) throws InterruptedException {NioEventLoopGroup group = new NioEventLoopGroup();ChannelFuture channelFuture = new Bootstrap().group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer<NioSocketChannel>() {@Override // 在连接建立后被调用protected void initChannel(NioSocketChannel ch) throws Exception {//LoggingHandler一般用于调试使用,会将channel运行流程、状态显示出来ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));ch.pipeline().addLast(new StringEncoder());}}).connect(new InetSocketAddress("localhost", 8080));System.out.println(channelFuture.getClass());Channel channel = channelFuture.sync().channel();log.debug("{}", channel);new Thread(()->{Scanner scanner = new Scanner(System.in);//不断拿到用户输入的信息while (true) {String line = scanner.nextLine();if ("q".equals(line)) {channel.close(); // 调用close()方法是交给其他线程执行异步操作
// log.debug("处理关闭之后的操作"); // 不能在这里善后,因为是交给其他线程执行可能出现1s之后执行close()的情况break;}//输入的不是q则发送给服务器channel.writeAndFlush(line);}}, "input").start();// 获取 CloseFuture 对象, 1) 同步处理关闭, 2) 异步处理关闭ChannelFuture closeFuture = channel.closeFuture();//1) 同步处理关闭
// log.debug("waiting close...");
// closeFuture.sync();//只有在调用了channel.close()后才会继续向下运行
// log.debug("处理关闭之后的操作");//2) 异步处理关闭System.out.println("closeFuture.getClass():"+closeFuture.getClass());// channelFuture.addListener(new ChannelFutureListener() {
// //关闭channel的线程调用operationComplete方法
// //也就是nio线程执行完close(),找到该回调对象调用该方法
// @Override
// public void operationComplete(ChannelFuture future) throws Exception {
// log.debug("处理关闭之后的操作");
// }
// });//以上代码会出现用户输入q时,程序没有结束的情况。// 原因是NioEventLoopGroup中还有一些线程在运行,需要进行关闭channelFuture.addListener(new ChannelFutureListener() {@Overridepublic void operationComplete(ChannelFuture future) throws Exception {log.debug("处理关闭之后的操作");group.shutdownGracefully();}});//或使用以下代码替换
// closeFuture.addListener((ChannelFutureListener) future -> {
// log.debug("处理关闭之后的操作");
// group.shutdownGracefully();
// });}
}
💡 netty异步提升的是什么
- 有些同学看到这里会有疑问:为什么不在一个线程中去执行建立连接、去执行关闭 channel,那样不是也可以吗?非要用这么复杂的异步方式:比如一个线程发起建立连接,另一个线程去真正建立连接。
- 还有同学会笼统地回答,因为 netty 异步方式用了多线程、多线程就效率高。其实这些认识都比较片面,多线程和异步所提升的效率并不是所认为的。(其实提高的是单位时间内处理请求的吞吐量)
思考:
思考下面的场景,4 个医生给人看病,每个病人花费 20 分钟,而且医生看病的过程中是以病人为单位的,一个病人看完了,才能看下一个病人。假设病人源源不断地来,可以计算一下 4 个医生一天工作 8 小时,处理的病人总数是:4 * 8 * 3 = 96
经研究发现,看病可以细分为四个步骤,经拆分后每个步骤需要 5 分钟,如下
因此可以做如下优化,只有一开始,医生 2、3、4 分别要等待 5、10、15 分钟才能执行工作,但只要后续病人源源不断地来,他们就能够满负荷工作,并且处理病人的能力提高到了 4 * 8 * 12 效率几乎是原来的四倍。
要点
- 单线程没法异步提高效率,必须配合多线程、多核 cpu 才能发挥异步的优势
- 异步并没有缩短响应时间,反而有所增加
- 合理进行任务拆分,也是利用异步的关键
总结
异步并没有缩短处理单位的时间,反而有所增加请求的处理和响应时间。
关键点是提高了单位时间内处理请求的(个数)吞吐量,单位时间内能过处理请求的速度
。
全部文章:
Netty笔记01-Netty的基本概念与用法
Netty笔记02-组件EventLoop
Netty笔记03-组件Channel
Netty笔记04-组件Future & Promise
Netty笔记05-组件Handler & Pipeline
相关文章:

Netty笔记03-组件Channel
文章目录 Channel概述Channel 的概念Channel 的主要功能Channel 的生命周期Channel 的状态Channel 的类型channel 的主要方法 ChannelFutureCloseFuture💡 netty异步提升的是什么要点总结 Channel概述 Channel 的概念 在 Netty 中,Channel 是一个非常重…...

1----安卓机型修复串码 开启端口 檫除基带 支持高通与MTK机型工具预览与操作解析
在玩机过程中。很多玩家会碰到各种各样的故障 。其中最多的就在于基带 串码类。由于目前的安卓机型必须修改或者写入串码等参数必须开启端口。而一些初级玩友不太了解开启参数端口的步骤。这个工具很简单的为安卓机型开启端口。并且操作相对简单。 此工具基本功能 1-----可以…...

Docker容器技术1——docker基本操作
Docker容器技术 随着云计算和微服务架构的普及,容器技术成为了软件开发、测试和部署过程中的重要组成部分。其中,Docker作为容器技术的代表之一,以其简便易用的特点赢得了广大开发者的青睐。 Docker允许开发者在轻量级、可移植的容器中打包和…...
ElasticSearch介绍+使用
ElasticSearch 1.背景 ElasticSearch的最明显的优势在于其分布式特性,能够扩展到上百台服务器,极大地提高了服务器的容错率。在大数据时代背景下,ElasticSearch与传统的数据库相比较,能够应对大规模的并发搜索请求,同…...

Redis——常用数据类型List
目录 List列表常用命令lpushlpushxrpushrpushlrangelpoprpoplindexlinsertllenlremltrim key start stoplset 阻塞版本命令blpopbrpop list的编码方式list的应用 List列表 Redis中的list相当于数组,或者 顺序表,一些常用的操作可以通过下面这张图来理解…...

前端基础知识+算法(一)
文章目录 算法二分查找条件注意方式基本原理左闭右闭正向写法 左闭右开正向写法 前端基础知识定时器及清除盒子垂直水平居中的方式垂直水平1.flex布局2.grid布局3.定位对于块级元素 解决高度塌陷的方式1.给父元素一个固定的高度2.给父元素添加属性 overflow: hidden;3.在子元素…...

photozoom classic 9解锁码2024年最新25位解锁码
photozoom classic 9 破解版顾及比恐龙还要稀有,我曾经和你一样一直再找,找了好几个月,也没有找到真的破解版,下载很多次, 都是病毒插件之类的 我昨天下了几次,没有一个不附带插件病毒木马的.......&#x…...

Oracle发邮件功能:设置的步骤与注意事项?
Oracle发邮件配置教程?如何实现Oracle发邮件功能? Oracle数据库作为企业级应用的核心,提供了内置的发邮件功能,使得数据库管理员和开发人员能够通过数据库直接发送邮件。AokSend将详细介绍如何设置Oracle发邮件功能。 Oracle发邮…...
优化理论及应用精解【9】
文章目录 二次型函数二次型函数详细解释一、定义二、性质三、应用四、示例五、图表辅助说明(由于文本限制,无法直接提供图表) “西尔维斯特准则”一、定义二、来源三、应用场景 参考文献 二次型函数 二次型函数详细解释 一、定义 二次型函…...

nginx实现https安全访问的详细配置过程
文章目录 前言什么是 HTTP?什么是 HTTPS?HTTP 和 HTTPS 的区别为什么 HTTPS 被称为安全的?配置过程配置自签名证书 前言 首先我们来简单了解一下什么是http和https以及他们的区别所在. 什么是 HTTP? HTTP,全称为“超…...
1. TypeScript基本语法
TypeScript 学习总结 TypeScript 是一种 JavaScript 的超集,增加了静态类型检查和编译时错误检测,从而提高了代码的可维护性和可靠性。以下是 TypeScript 的基础知识总结,包括语法、运算符、数据类型、变量声明和作用域。 ## 基本语法TypeS…...
C# UDP与TCP点发【速发速断】模式
1、UDP 客户端 //由于收发都在本机,所以只用一个IP地址 IPAddress addr IPAddress.Parse("127.0.0.1"); var ptLocal new IPEndPoint(addr,9001);//本机节点,用于发送var ptDst new IPEndPoint(addr,9002);//目标节点…...

pikachu下
CSRF(跨站请求伪造) CSRF(get) url变成了这样了,我们就可以新开个页面直接拿url去修改密码 http://pikachu-master/vul/csrf/csrfget/csrf_get_login.php?username1&password2&submitLogin CSRF(post) 这里只是请求的方式不同,…...

Go语言开发im-websocket服务和vue3+ts开发类似微信pc即时通讯
前言 IM即时通讯聊天, 为软件开发者打造,不依赖第三方sdk,完全用Go语言开发即时通讯服务,支持H5、Electron、Wails 、Uniapp和各种小程序的IM即时通讯, 快速实现私聊、群聊、在线客服!让你快速搭建一个微信聊天系统,打…...

Redis如何实现分布式锁
目录 获取锁: 释放锁: Lua脚本: Redisson 分布式锁是,满足分布式系统或集群模式下多进程可见并且互斥的锁,因为我们熟知的java中的锁只是在单体架构下单个jvm中才会生效,如果部署了多个jvm则会导致新的…...

面向对象程序设计之继承(C++)
1.继承的定义 1.1继承的概念 继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。继承 呈现了⾯向…...

IAPP发布《2024年人工智能治理实践报告》
文章目录 前言一、黑箱问题►透明度、可理解性与可解释性二、法律和政策中的注意事项►欧盟的《通用数据保护条例》►欧盟的AI法案►NIST的AI风险管理框架►美国的第14110号行政命令►《生成式人工智能服务管理暂行办法》►新加坡的AI验证三、实施人工智能治理►模型卡与系统卡…...

了解MySQL 高可用架构:主从备份
为了防止数据库的突然挂机,我们需要对数据库进行高可用架构。主从备份是常见的场景,通常情况下都是“一主一从/(多从)”。正常情况下,都是主机进行工作,从机进行备份主机数据,如果主机某天突然意外宕机,从机…...

[OpenCV] 数字图像处理 C++ 学习——15像素重映射(cv::remap) 附完整代码
文章目录 前言1.像素重映射理论基础2.代码实现(1) remap()细节(2)水平翻转(2)垂直翻转(3)旋转 180 度(4)径向扭曲 3.完整代码 前言 像素重映射将图像中的每个像素映射到新位置,实现图像的扭曲、校正等操作。在 OpenCV 中,cv::remap() 函数就是用于实现这…...
Oreace每日运维操作
一.Oreace每日运维操作 目录 一.Oreace每日运维操作 1.1、确认所有的INSTANCE状态正常 1.2、检查文件系统的使用(剩余空间) 1.3 lwh暗码,,、检查日志文件和trace文件记录 1.4 lwh、检查数据库当日备份…...

8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...
安卓基础(aar)
重新设置java21的环境,临时设置 $env:JAVA_HOME "D:\Android Studio\jbr" 查看当前环境变量 JAVA_HOME 的值 echo $env:JAVA_HOME 构建ARR文件 ./gradlew :private-lib:assembleRelease 目录是这样的: MyApp/ ├── app/ …...

佰力博科技与您探讨热释电测量的几种方法
热释电的测量主要涉及热释电系数的测定,这是表征热释电材料性能的重要参数。热释电系数的测量方法主要包括静态法、动态法和积分电荷法。其中,积分电荷法最为常用,其原理是通过测量在电容器上积累的热释电电荷,从而确定热释电系数…...

C/C++ 中附加包含目录、附加库目录与附加依赖项详解
在 C/C 编程的编译和链接过程中,附加包含目录、附加库目录和附加依赖项是三个至关重要的设置,它们相互配合,确保程序能够正确引用外部资源并顺利构建。虽然在学习过程中,这些概念容易让人混淆,但深入理解它们的作用和联…...

力扣热题100 k个一组反转链表题解
题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...

接口自动化测试:HttpRunner基础
相关文档 HttpRunner V3.x中文文档 HttpRunner 用户指南 使用HttpRunner 3.x实现接口自动化测试 HttpRunner介绍 HttpRunner 是一个开源的 API 测试工具,支持 HTTP(S)/HTTP2/WebSocket/RPC 等网络协议,涵盖接口测试、性能测试、数字体验监测等测试类型…...
NPOI操作EXCEL文件 ——CAD C# 二次开发
缺点:dll.版本容易加载错误。CAD加载插件时,没有加载所有类库。插件运行过程中用到某个类库,会从CAD的安装目录找,找不到就报错了。 【方案2】让CAD在加载过程中把类库加载到内存 【方案3】是发现缺少了哪个库,就用插件程序加载进…...

从零开始了解数据采集(二十八)——制造业数字孪生
近年来,我国的工业领域正经历一场前所未有的数字化变革,从“双碳目标”到工业互联网平台的推广,国家政策和市场需求共同推动了制造业的升级。在这场变革中,数字孪生技术成为备受关注的关键工具,它不仅让企业“看见”设…...

【51单片机】4. 模块化编程与LCD1602Debug
1. 什么是模块化编程 传统编程会将所有函数放在main.c中,如果使用的模块多,一个文件内会有很多代码,不利于组织和管理 模块化编程则是将各个模块的代码放在不同的.c文件里,在.h文件里提供外部可调用函数声明,其他.c文…...
JavaScript 标签加载
目录 JavaScript 标签加载script 标签的 async 和 defer 属性,分别代表什么,有什么区别1. 普通 script 标签2. async 属性3. defer 属性4. type"module"5. 各种加载方式的对比6. 使用建议 JavaScript 标签加载 script 标签的 async 和 defer …...