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

粘包与拆包

优质博文:IT-BLOG-CN

一、粘包出现的原因

服务端与客户端没有约定好要使用的数据结构。Socket Client实际是将数据包发送到一个缓存buffer中,通过buffer刷到数据链路层。因服务端接收数据包时,不能断定数据包1何时结束,就有可能出现数据包2的部分数据结合数据包1发送出去,导致服务器读取数据包1时包含了数据包2的数据。这种现象称为粘包。
在这里插入图片描述

二、案例展示

【1】服务端代码如下,具体注释说明

package com.server;import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;/*** Netty5服务端* @author zhengzx**/
public class ServerSocket {public static void main(String[] args) {//创建服务类ServerBootstrap serverBootstrap = new ServerBootstrap();//boss和workerNioEventLoopGroup boss = new NioEventLoopGroup();NioEventLoopGroup worker = new NioEventLoopGroup();try {//设置线程池serverBootstrap.group(boss,worker);//设置socket工厂,Channel 是对 Java 底层 Socket 连接的抽象serverBootstrap.channel(NioServerSocketChannel.class);//设置管道工厂serverBootstrap.childHandler(new ChannelInitializer<Channel>() {@Overrideprotected void initChannel(Channel ch) throws Exception {//设置后台转换器(二进制转换字符串)ch.pipeline().addLast(new StringDecoder());ch.pipeline().addLast(new StringEncoder());ch.pipeline().addLast(new ServerSocketHandler());}});//设置TCP参数serverBootstrap.option(ChannelOption.SO_BACKLOG, 2048);//连接缓冲池大小serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);//维持连接的活跃,清除死连接serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true);//关闭超时连接ChannelFuture future = serverBootstrap.bind(10010);//绑定端口System.out.println("服务端启动");//等待服务端关闭future.channel().closeFuture().sync();} catch (Exception e) {e.printStackTrace();} finally {//释放资源boss.shutdownGracefully();worker.shutdownGracefully();}}
}

【2】ServerSocketHandler处理类展示:

package com.server;import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;public class ServerSocketHandler extends SimpleChannelInboundHandler<String>{@Overrideprotected void messageReceived(ChannelHandlerContext ctx, String msg) throws Exception {System.out.println(msg);}}

【3】客户端发送请求代码展示:

package com.client;import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;public class Client {public static void main(String[] args) throws UnknownHostException, IOException {//创建连接Socket socket = new Socket("127.0.0.1", 10010);//循环发送请求for(int i=0;i<1000;i++){socket.getOutputStream().write("hello".getBytes());}    //关闭连接socket.close();}
}

【4】打印结果。(正常情况应为一行一个hello打印)

三、分包

数据包数据被分开一部分发送出去,服务端一次读取数据时可能读取到完整数据包的一部分,剩余部分被第二次读取。具体情况如下图展示:

四、解决办法

方案一:定义一个稳定的结构:

【1】包头+length+数据包: 客户端代码展示:包头用来防止socket攻击,length用来获取数据包的长度。

package com.server;import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;import org.omg.CORBA.PRIVATE_MEMBER;
import org.omg.CORBA.PUBLIC_MEMBER;/*** @category 通过长度+数据包的方式解决粘包分包问题* @author zhengzx**/
public class Client {//定义包头public static int BAO = 24323455;public static void main(String[] args) throws UnknownHostException, IOException {//创建连接Socket socket = new Socket("127.0.0.1", 10010);//客户端发送的消息String msg = "hello";//获取消息的字节码byte[] bytes = msg.getBytes();//初始化buffer的长度:4+4表示包头长度+存放数据长度的整数的长度ByteBuffer buffer = ByteBuffer.allocate(8+bytes.length);//将长度和数据存入buffer中buffer.putInt(BAO);buffer.putInt(bytes.length);buffer.put(bytes);//获取缓冲区中的数据byte[] array = buffer.array();//循环发送请求for(int i=0;i<1000;i++){socket.getOutputStream().write(array);}    //关闭连接socket.close();}
}

【2】服务端: 需要注意的是,添加了MyDecoder类,此类具体下面介绍

package com.server;import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;public class Server {public static void main(String[] args) {//服务类ServerBootstrap bootstrap = new ServerBootstrap();//boss线程监听端口,worker线程负责数据读写ExecutorService boss = Executors.newCachedThreadPool();ExecutorService worker = Executors.newCachedThreadPool();//设置niosocket工厂bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));//设置管道的工厂bootstrap.setPipelineFactory(new ChannelPipelineFactory() {@Overridepublic ChannelPipeline getPipeline() throws Exception {ChannelPipeline pipeline = Channels.pipeline();pipeline.addLast("decoder", new MyDecoder());pipeline.addLast("handler1", new HelloHandler());return pipeline;}});bootstrap.bind(new InetSocketAddress(10101));System.out.println("start!!!");}}

【3】MyDecode类: 需要继承FrameDecoder类。此类中用ChannelBuffer缓存没有读取的数据包,等接收到第二次发送的数据包时,会将此数据包与缓存的数据包进行拼接处理。当return一个String时,FarmedDecoder通过判断返回类型,调用相应的sendUpStream(event)向下传递数据。源码展示:

public static void fireMessageReceived(ChannelHandlerContext ctx, Object message, SocketAddress remoteAddress) {ctx.sendUpstream(new UpstreamMessageEvent(ctx.getChannel(), message, remoteAddress));}
}

当返回null时,会进行break,不处理数据包中的数据,源码展示:

while (cumulation.readable()) {int oldReaderIndex = cumulation.readerIndex();Object frame = decode(context, channel, cumulation);if (frame == null) {if (oldReaderIndex == cumulation.readerIndex()) {// Seems like more data is required.// Let us wait for the next notification.break;} else {// Previous data has been discarded.// Probably it is reading on.continue;}}
}

我们自己写的MyDecoder类,代码展示:(包含socket攻击的校验)

package com.server;import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;public class MyDecoder extends FrameDecoder{@Overrideprotected Object decode(ChannelHandlerContext arg0, Channel arg1, ChannelBuffer buffer) throws Exception {//buffer.readableBytes获取缓冲区中的数据 需要 大于基本长度if(buffer.readableBytes() > 4) {//防止socket攻击,当缓冲区数据大于2048时,清除数据。if(buffer.readableBytes() > 2048) {buffer.skipBytes(buffer.readableBytes());}//循环获取包头,确定数据包的开始位置while(true) {buffer.markReaderIndex();if(buffer.readInt() == Client.BAO) {break;}//只读取一个字节buffer.resetReaderIndex();buffer.readByte();if(buffer.readableBytes() < 4) {return null;}}//做标记buffer.markReaderIndex();//获取数据包的发送过来时的长度int readInt = buffer.readInt();//判断buffer中剩余的数据包长度是否大于单个数据包的长度(readInt)if(buffer.readableBytes() < readInt) {//返回到上次做标记的地方,因为此次数据读取的不是一个完整的数据包。buffer.resetReaderIndex();//缓存当前数据,等待剩下数据包到来return null;}//定义一个数据包的长度byte[] bt = new byte[readInt];//读取数据buffer.readBytes(bt);//往下传递对象return new String(bt);}//缓存当前数据包,等待第二次数据的到来return null;}
}

【4】服务端: 处理请求的handler

package com.server;import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;public class HelloHandler extends SimpleChannelHandler {private int count = 1;@Overridepublic void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {System.out.println(e.getMessage() + "  " +count);count++;}
}

【5】结果展示(按顺序打印):
结果展示

方案二: 在消息的尾部加一些特殊字符,那么在读取数据的时候,只要读到这个特殊字符,就认为已经可以截取一个完整的数据包了,这种情况在一定的业务情况下实用。

方案三:LengthFieldBasedFrameDecoderLengthFieldPrepender
LengthFieldBasedFrameDecoderLengthFieldPrepender需要配合起来使用,这两者一个是解码,一个是编码的关系。它们处理粘拆包的主要思想是在生成的数据包中添加一个长度字段,用于记录当前数据包的长度。LengthFieldBasedFrameDecoder会按照参数指定的包长度偏移量数据对接收到的数据进行解码,从而得到目标消息体数据;而LengthFieldPrepender则会在响应的数据前面添加指定的字节数据,这个字节数据中保存了当前消息体的整体字节数据长度。

关于 LengthFieldBasedFrameDecoder,这里需要对其构造函数参数进行介绍:

public LengthFieldBasedFrameDecoder(int maxFrameLength,  //指定了每个包所能传递的最大数据包大小;int lengthFieldOffset,  //指定了长度字段在字节码中的偏移量;int lengthFieldLength,  //指定了长度字段所占用的字节长度;int lengthAdjustment,  //对一些不仅包含有消息头和消息体的数据进行消息头的长度的调整,这样就可以只得到消息体的数据,这里的 lengthAdjustment 指定的就是消息头的长度;int initialBytesToStrip)   //对于长度字段在消息头中间的情况,可以通过 initialBytesToStrip 忽略掉消息头以及长度字段占用的字节。

我们以json序列化为例对LengthFieldBasedFrameDecoderLengthFieldPrepender的使用方式进行说明。如下是EchoServer的源码:

public class EchoServer {public void bind(int port) throws InterruptedException {EventLoopGroup bossGroup = new NioEventLoopGroup();EventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024).handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {// 这里将LengthFieldBasedFrameDecoder添加到pipeline的首位,因为其需要对接收到的数据// 进行长度字段解码,这里也会对数据进行粘包和拆包处理ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 2, 0, 2));// LengthFieldPrepender是一个编码器,主要是在响应字节数据前面添加字节长度字段ch.pipeline().addLast(new LengthFieldPrepender(2));// 对经过粘包和拆包处理之后的数据进行json反序列化,从而得到User对象ch.pipeline().addLast(new JsonDecoder());// 对响应数据进行编码,主要是将User对象序列化为jsonch.pipeline().addLast(new JsonEncoder());// 处理客户端的请求的数据,并且进行响应ch.pipeline().addLast(new EchoServerHandler());}});ChannelFuture future = bootstrap.bind(port).sync();future.channel().closeFuture().sync();} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}public static void main(String[] args) throws InterruptedException {new EchoServer().bind(8080);}
}

EchoServer主要是在pipeline中添加了两个编码器和两个解码一器,编码器主要是负责将响应的User对象序列化为json对象,然后在其字节数组前面添加一个长度字段的字节数组;解码一器主要是对接收到的数据进行长度字段的解码,然后将其反序列化为一个User对象。下面是JsonDecoder的源码:

public class JsonDecoder extends MessageToMessageDecoder<ByteBuf> {@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> out) throws Exception {byte[] bytes = new byte[buf.readableBytes()];buf.readBytes(bytes);User user = JSON.parseObject(new String(bytes, CharsetUtil.UTF_8), User.class);out.add(user);}
}

JsonDecoder首先从接收到的数据流中读取字节数组,然后将其反序列化为一个User对象。下面我们看看JsonEncoder的源码:

public class JsonEncoder extends MessageToByteEncoder<User> {@Overrideprotected void encode(ChannelHandlerContext ctx, User user, ByteBuf buf)throws Exception {String json = JSON.toJSONString(user);ctx.writeAndFlush(Unpooled.wrappedBuffer(json.getBytes()));}
}

JsonEncoder将响应得到的User对象转换为一个json对象,然后写入响应中。对于EchoServerHandler,其主要作用就是接收客户端数据,并且进行响应,如下是其源码:

public class EchoServerHandler extends SimpleChannelInboundHandler<User> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, User user) throws Exception {System.out.println("receive from client: " + user);ctx.write(user);}
}

对于客户端,其主要逻辑与服务端的基本类似,这里主要展示其pipeline的添加方式,以及最后发送请求,并且对服务器响应进行处理的过程:

@Override
protected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 2, 0, 2));ch.pipeline().addLast(new LengthFieldPrepender(2));ch.pipeline().addLast(new JsonDecoder());ch.pipeline().addLast(new JsonEncoder());ch.pipeline().addLast(new EchoClientHandler());
}

这里客户端首先会在连接上服务器时,往服务器发送一个User对象数据,然后在接收到服务器响应之后,会打印服务器响应的数据。

public class EchoClientHandler extends SimpleChannelInboundHandler<User> {@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {ctx.write(getUser());}private User getUser() {User user = new User();user.setAge(27);user.setName("zhangxufeng");return user;}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, User user) throws Exception {System.out.println("receive message from server: " + user);}
}

方案四:自定义粘包与拆包器:
对于一些更加复杂的协议,可能有一些定制化的需求。通过继承LengthFieldBasedFrameDecoderLengthFieldPrepender来实现粘包和拆包的处理。

如果用户确实需要不通过继承的方式实现自己的粘包和拆包处理器,这里可以通过实现MessageToByteEncoderByteToMessageDecoder来实现。这里MessageToByteEncoder的作用是将响应数据编码为一个ByteBuf对象,而ByteToMessageDecoder则是将接收到的ByteBuf数据转换为某个对象数据。通过实现这两个抽象类,用户就可以达到实现自定义粘包和拆包处理的目的。如下是这两个类及其抽象方法的声明:

public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
}public abstract class MessageToByteEncoder<I> extends ChannelOutboundHandlerAdapter {protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception;
}

相关文章:

粘包与拆包

优质博文&#xff1a;IT-BLOG-CN 一、粘包出现的原因 服务端与客户端没有约定好要使用的数据结构。Socket Client实际是将数据包发送到一个缓存buffer中&#xff0c;通过buffer刷到数据链路层。因服务端接收数据包时&#xff0c;不能断定数据包1何时结束&#xff0c;就有可能出…...

基于QGIS的研究区域遥感影像裁切下载方法-以岳麓区为例

目录 前言 一、数据说明 1、遥感影像 2、矢量范围 二、按矢量范围导出 1、第一步、导出影像 2、第二步、设置输出格式 3、设置裁切范围 4、设置分辨率 三、按矢量范围掩膜 1、第一步、打开裁剪工具 2、第二步、参数设置 ​编辑 3、执行掩膜 四、webgis支持 1、生成运行…...

YOLOv8-Openvino-ByteTrack【CPU】

纯检测如下&#xff1a; YOLOv5-Openvino和ONNXRuntime推理【CPU】 YOLOv6-Openvino和ONNXRuntime推理【CPU】 YOLOv8-Openvino和ONNXRuntime推理【CPU】 YOLOv9-Openvino和ONNXRuntime推理【CPU】 注&#xff1a;YOLOv8和YOLOv9代码内容基本一致&#xff01; 全部代码Github&…...

【Linux命令】tload

tload 显示系统负载状况。 详细 tload命令以图形化的方式输出当前系统的平均负载到指定的终端。假设不给予终端机编号&#xff0c;则会在执行tload指令的终端机显示负载情形。 语法 tload &#xff08;选项&#xff09;&#xff08;参数&#xff09;选项 -s : 指定闲时的…...

Qt 通过pdfium将网络上的pdf显示为图片

前言 遇到个需求&#xff0c;就是在qt客户端显示服务器上的pdf文档&#xff0c;文档以base64格式返回给客户端。以下是实现方法&#xff1a; 1、在pro文件增加以下代码&#xff1a; INCLUDEPATH $$PWD/PDFiumSDK/include/publicDEPENDPATH $$PWD/PDFiumSDK/include/public…...

C语言数据结构与算法——深度、广度优先搜索(DFS、BFS)

目录 一、深度优先搜索&#xff08;Depth-First-Search 简称&#xff1a;DFS&#xff09; 无向图的深度优先搜索 有向图的深度优先搜索 二、广度优先搜索&#xff08;Breadth-First-Search 简称&#xff1a;BFS&#xff09; 无向图的广度优先搜索 有向图的广度优先搜索 深…...

Golang Channel 详细原理和使用技巧

1.简介 Channel(一般简写为 chan) 管道提供了一种机制:它在两个并发执行的协程之间进行同步&#xff0c;并通过传递与该管道元素类型相符的值来进行通信,它是Golang在语言层面提供的goroutine间的通信方式.通过Channel在不同的 goroutine中交换数据&#xff0c;在goroutine之间…...

CSS的浮动属性,web前端开发工程师

了解校招 知己知彼才能百战百胜&#xff0c;在准备校招之前&#xff0c;我们先要了解校招。 什么是校招&#xff1f; 校招&#xff0c;全称校园招聘&#xff0c;指企业招聘那些即将毕业的学生。校招主要分为三个部分&#xff1a;简历筛选&#xff0c;笔试&#xff0c;面试。 …...

Dubbo的集群容错方案

Dubbo提供了多种集群容错方案来保证分布式环境下的高可用性。这些容错方案可以在服务提供者不可用时&#xff0c;根据不同的业务需求和场景&#xff0c;选择不同的策略来处理。以下是Dubbo支持的一些主要集群容错方案&#xff1a; 1. Failover Cluster&#xff08;失败自动切换…...

两天学会微服务网关Gateway-Gateway路由规则

锋哥原创的微服务网关Gateway视频教程&#xff1a; Gateway微服务网关视频教程&#xff08;无废话版&#xff09;_哔哩哔哩_bilibiliGateway微服务网关视频教程&#xff08;无废话版&#xff09;共计17条视频&#xff0c;包括&#xff1a;1_Gateway简介、2_Gateway工作原理、3…...

three.js如何实现简易3D机房?(一)基础准备-下

three.js如何实现简易3D机房?(一)基础准备-上&#xff1a;http://t.csdnimg.cn/MCrFZ 目录 四、按需引入 五、导入模型 四、按需引入 index.vue文件中 <template><div class"three-area"><div class"three-box" ref"threeDemoRef…...

Android高级工程师面试实战,三幅图给你弄懂EventBus核心原理

阿里技术一面-35min 自我介绍 Android 有没有遇到OOM问题(有遇到内存泄漏问题)Handler机制ThreadLocalActivity启动到加载View过程View绘制过程LinearLayout (wrap_content) & TextView (match_parent) 最终结果???OKHttp(1. 为什么选择它&#xff1f; 2. 性能了解不…...

消息队列-kafka-服务端处理架构(架构,Topic文件结构,服务端数据的一致性)

服务端处理架构 资料来源于网络 网络线程池&#xff1a; 接受请求&#xff0c;num.network.threads&#xff0c;默认为 3&#xff0c;专门处理客户的发送的请求。 IO 线程池&#xff1a; num.io.threads&#xff0c;默认为 8&#xff0c;专门处理业务请求。也就是它不负责发…...

ES之API系列--index template(索引模板)的用法(有实例)

原文网址&#xff1a;ES之API系列--index template(索引模板)的用法(有实例)_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍ElasticSearch的index template(索引模板)的用法(有实例)。 官网网址 https://www.elastic.co/guide/en/elasticsearch/reference/8.0/index-temp…...

electron+vue3全家桶+vite项目搭建【28】封装窗口工具类【2】窗口组,维护窗口关系

文章目录 引入实现效果思路主进程模块渲染进程模块测试效果 引入 demo项目地址 窗口工具类系列文章&#xff1a; 封装窗口工具类【1】雏形 封装窗口工具类【2】窗口组&#xff0c;维护窗口关系 封装窗口工具类【3】控制窗口定向移动 我们思考一下窗口间的关系&#xff0c;窗…...

docker安装ES和kibana

文章目录 一、安装Elasticsearch1. 安装Elasticsearch2. 安装IK分词器3. elasticsearch-head 监控的插件4. 配置跨域 二、安装kibana 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、安装Elasticsearch 1. 安装Elasticsearch 安装Elasticsearch参…...

uniapp微信小程序获取当前位置

uni-app微信小程序uni.getLocation获取位置&#xff1b;authorize scope.userLocation需要在app.json中声明permission&#xff1b;小程序用户拒绝授权后重新授权-CSDN博客...

HarmonyOS创建项目和应用—设置数据处理位置

项目和应用介绍 关于项目 项目是资源、应用的组织实体。资源包括服务器、数据库、存储&#xff0c;以及您的应用、终端用户的数据等。在您使用部分服务时&#xff0c;您是数据的控制者&#xff0c;数据将按照您设置的数据处理位置来存储在指定区域。 通常&#xff0c;您不需…...

3.1_2024ctf青少年比赛部分web题

php后门 根据x-powered-by知道php的版本 该版本存在漏洞&#xff1a; PHP 8.1.0-dev 开发版本后门 根据报错信息&#xff0c;进行提示&#xff0c;前 GET / HTTP/1.1 Host: challenge.qsnctf.com:31639 User-Agentt:12345678system(cat /flag);var_dump(2*3);zerodium12345678…...

Vue3:OptionsAPI 与 CompositionAPI的比较

1、Vue2 Vue2的API设计是Options&#xff08;配置&#xff09;风格的。 Options API 的弊端 Options类型的 API&#xff0c;数据、方法、计算属性等&#xff0c;是分散在&#xff1a;data、methods、computed中的&#xff0c;若想新增或者修改一个需求&#xff0c;就需要分别…...

JavaSec-RCE

简介 RCE(Remote Code Execution)&#xff0c;可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景&#xff1a;Groovy代码注入 Groovy是一种基于JVM的动态语言&#xff0c;语法简洁&#xff0c;支持闭包、动态类型和Java互操作性&#xff0c…...

K8S认证|CKS题库+答案| 11. AppArmor

目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作&#xff1a; 1&#xff09;、切换集群 2&#xff09;、切换节点 3&#xff09;、切换到 apparmor 的目录 4&#xff09;、执行 apparmor 策略模块 5&#xff09;、修改 pod 文件 6&#xff09;、…...

【WiFi帧结构】

文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成&#xff1a;MAC头部frame bodyFCS&#xff0c;其中MAC是固定格式的&#xff0c;frame body是可变长度。 MAC头部有frame control&#xff0c;duration&#xff0c;address1&#xff0c;address2&#xff0c;addre…...

React第五十七节 Router中RouterProvider使用详解及注意事项

前言 在 React Router v6.4 中&#xff0c;RouterProvider 是一个核心组件&#xff0c;用于提供基于数据路由&#xff08;data routers&#xff09;的新型路由方案。 它替代了传统的 <BrowserRouter>&#xff0c;支持更强大的数据加载和操作功能&#xff08;如 loader 和…...

前端导出带有合并单元格的列表

// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...

c++ 面试题(1)-----深度优先搜索(DFS)实现

操作系统&#xff1a;ubuntu22.04 IDE:Visual Studio Code 编程语言&#xff1a;C11 题目描述 地上有一个 m 行 n 列的方格&#xff0c;从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子&#xff0c;但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等

&#x1f50d; 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术&#xff0c;可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势&#xff0c;还能有效评价重大生态工程…...

JavaScript 数据类型详解

JavaScript 数据类型详解 JavaScript 数据类型分为 原始类型&#xff08;Primitive&#xff09; 和 对象类型&#xff08;Object&#xff09; 两大类&#xff0c;共 8 种&#xff08;ES11&#xff09;&#xff1a; 一、原始类型&#xff08;7种&#xff09; 1. undefined 定…...

免费数学几何作图web平台

光锐软件免费数学工具&#xff0c;maths,数学制图&#xff0c;数学作图&#xff0c;几何作图&#xff0c;几何&#xff0c;AR开发,AR教育,增强现实,软件公司,XR,MR,VR,虚拟仿真,虚拟现实,混合现实,教育科技产品,职业模拟培训,高保真VR场景,结构互动课件,元宇宙http://xaglare.c…...

PostgreSQL——环境搭建

一、Linux # 安装 PostgreSQL 15 仓库 sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-$(rpm -E %{rhel})-x86_64/pgdg-redhat-repo-latest.noarch.rpm# 安装之前先确认是否已经存在PostgreSQL rpm -qa | grep postgres# 如果存在&#xff0…...