当前位置: 首页 > news >正文

Netty系列-7 Netty编解码器

背景

netty框架中,自定义解码器的起点是ByteBuf类型的消息, 自定义编码器的终点是ByteBuf类型。

1.解码器

业务解码器的起点是ByteBuf类型

netty中可以通过继承MessageToMessageEncoder类自定义解码器类。MessageToMessageEncoder继承自ChannelInboundHandlerAdapter,ChannelInboundHandlerAdapter使用默认方式(不处理,向下传递事件)实现了所有的Inbound接口。因此,MessageToMessageEncoder只需要重写channelRead方法,并在该方法中提取消息、转换消息、通过ChannelInvoker将转换后的消息以channelRead事件发向pipeline即可。
MessageToMessageEncoder抽象类的实现如下:

public abstract class MessageToMessageDecoder<I> extends ChannelInboundHandlerAdapter {private final TypeParameterMatcher matcher;protected MessageToMessageDecoder() {matcher = TypeParameterMatcher.find(this, MessageToMessageDecoder.class, "I");}protected MessageToMessageDecoder(Class<? extends I> inboundMessageType) {matcher = TypeParameterMatcher.get(inboundMessageType);}public boolean acceptInboundMessage(Object msg) throws Exception {return matcher.match(msg);}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {CodecOutputList out = CodecOutputList.newInstance();try {if (acceptInboundMessage(msg)) {I cast = (I) msg;try {decode(ctx, cast, out);} finally {ReferenceCountUtil.release(cast);}} else {out.add(msg);}} catch (DecoderException e) {throw e;} catch (Exception e) {throw new DecoderException(e);} finally {try {int size = out.size();for (int i = 0; i < size; i++) {ctx.fireChannelRead(out.getUnsafe(i));}} finally {out.recycle();}}}protected abstract void decode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;
}

1.1 类型的匹配器

MessageToMessageDecoder内部维护了一个TypeParameterMatcher类型的匹配器对象matcher,用于指定解码器可以处理的消息类型。可通过构造函数为其设置类型,也可通过泛型指定:

// 使用泛型类型
protected MessageToMessageDecoder() {matcher = TypeParameterMatcher.find(this, MessageToMessageDecoder.class, "I");
}// 子类调用MessageToMessageDecoder构造器时,传入类型
protected MessageToMessageDecoder(Class<? extends I> inboundMessageType) {matcher = TypeParameterMatcher.get(inboundMessageType);
}

一般,通过泛型指定解码器处理的消息对象,即使用MessageToMessageDecoder的无参构造函数。
acceptInboundMessage方法封装matcher的实现,返回布尔值,表示是否支持处理msg消息类型:

public boolean acceptInboundMessage(Object msg) throws Exception {return matcher.match(msg);
}

根据matcher的match方法:

private static final class ReflectiveMatcher extends TypeParameterMatcher {private final Class<?> type;//...@Overridepublic boolean match(Object msg) {// msg消息是否为type类型或者其子类型return type.isInstance(msg);}
}

1.2 channelRead方法

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {// 构造List列表对象,存储解码后的对象CodecOutputList out = CodecOutputList.newInstance();try {// 判断是否支持处理消息if (acceptInboundMessage(msg)) {I cast = (I) msg;try {// 处理消息,将cast对象解码后的结果存放到out列表中decode(ctx, cast, out);} finally {ReferenceCountUtil.release(cast);}} else {// 不处理消息,以原样保存out.add(msg);}} catch (DecoderException e) {throw e;} catch (Exception e) {throw new DecoderException(e);} finally {try {int size = out.size();// 遍历列表,依次向pipeline触发解码后的对象for (int i = 0; i < size; i++) {ctx.fireChannelRead(out.getUnsafe(i));}} finally {out.recycle();}}
}

逻辑较为清晰:
[1] 构造列表对象out,用于临时存放解码后的消息;
[2] 判断当前解码器是否可以处理该消息,不可以处理,直接添加到out中;可以处理,调用decode方法解码消息,解码结果都添加到out中;
[3] 遍历out列表,将消息以ChannelRead事件传递给向pipeline;
[4] out清理、回收再利用;

1.3 decode方法

decode方法是实际进行消息转换的逻辑,由子类根据业务具体实现:

protected abstract void decode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;

将msg解码,解码后的对象存放在out中;由于out是数组,因此可以从msg中解码出一个对象,也可以解码出多个。如下所示:

protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) {out.add(msg.toString(charset));
}

将ByteBuf类型的msg消息转为一个String类型的对象;

protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) {String[] decodedMsgs = msg.toString(charset).split(";");for (String decodedMsg: decodedMsgs) {out.add(decodedMsg);}
}

将ByteBuf转为String,并按照;分隔符进行拆分,每个字符串作为一个消息对象。

2.解码器案例

案例的结构图如下所示,消息流入解码器和流出时的消息类型会发生变化:
在这里插入图片描述
引入三个解码器和一个业务Handler:
[1] 编码器1实现ByteBuf->String类型的转换;
[2] 编码器2实现String->Message1类型的转换;
[3] 编码器3实现Message1->Message2类型的转换;
[4] 业务Handler打印消息类型和消息;
实现类依次为:

// MyMessageDecoder1
public class MyMessageDecoder1 extends MessageToMessageDecoder<ByteBuf> {@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) {out.add(msg.toString(Charset.defaultCharset()));}
}// MyMessageDecoder2
class MyMessageDecoder2 extends MessageToMessageDecoder<String> {@Overrideprotected void decode(ChannelHandlerContext ctx, String msg, List<Object> out) {String[] decodedMsgs = msg.split(";");for (String decodedMsg : decodedMsgs) {out.add(new Message1(decodedMsg));}}
}// MyMessageDecoder3
class MyMessageDecoder3 extends MessageToMessageDecoder<Message1> {@Overrideprotected void decode(ChannelHandlerContext ctx, Message1 msg, List<Object> out) {out.add(new Message2(msg.getContent()));}
}

业务Handler定义如下:

private static class MyHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) {System.out.println(msg);}
}

Message1和Message2消息定义如下:

@Data
@RequiredArgsConstructor
pulic class Message1 {private final String content;
}@Data
@RequiredArgsConstructor
pulic class Message2 {private final String content;
}

客户端发送消息:"test1;test2;test3"时:

Microsoft Telnet> send test1;test2;test3
发送字符串 test1;test2;test3
Microsoft Telnet>

服务器日志如下所示:

Message2(content=test1)
Message2(content=test2)
Message2(content=test3)

注意:解码的顺序沿着pipeline进行,因此需要注意调整netty解码器在pipeline中的位置。

如果将3和解码器2的顺序调整一下:

protected void initChannel(NioSocketChannel channel) {channel.pipeline().addLast(new MyMessageDecoder1());channel.pipeline().addLast(new MyMessageDecoder3());channel.pipeline().addLast(new MyMessageDecoder2());channel.pipeline().addLast(new MyHandler());
}

重复上述操作,服务器日志如下:

Message1(content=test1)
Message1(content=test2)
Message1(content=test3)

此时,解码器1流出的数据为String类型,流入解码器2时-类型校验不通过直接以流入的String类型流出,流入解码器3时,将String类型转为Message1类型,流入业务Handler进行打印。

3.编码器

业务编码器的终点是ByteBuf类型

netty中可以通过继承MessageToMessageEncoder类自定义解码器类。MessageToMessageEncoder继承自ChannelOutboundHandlerAdapter,ChannelOutboundHandlerAdapter使用默认方式实现(不处理,向前传递事件)了所有的Outbound接口。因此,MessageToMessageEncoder只需要重写write方法,并在该方法中编码消息、并通过ChannelInvoker将编码后的消息发送到pipeline即可。
MessageToMessageEncoder抽象类的实现如下:

public abstract class MessageToMessageEncoder<I> extends ChannelOutboundHandlerAdapter {private final TypeParameterMatcher matcher;protected MessageToMessageEncoder() {matcher = TypeParameterMatcher.find(this, MessageToMessageEncoder.class, "I");}protected MessageToMessageEncoder(Class<? extends I> outboundMessageType) {matcher = TypeParameterMatcher.get(outboundMessageType);}public boolean acceptOutboundMessage(Object msg) throws Exception {return matcher.match(msg);}@Overridepublic void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {CodecOutputList out = null;try {if (acceptOutboundMessage(msg)) {out = CodecOutputList.newInstance();@SuppressWarnings("unchecked")I cast = (I) msg;try {encode(ctx, cast, out);} finally {ReferenceCountUtil.release(cast);}if (out.isEmpty()) {throw new EncoderException(StringUtil.simpleClassName(this) + " must produce at least one message.");}} else {ctx.write(msg, promise);}} catch (EncoderException e) {throw e;} catch (Throwable t) {throw new EncoderException(t);} finally {if (out != null) {try {final int sizeMinusOne = out.size() - 1;if (sizeMinusOne == 0) {ctx.write(out.getUnsafe(0), promise);} else if (sizeMinusOne > 0) {if (promise == ctx.voidPromise()) {writeVoidPromise(ctx, out);} else {writePromiseCombiner(ctx, out, promise);}}} finally {out.recycle();}}}}private static void writeVoidPromise(ChannelHandlerContext ctx, CodecOutputList out) {final ChannelPromise voidPromise = ctx.voidPromise();for (int i = 0; i < out.size(); i++) {ctx.write(out.getUnsafe(i), voidPromise);}}private static void writePromiseCombiner(ChannelHandlerContext ctx, CodecOutputList out, ChannelPromise promise) {final PromiseCombiner combiner = new PromiseCombiner(ctx.executor());for (int i = 0; i < out.size(); i++) {combiner.add(ctx.write(out.getUnsafe(i)));}combiner.finish(promise);}protected abstract void encode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;
}

3.1 类型的匹配器

MessageToMessageEncoder内部维护了一个TypeParameterMatcher类型的匹配器对象matcher,用于指定该编码器器可以处理的消息类型,与解码器中的matcher作用完全相同,不再赘述。

3.2 write方法

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {CodecOutputList out = null;try {// 判断当前编码器是否可以编码消息if (acceptOutboundMessage(msg)) {out = CodecOutputList.newInstance();@SuppressWarnings("unchecked")I cast = (I) msg;try {// 编码消息,并将编码后的消息保存到out列表中encode(ctx, cast, out);} finally {ReferenceCountUtil.release(cast);}if (out.isEmpty()) {throw new EncoderException(StringUtil.simpleClassName(this) + " must produce at least one message.");}} else {// 不能编码的消息不处理,直接沿着pipeline向前传递ctx.write(msg, promise);}} catch (EncoderException e) {throw e;} catch (Throwable t) {throw new EncoderException(t);} finally {// 遍历out,依次调用ctx.write,沿着pipeline向前传递if (out != null) {try {final int sizeMinusOne = out.size() - 1;if (sizeMinusOne == 0) {ctx.write(out.getUnsafe(0), promise);} else if (sizeMinusOne > 0) {if (promise == ctx.voidPromise()) {writeVoidPromise(ctx, out);} else {writePromiseCombiner(ctx, out, promise);}}} finally {// 清理out列表,回收再利用out.recycle();}}}
}

逻辑较为清晰:
[1] 构造列表对象out,用于临时存放编码后的消息;
[2] 判断当前编码器是否可以处理该消息,不可以处理,直接通过ctx.write沿着pipeline向前传递;可以处理,调用encode方法编码消息,编码结果添加到out中;
[3] 遍历out列表,将消息以write事件传递给向pipeline;
[4] out清理、回收再利用;

3.2 encode方法

encode方法是实际进行消息转换的逻辑,由子类根据业务具体实现:

protected abstract void encode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;

将msg消息进行编码,编码后的对象存放在out中;由于out是数组,因此可以从msg中编码出一个对象,也可以编码出多个,与解码器逻辑相同。

protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) {out.add(msg.toString(charset));
}

将ByteBuf类型的msg消息转为一个String类型的对象;

protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) {String[] decodedMsgs = msg.toString(charset).split(";");for (String decodedMsg: decodedMsgs) {out.add(decodedMsg);}
}

将ByteBuf转为String,并按照;分隔符进行拆分,每个字符串作为一个消息对象。
netty向外发送数据时,一般经过业务Handler->编码器->HeadContext的流程。
向客户端发送消息的底层实现在HeadContext的unsafe对象(NioSocketChannel的unsafe对象)中,而发送前有消息类型判断:

final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler{ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {unsafe.write(msg, promise);}
}

unsafe对象的write方法如下:

public final void write(Object msg, ChannelPromise promise) {//...msg = filterOutboundMessage(msg);//...
}

在真实写操作前,通过filterOutboundMessage进行消息类型的判断:

@Override
protected final Object filterOutboundMessage(Object msg) {// 要求消息必须时ByteBuf或者FileRegion类型或其子类型if (msg instanceof ByteBuf) {ByteBuf buf = (ByteBuf) msg;if (buf.isDirect()) {return msg;}return newDirectBuffer(buf);}if (msg instanceof FileRegion) {return msg;}throw new UnsupportedOperationException("unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES);
}

由此,编码器将消息传递给HeadContext前,需要将消息最终编码为ByteBuf类型。

4.解码器案例

案例结构图如下所示:
在这里插入图片描述

在章节2中的案例基础上新增两个编码器,并修改业务Handler:
[1] 业务Handler,接收客户端消息后,响应相同消息;
[2] 编码器1:将Message2类型的消息转为String类型;
[3] 编码器2: 将String类型消息转为ByteBuf类型;
代码实现如下:
修改业务Handler:

private static class MyHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) {System.out.println(msg);// 新增逻辑“将消息对象发送给客户端ctx.write(msg);}
}

添加编码器:

// 将Message2消息转为String消息
public class MyEncoder1 extends MessageToMessageEncoder<Message2> {@Overrideprotected void encode(ChannelHandlerContext ctx, Message2 msg, List<Object> out) throws Exception {out.add(msg.getContent());}
}// 将String消息转为ByteBuf消息
public class MyEncoder2 extends MessageToMessageEncoder<String> {@Overrideprotected void encode(ChannelHandlerContext ctx, String msg, List<Object> out) {out.add(ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.wrap(msg), Charset.defaultCharset()));}
}

在MyHandler前依次添加解码器MyEncoder2和MyEncoder1:

protected void initChannel(NioSocketChannel channel) {channel.pipeline().addLast(new MyMessageDecoder1());channel.pipeline().addLast(new MyMessageDecoder2());channel.pipeline().addLast(new MyMessageDecoder3());channel.pipeline().addLast(new MyEncoder2());channel.pipeline().addLast(new MyEncoder1());channel.pipeline().addLast(new MyHandler());
}

可以使用Netty写一个客户端, 也可用客户端工具模拟,这里为了方便,使用SocketTool.exe,控制台日志如下:

14:36:15 发送数据:test1;test2;test3[1次]
14:36:15 收到数据:test1test2test3

注意:客户端收到了test1test2test3消息,在客户端开来是一个消息,但在服务器看来是连续发送的3个消息,消息内容分别为test1和test2和test3。这是TCP的流传输模式导致,可在业务层添加额外处理解决这个问题。将在下一篇文件介绍Netty如何处理粘包和分包问题。

相关文章:

Netty系列-7 Netty编解码器

背景 netty框架中&#xff0c;自定义解码器的起点是ByteBuf类型的消息, 自定义编码器的终点是ByteBuf类型。 1.解码器 业务解码器的起点是ByteBuf类型 netty中可以通过继承MessageToMessageEncoder类自定义解码器类。MessageToMessageEncoder继承自ChannelInboundHandlerAdap…...

OpenHarmony标准系统上实现对rk系列芯片NPU的支持(npu使用)

在上篇文章中&#xff0c;我们学习了移植rk的npu驱动到OpenHarmony提供的内核。本文我们来学习如何在OpenHarmony标准系统rk系列芯片如何使用npu OpenHarmony RK系列芯片运行npu测试用例 在移植npu驱动到OpenHarmony之后&#xff0c;来运行npu样例进行简单测试 1.O 测试准备…...

大表性能优化的关键技术

1 引言 在现代企业应用中,随着数据量的不断增长,大表的性能优化成为数据库管理的重要环节。本文将探讨大表性能优化的关键技术,包括索引优化、查询优化、分区分表、读写分离以及缓存策略等方面。通过综合运用这些技术,可以显著提升大表的处理效率和响应速度,确保系统的稳…...

广联达 Linkworks办公OA Service.asmx接口存在信息泄露漏洞

漏洞描述 广联达科技股份有限公司以建设工程领域专业应用为核心基础支撑&#xff0c;提供一百余款基于“端云大数据”产品/服务&#xff0c;提供产业大数据、产业新金融等增值服务的数字建筑平台服务商。广联达OA存在信息泄露漏洞&#xff0c;由于某些接口没有鉴权&#xff0c…...

如何成为成功的AI产品经理:经验与策略分享

引言 随着人工智能(AI)技术的迅猛发展,AI产品经理(AI PM)的角色变得越来越重要。Google AI产品负责人Marily Nika在最近的一次播客中分享了她在AI产品管理领域的宝贵经验和见解。本文将整理并总结她的核心内容,帮助有志于进入AI PM领域的人士了解如何准备、所需的核心技…...

spring loCDI 详解

文章目录 一、IoC & DI 基本知识1.1 IoC 的基本概念&#xff1a;1.2 IoC 的优势&#xff1a;1.3 DI 介绍&#xff1a; 二、IoC 详解2.1 Spring 容器&#xff1a;2.2 被存储 Bean 的命名约定&#xff1a;2.3 Bean 的存储方式&#xff1a;2.3.1 五大类注解&#xff1a;2.3.1.…...

遇到 Docker 镜像拉取失败的问题时该如何解决

遇到 Docker 镜像拉取失败的问题时&#xff0c;可以按照以下步骤进行排查和解决&#xff1a; 1. 检查网络连接 确保你的计算机可以访问互联网。尝试 ping 通 Docker Hub 或其他镜像仓库的域名&#xff1a; ping hub.docker.com2. 检查 Docker 服务状态 确保 Docker 服务正在…...

【C/C++】错题记录(三)

题目一 题目二 题目三 题目四 题目五 题目六 题目七&#xff1f;&#xff1f;&#xff1f; 题目八 这道题主要考查对数据类型和位运算的理解与运用。 分析选项 A&#xff1a; *((unsigned char *)(&number) 1)0xcd; 这里将 number 的地址强制转换为 unsigned char* 类型&a…...

深入理解Web浏览器与服务器的连接过程

目录 1. 域名解析&#xff1a;找到地址 2. TCP连接&#xff1a;建立通信 3. HTTP请求&#xff1a;点菜 4. 服务器处理请求&#xff1a;厨房做菜 5. HTTP响应&#xff1a;上菜 6. 客户端接收响应&#xff1a;品尝美食 7. 关闭TCP连接&#xff1a;吃完离开 8. 持久连接&a…...

深入解析 https

我的主页&#xff1a;2的n次方_ 1. 背景介绍 在使用 http 协议的时候是不安全的&#xff0c;可能会出现运营商劫持等安全问题&#xff0c;运营商通过劫持 http 流量&#xff0c;篡改返回的网页内容&#xff0c;例如广告业务&#xff0c;可能会通过 Referer 字段 来统计是…...

NP-hard问题

一、前置知识 1.多项式 多项式是由变量&#xff08;如x、y等&#xff09;和系数通过有限次的加、减、乘运算得到的表达式。例如3x^22x 1就是一个关于(x)的多项式 2.时间复杂度 时间复杂度是用来衡量算法运行效率的一个指标。它描述了算法运行时间随着输入规模增长而增长的量…...

【Nacos架构 原理】内核设计之Nacos通信通道

文章目录 Nacos通信通道 &#xff08;长链接&#xff09;现状背景场景分析配置服务 长链接核心诉求功能性诉求负载均衡连接生命周期 Nacos通信通道 &#xff08;长链接&#xff09; 现状背景 Nacos 1.X 版本 Config/Naming 模块各自的推送通道都是按照自己的设计模型来实现的…...

【单片机】单片机map表详细解析

1、RO Size、RW Size、ROM Size分别是什么 首先将map文件翻到最下面&#xff0c;可以看到 1.1 RO Size&#xff1a;只读段 Code&#xff1a;程序的代码部分&#xff08;也就是 .text 段&#xff09;&#xff0c;它存放了程序的指令和可执行代码。 RO Data&#xff1a;只读…...

考研笔记之操作系统(三)- 存储管理

操作系统&#xff08;三&#xff09;- 存储管理 1. 内存的基础知识1.1 存储单元与内存地址1.2 按字节编址和按字编址1.3 指令1.4 物理地址和逻辑地址1.5 从写程序到程序运行1.6 链接1.6.1 静态链接1.6.2 装入时动态链接1.6.3 运行时动态链接 1.7 装入1.7.1 概念1.7.2 绝对装入1…...

vim/vi常用命令大全

启动和退出Vim 命令/操作作用vim启动Vimvim filename直接打开指定的文件命令模式下&#xff0c;输入 :q退出&#xff0c;q!强制退出:wq保存并退出:wq!保存并强制退出vim中按下a进入编辑模式Esc退出编辑模式进入命令模式new创建新窗口close关闭窗口 光标移动 命令/操作作用h、…...

什么是大语言模型,一句话解释

定义 先说语言模型&#xff08;Language Model&#xff09;旨在建模词汇序列的生成概率&#xff0c;提升机器的语言智能水平&#xff0c;使机 器能够模拟人类说话、写作的模式进行自动文本输出。 白话&#xff1a;语言模式是一种解决机器与人类交流的手段&#xff0c;机器人与…...

【数据库】 MongoDB 撤销用户的角色和权限

在 MongoDB 中&#xff0c;撤销用户的角色和权限是一项重要的管理任务&#xff0c;确保用户仅能访问和操作他们需要的数据。以下是如何撤销用户的角色和权限的详细步骤。 1. 使用 MongoDB Shell 撤销角色 1.1 修改用户角色 要撤销用户的角色&#xff0c;可以使用 updateUser…...

vue2接入高德地图实现折线绘制、起始点标记和轨迹打点的完整功能(提供Gitee源码)

目录 一、申请密钥 二、安装element-ui 三、安装高德地图依赖 四、完整代码 五、运行截图 六、官方文档 七、Gitee源码 一、申请密钥 登录高德开放平台&#xff0c;点击我的应用&#xff0c;先添加新应用&#xff0c;然后再添加Key。 ​ 如图所示填写对应的信息&…...

【重学 MySQL】四十六、创建表的方式

【重学 MySQL】四十六、创建表的方式 使用CREATE TABLE语句创建表使用CREATE TABLE LIKE语句创建表使用CREATE TABLE AS SELECT语句创建表使用CREATE TABLE SELECT语句创建表并从另一个表中选取数据&#xff08;与CREATE TABLE AS SELECT类似&#xff09;使用CREATE TEMPORARY …...

WPS在表格中填写材料时,内容过多导致表格不换页,其余内容无法正常显示 以及 内容过多,导致表格换页——解决方法

一、现象 1&#xff0c;内容过多导致表格不换页&#xff0c;其余内容无法正常显示 2&#xff0c;内容过多&#xff0c;导致表格换页 二、解决方法 在表格内右击&#xff0c;选择表格属性 在菜单栏选择行&#xff0c;勾选允许跨页断行&#xff0c;点击确定即可 1&#xff0…...

[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?

&#x1f9e0; 智能合约中的数据是如何在区块链中保持一致的&#xff1f; 为什么所有区块链节点都能得出相同结果&#xff1f;合约调用这么复杂&#xff0c;状态真能保持一致吗&#xff1f;本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里&#xf…...

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地

借阿里云中企出海大会的东风&#xff0c;以**「云启出海&#xff0c;智联未来&#xff5c;打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办&#xff0c;现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...

IGP(Interior Gateway Protocol,内部网关协议)

IGP&#xff08;Interior Gateway Protocol&#xff0c;内部网关协议&#xff09; 是一种用于在一个自治系统&#xff08;AS&#xff09;内部传递路由信息的路由协议&#xff0c;主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...

linux 错误码总结

1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...

高等数学(下)题型笔记(八)空间解析几何与向量代数

目录 0 前言 1 向量的点乘 1.1 基本公式 1.2 例题 2 向量的叉乘 2.1 基础知识 2.2 例题 3 空间平面方程 3.1 基础知识 3.2 例题 4 空间直线方程 4.1 基础知识 4.2 例题 5 旋转曲面及其方程 5.1 基础知识 5.2 例题 6 空间曲面的法线与切平面 6.1 基础知识 6.2…...

【算法训练营Day07】字符串part1

文章目录 反转字符串反转字符串II替换数字 反转字符串 题目链接&#xff1a;344. 反转字符串 双指针法&#xff0c;两个指针的元素直接调转即可 class Solution {public void reverseString(char[] s) {int head 0;int end s.length - 1;while(head < end) {char temp …...

WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成

厌倦手动写WordPress文章&#xff1f;AI自动生成&#xff0c;效率提升10倍&#xff01; 支持多语言、自动配图、定时发布&#xff0c;让内容创作更轻松&#xff01; AI内容生成 → 不想每天写文章&#xff1f;AI一键生成高质量内容&#xff01;多语言支持 → 跨境电商必备&am…...

全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比

目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec&#xff1f; IPsec VPN 5.1 IPsec传输模式&#xff08;Transport Mode&#xff09; 5.2 IPsec隧道模式&#xff08;Tunne…...

laravel8+vue3.0+element-plus搭建方法

创建 laravel8 项目 composer create-project --prefer-dist laravel/laravel laravel8 8.* 安装 laravel/ui composer require laravel/ui 修改 package.json 文件 "devDependencies": {"vue/compiler-sfc": "^3.0.7","axios": …...

服务器--宝塔命令

一、宝塔面板安装命令 ⚠️ 必须使用 root 用户 或 sudo 权限执行&#xff01; sudo su - 1. CentOS 系统&#xff1a; yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系统…...