Wireshark自定义Lua插件

背景:

常见的抓包工具有tcpdump和wireshark,二者可基于网卡进行抓包:tcpdump用于Linux环境抓包,而wireshark用于windows环境。抓包后需借助包分析工具对数据进行解析,将不可读的二进制数转换为可读的数据结构。
wireshark不仅可以作为抓包工具,还可以作为包解析工具。Wireshark针对常见协议都提供了对应的解析插件,
如: TCP、UDP、HTTP、SIP等;同时提供了自定义插件机制,用户可以基于此解析自定义消息。至于插件,wireshark支持C语言插件和Lua插件,Lua作为脚本不需要编译,方便调试,速度相对C语言较慢。由于抓包时可以根据条件过滤,且一般数据包分析在本地进行,这部分性能优势相对于Lua脚本的方便性可以忽略。
因此,本文的主体内容是介绍如何在Wireshark中开发自定义插件解析消息。

1.插件配置方式

1.1 配置protobuf加载路径

根据Wireshark->Preferences->Protocols路径进入配置页面(Windows中路径为 “编辑->首选项->Protocols” ):
在这里插入图片描述
勾选Load .proto files on startup选项,然后点击Edit按钮开始配置。添加proto文件所在文件夹,勾选"load all files"选项。
经过上述配置,已经为wireshark指定了查找proto文件的路径,后续在lua脚本中可直接使用proto文件。

1.2 配置lua脚本路径

根据Wireshark->About Wireshark->Folders路径进入配置页面(windows下路径为: 帮助->关于->文件夹):
在这里插入图片描述
添加或者查看个人Lua插件的存放位置,后面开发的插件需要存放到这个路径下才会生效。添加或者修改lua插件后,需要重新加载lua插件:"分析->重新载入Lua插件"或者通过快捷键Ctrl+Shift+L.

1.3 Lua console调试工具

在"Tools->Lua console"页面可以编写和执行Lua脚本,可用于调试:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W2T20zdx-1717753458062)(C:\Users\0216001379\AppData\Roaming\Typora\typora-user-images\1716602725265.png)]
说明:调试工具是开发Lua插件的关键,结合快捷键Ctrl+Shift+L,通过打印提示信息,可以快速定位和发现问题。

2.wireshark关于Lua API介绍

Lua语法请参考: Lua使用方式介绍

Lua API参考自: https://mika-s.github.io/wireshark/lua/dissector/2017/11/04/creating-a-wireshark-dissector-in-lua-1.html

这部分介绍wireshark为Lua脚本的API,以及根据如何使用这些API实现自定义Lua插件。介绍Lua API前,有必要对Wireshark页面进行介绍:
在这里插入图片描述

需要关注上图红色标注的区域,包括:column区、tree区、data区,后续API会操作这些区域。

2.1 定义协议

seong_protocol = Proto("seong",  "seong description")

seong_protocol.dissector = function(buffer, pinfo, tree)
 local subtree = tree:add(seong_protocol, buffer(),"Seong Message Data"); 
end

DissectorTable.get("tcp.port"):add(9003, seong_protocol)

将上述Lua插件注册到环境后,可使用自定义的seong协议过滤消息,Wireshark页面显示如下:
在这里插入图片描述
定义协议需要三个步骤:定义Proto协议对象,为Proto对象添加解码器方法,将Proto对象与对应的端口进行绑定。
[1] 定义协议对象

seong_protocol = Proto("seong",  "seong description")

Proto作为构造参数用于创建Proto对象,接收两个参数,协议名称和协议描述。

[2] 为协议对象添加解析器
解析器函数:

seong_protocol.dissector = function(buffer, pinfo, tree)
 local subtree = tree:add(seong_protocol, buffer(),"Seong Message Data"); 
end

函数包括三个入参:
(1) buffer为二进制消息数据,可以通过类似buffer(0,2)方式从消息中截取字节数组;
(2) pinfo为数据包的元数据对象,包括消息大小、源地址/目标地址、大小、时间戳等信息;
(3) tree为协议树节点对象,数据结构会被渲染在tree区。

tree.add方法:
解析器内部的tree:add(seong_protocol, buffer(),"Seong Message Data")功能是: 在tree区域添加一个子树(并返回子树的引用)。其中第一个参数是必选的,后面两个参数是可选的:
(1) 协议参数
tree区域中,每层协议对应一个子树,即每个子tree需要与指定的协议绑定,此时需要为seong协议创建一个子树:

local subtree = tree:add(seong_protocol, nil, nil);

后续通过操作subtree对象,为seong协议子树添加显示数据。

(2) 数据参数
当传递为nil和buffer或者buffer(0,2) 时,鼠标选中seong协议时,关联的data区域不同:
在这里插入图片描述
(3) 描述信息
当描述信息为nil时,wireshark会选择使用协议的描述信息展示。

[3] 将协议与端口绑定
将协议与端口绑定后,Wireshark会自动将该端口上的消息使用绑定的协议解析:

DissectorTable.get("tcp.port"):add(9003, seong_protocol)

此时,TCP协议的9003端口的消息使用seong插件解析。

2.2 修改Column区

在过滤窗口,通过seong过滤后,可以得到TCP-9003端口的消息,显示的Protocol协议仍未TCP,应该修改为seong. 在解析器内部添加语句pinfo.columns.protocol:set(seong_protocol.name),得到:

seong_protocol = Proto("seong",  "seong description")

seong_protocol.dissector = function(buffer, pinfo, tree)
 local subtree = tree:add(seong_protocol, buffer(),"Seong Message Data");
 pinfo.columns.protocol:set(seong_protocol.name); 
end

DissectorTable.get("tcp.port"):add(9003, seong_protocol)

Wireshark显示为:
在这里插入图片描述
消息的协议名称修改为了seong.
除了protocol外,还可以通过pinfo.columns对象修改columns区域的其他字段的内容, 如修改info消息:
pinfo.columns.info:set("此时充值VIP可观看");
在这里插入图片描述

2.3 修改Tree区

Tree区为重点区域,自定义插件的核心功能是为了在这个区域直观地展示消息的内容。解析器的重点职责是从二进制数据中解析消息,并将消息作为字段添加到tree上,从而在Tree区域展示。
以下结合两种方式,其中通过Proto对象的fields属性方式是官方文档的推荐方式;直接操作tree对象方式是个人探索所得,相对比较简单(可能有坑)。

2.3.1 Proto对象的fields属性方式

先给出案例:

seong_protocol = Proto("seong",  "seong description")

message_length = ProtoField.int32("message_length", "Message-Length", base.DEC)
seong_protocol.fields = {message_length}

seong_protocol.dissector = function(buffer, pinfo, tree)
 local subtree = tree:add(seong_protocol, buffer(),"Seong Message Data");
 pinfo.columns.protocol:set(seong_protocol.name);
 
 subtree:add(message_length, 123456)
end

DissectorTable.get("tcp.port"):add(9003, seong_protocol)

与之前的lua脚本区域在于新增了Proto.fields相关的逻辑:
[1] 声明字段类型
message_length = ProtoField.int32(“message_length”, “Message-Length”, base.DEC)
创建一个属性,属性名称为message_length,描述为Message-Length(显示使用), 为十进制的整数。
[2] 字段添加到协议对象中

seong_protocol.fields = {message_length}

[3] 为message_length赋值,并添加到tree中

subtree:add(message_length, 123456)

此时, wireshark显示如下:
在这里插入图片描述

2.3.2 直接操作tree对象

通过subtree:add(字符串)方法直接将字符串设置到tree对象上:

seong_protocol = Proto("seong",  "seong description")

seong_protocol.dissector = function(buffer, pinfo, tree)
 local subtree = tree:add(seong_protocol, buffer(),"Seong Message Data");
 pinfo.columns.protocol:set(seong_protocol.name);
 
 subtree:add("Message-Length: " .. 11223344)
end

DissectorTable.get("tcp.port"):add(9003, seong_protocol)

在这里插入图片描述

2.3.3 简单案例

假设消息中前两个字节表示有效的数据长度:

seong_protocol = Proto("seong",  "seong description")

seong_protocol.dissector = function(buffer, pinfo, tree)
 local subtree = tree:add(seong_protocol, buffer(),"Seong Message Data");
 pinfo.columns.protocol:set(seong_protocol.name);
 
 subtree:add("Message-Length: " .. buffer(0,2))
end

DissectorTable.get("tcp.port"):add(9003, seong_protocol)

其中,buffer(0,2)从二进制消息中提取前两个字节; subtree:add方法调用时,可以关联data区域:
subtree:add("Message-Length: " .. buffer(0,2)) 修改为
subtree:add(buffer(0,2), "Message-Length: " .. buffer(0,2)):
显示如下:
在这里插入图片描述

3.案例

3.1 protobuf文件准备

Person.proto文件:

syntax = "proto2";

option java_package = "com.seong";

option java_outer_classname = "TestProtoMsg";

message Person {
    required int32 id = 1;
    required string name = 2;
    required bool isMale = 3;
    repeated Address address = 4;
};

message Address {
    required string country = 1;
    optional string location = 2;
};

编译后,生成com.seong.TestProtoMsg类,内部有Person和Address两个内部类,生成的Java类将在服务端和客户端程序中使用。然后将Person.proto文件放到1.1章节中配置的protobuf加载路径下。

3.2 Java服务端和客户端单例

使用Netty构建一个服务端(监听端口为9999)与客户端, 二者之间通过TCP-Protobuf通信,消息格式如下:
在这里插入图片描述
首部固定为AAAA(2字节),消息大类为BB(1字节), 消息子类为CC(1字节),消息长度表示PB消息体的长度(2字节),PB消息内容为5.1中Person.proto文件的protobuf消息。

关于Netty相关代码这里不进行介绍,请参考IO系列-netty相关的文章。

客户端:

(1) 客户端Netty模板代码:

public class Application {
    public static void main(String[] args) throws Exception {
        new Application().start();
    }

    public void start() throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) {
                    ch.pipeline().addLast(new PersonProtoBufEncoder());
                }
            });

            ChannelFuture f = b.connect("localhost", 9999).sync();
            f.channel().writeAndFlush(buildPersonMsg());
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

(2) 构造消息: 根据PB定义构造案例消息

private TestProtoMsg.Person buildPersonMsg() {
    TestProtoMsg.Person.Builder personBuilder = TestProtoMsg.Person.newBuilder();
    personBuilder.setId(1960001001);
    personBuilder.setName("ue001");
    personBuilder.setIsMale(false);
    TestProtoMsg.Address.Builder addressBuilder = TestProtoMsg.Address.newBuilder();
    addressBuilder.setCountry("zh-CN");
    addressBuilder.setLocation("NanJing");
    personBuilder.addAddress(addressBuilder.build());
    return personBuilder.setId(1).build();
}

(3) Protobuf编码器:将TestProtoMsg.Person对象编码为二进制数据,然后发送给服务端

public class PersonProtoBufEncoder extends MessageToByteEncoder<TestProtoMsg.Person> {
    private static final int TYPE = 4;

    private static final int LENGTH = 4;

    @Override
    protected void encode(ChannelHandlerContext ctx, TestProtoMsg.Person person, ByteBuf byteBuf) {
        byte[] playLoadBytes = person.toByteArray();
        int playLoadLen = playLoadBytes.length;
        ByteBuf msgBuffer = Unpooled.buffer(TYPE + LENGTH + playLoadLen);
        msgBuffer.writeBytes(new byte[] {(byte)0xAA, (byte)0xAA});
        msgBuffer.writeBytes(new byte[] {(byte)0xBB, (byte)0xCC});
        msgBuffer.writeInt(playLoadLen);
        msgBuffer.writeBytes(playLoadBytes);
        byteBuf.writeBytes(msgBuffer);
    }
}

服务端:

(1) 服务端Netty模板代码:

public class Application {
    public static void main(String[] args) throws Exception {
        new Application().start(9999);
    }

    public void start(int port) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) {
                        ch.pipeline().addLast(new PersonProtoBufDecoder());
                        ch.pipeline().addLast(new PersonProtoServerHandler());
                    }
                })
                .option(ChannelOption.SO_BACKLOG, 128)
                .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture f = b.bind(port).sync();

            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

(2) 解码器: 将来自客户端的二进制数据解码为TestProtoMsg.Person对象

public class PersonProtoBufDecoder extends ByteToMessageDecoder {
    private static final int TYPE_HEAD = 4;

    private static final int LENGTH_HEAD = 4;

    private static final int HEAD_LEN = TYPE_HEAD + LENGTH_HEAD;

    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        byte[] msgBytes = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(msgBytes);

        int msgLen = msgBytes.length;
        if (msgLen <= HEAD_LEN) {
            return;
        }
        byte[] bodyMsg = new byte[msgLen - HEAD_LEN];
        System.arraycopy(msgBytes, HEAD_LEN, bodyMsg, 0, msgLen - HEAD_LEN);
        TestProtoMsg.Person person = TestProtoMsg.Person.parseFrom(bodyMsg);
        list.add(person);
    }
}

(3) 解码后的消息处理: 打印TestProtoMsg.Person对象

@Slf4j
public class PersonProtoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        if (!(msg instanceof TestProtoMsg.Person)) {
            ctx.fireChannelRead(msg);
            return;
        }
        LOGGER.info("Receive from client, msg is {}.", msg);
    }
}

运行结果如下所示:

17:17:42.874 [nioEventLoopGroup-3-1] INFO com.seong.PersonProtoServerHandler - Receive from client, msg is id: 1
name: "ue001"
isMale: false
address {
  country: "zh-CN"
  location: "NanJing"
}
.

3.3 抓包分析

通过wireshark或者tcpdump可进行抓包,这里对端口进行过滤(9999):
在这里插入图片描述
可以看到客户端与服务端的通信数据包已被获取,为二进制数据,没有可读性。

3.4 Lua脚本定义协议

-- 自定义协议:Proto构造函数有两个参数:名称和描述
seong_protocol = Proto("seong",  "seong Message")
-- 添加一个字段,用于在数据树中显示
message_length = ProtoField.int32("seong.message_length", "PB-Message-Length", base.DEC)
seong_protocol.fields = {message_length}

-- 自定义协议的解析器
seong_protocol.dissector = function(buffer, pinfo, tree)
 -- 消息长度为0,直接返回
    local length = buffer:len();
    if length == 0 then return end;
 
    -- 添加子树,显示为Seong Message Data
    local subtree = tree:add(seong_protocol, buffer(),"Seong Message Data"); 
    
    -- 消息树中添加PB-Message-Length信息
 local msgLen = buffer(4,4):uint()
 subtree:add(message_length, msgLen)
 
    -- 消息树中添加HEAD,Main-Type,Sub-Type数据
    local headFlag = "" .. buffer(0,2)
 local mainType = "" .. buffer(2,1)
 local subType = "" .. buffer(3,1)
 subtree:add(buffer(0,2),"HEAD: " .. headFlag)
 subtree:add(buffer(2,1),"Main-Type: " .. mainType)
 subtree:add(buffer(3,1),"Sub-Type: " .. subType)
    
 -- 调用wireshark内置的protobuf解析器
    sipProtoType = "Person";
 pinfo.private["pb_msg_type"] = "message," .. sipProtoType
 local protobuf_dissector = Dissector.get("protobuf");
 local result = pcall(Dissector.call, protobuf_dissector, buffer(8, msgLen):tvb(), pinfo, subtree)
    pinfo.columns.protocol:set(seong_protocol.name)
end

--注册协议到指定的端口
local tcp_port = DissectorTable.get("tcp.port"):add(9999, seong_protocol)
3.5 查看协议

在这里插入图片描述
此时二进制数据已经通过树区域进行了展示, 与服务端解码后的消息保持一致。

3.6 扩展

本文中涉及的protobuf文件只有一个,实际上系统间的消息类型数以百计,因此需要对上述Lua脚本进行扩展以具备更好的通用性。
注意到定义消息时,添加了消息大类和消息子类两个冗余字段,可通过这两个字段与protobuf之间建立映射关系,即这两个消息确定了消息类型和解码方式。

local function matchedProtoType(mainType, subType)
 print("mainType:" .. mainType .. "subType:" .. subType)
 if mainType == "bb" then
  if subType == "cc" then
   return "Person";
  end
 
 else
  return nil
 end
 
 return nil
end

定义一个函数,根据mainType和subType返回protobuf消息类型,相应地,Lua脚本中解析器的定义进行如下修改(将硬编码的Person类型改为通过matchedProtoType获取):

idslds_protocol.dissector = function(buffer, pinfo, tree)
 -- ...
 
    --sipProtoType = "Person";
 sipProtoType = matchedProtoType(mainType,subType)
 -- ...
end

本文主要介绍如何在wireshark中介绍自定义Lua插件,因此扩展这一部分不进行详细描述。后续在IO系列-Netty应用相关的文章中将介绍一个通过Netty实现子网穿越的案例;之后结合该案例对Lua插件的应用进行完整的阐述。

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

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

相关文章

SpringBoot+Vue实现前后端分离基本的环境搭建

目录 一、Vue项目的搭建 &#xff08;1&#xff09;基于vite创建vue项目 &#xff08;2&#xff09;引入elementplus &#xff08;3&#xff09;启动后端服务&#xff0c;并测试 二、SpringBoot项目的搭建 &#xff08;1&#xff09;通过idea创建SpringBoot项目 &#x…

ipables防火墙

一、Linux防火墙基础 Linux 的防火墙体系主要工作在网络层&#xff0c;针对 TCP/IP 数据包实施过滤和限制&#xff0c;属于典 型的包过滤防火墙&#xff08;或称为网络层防火墙&#xff09;。Linux 系统的防火墙体系基于内核编码实现&#xff0c; 具有非常稳定的性能和高效率&…

AI高考大战,揭秘五大热门模型谁能问鼎数学之巅?

在高考前&#xff0c;我就有想法了&#xff0c;这一次让AI来做做高考题。就用国内的大模型&#xff0c;看哪家的大模型解题最厉害。 第一天考完&#xff0c;就拿到了2024高考数学2卷的电子版&#xff0c;这也是重庆市采用的高考试卷 这次选了5个AI工具&#xff0c;分别是天工&a…

FlexJavaFramwork

FlexJavaFramwork架构

【python】python商业客户流失数据模型训练分析可视化(源码+数据集+课程论文)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

skywalking学习

文章目录 前言一、skywalking单体安装部署1. 下载skywalking2. 部署oap和oap-ui服务3. 测试skywalking监控springboot应用 二、搭建swck(skywalking集群)1.安装k8s2.下载swck3.设置pod自动注入java agent 三、skywalking监控python四、skywalking监控cpp总结参考 前言 本文主要…

生气时,你的“心”会发生什么变化?孟德尔随机化分析猛如虎,结果都是套路...

“不生气不生气&#xff0c;气出病来无人替”&#xff0c;不少人遇事常这样宽慰自己。事实上&#xff0c;“气死”真不是危言耸听。越来越多的研究证明了情绪稳定对健康的重要性&#xff0c;那么&#xff0c;当情绪频繁波动时&#xff0c;我们的心血管究竟会发生什么变化&#…

SpringBoot 的多配置文件

文章目录 SpringBoot 的多配置文件spring.profiles.active 配置Profile 和 ActiveProfiles 注解 SpringBoot 的多配置文件 spring.profiles.active 配置 默认情况下&#xff0c;当你启动 SpringBoot 项目时&#xff0c;会在日志中看到如下一条 INFO 信息&#xff1a; No act…

产气荚膜梭菌定植与婴儿食物过敏之间的关联

谷禾健康 牛奶蛋白过敏&#xff08;CMPA&#xff09;是婴儿最常见的食物过敏类型之一。粪便病原菌培养显示产气荚膜梭菌阳性率超过30%&#xff0c;明显高于其他细菌。因此推测产气荚膜梭菌定植可能是婴儿牛奶蛋白过敏的发病因素之一。 一项真实世界的研究&#xff0c;杨敏团队从…

C++全栈聊天项目(21) 滚动聊天布局设计

滚动聊天布局设计 我们的聊天布局如下图 最外层的是一个chatview&#xff08;黑色&#xff09;&#xff0c; chatview内部在添加一个MainLayout&#xff08;蓝色&#xff09;&#xff0c;MainLayout内部添加一个scrollarea(红色)&#xff0c;scrollarea内部包含一个widget&…

【Redis】Redis经典问题:缓存穿透、缓存击穿、缓存雪崩

目录 缓存的处理流程缓存穿透解释产生原因解决方案1.针对不存在的数据也进行缓存2.设置合适的缓存过期时间3. 对缓存访问进行限流和降级4. 接口层增加校验5. 布隆过滤器原理优点缺点关于扩容其他使用场景SpringBoot 整合 布隆过滤器 缓存击穿产生原因解决方案1.设置热点数据永不…

Swift 序列(Sequence)排序面面俱到 - 从过去到现在(二)

概览 在上篇 Swift 序列(Sequence)排序面面俱到 - 从过去到现在(一)博文中,我们讨论了 Swift 语言中序列和集合元素排序的一些基本知识,我们还给出了以自定义类型中任意属性排序的“康庄大道”。 不过在实际的撸码场景中,我们往往需要的是“多属性”同时参与到排序的考…

STM32F103C8T6基于HAL库移植uC/OS-III

文章目录 一、建立STM32CubeMX工程二、移植1、 uC/OS-III源码2、移植过程 三、配置相关代码1、bsp.c和bsp.h2、main.c3、修改启动代码4、修改app_cfg.h文件5、修改includes.h文件6、修改lib_cfg.h文件 四、编译与烧录总结参考资料 学习嵌入式实时操作系统&#xff08;RTOS&…

Swift 序列(Sequence)排序面面俱到 - 从过去到现在(一)

概览 在任何语言中对序列(或集合)元素的排序无疑是一种司空见惯的常规操作,在 Swift 语言里自然也不例外。序列排序看似简单,实则“暗藏玄机”。 要想真正掌握 Swift 语言中对排序的“各种姿势”,我们还得从长计议。不如就先从最简单的排序基本功开始聊起吧。 在本篇博…

9行超强代码用Python工具快速获取放假日期

9行超强代码用Python工具快速获取放假日期 在很多场景下,我们需要获知国内具体的节假日安排情况,而国内每一年具体的放假安排以及调休情况,都依赖于国务院发布的具体公告,如果不想自己手动整理相关数据的话,我们可以用Python来快速获取最新的放假日期. 可以通过调用公开的 API…

spark-3.5.1+Hadoop 3.4.0+Hive4.0 分布式集群 安装配置

Hadoop安装参考: Hadoop 3.4.0HBase2.5.8ZooKeeper3.8.4Hive4.0Sqoop 分布式高可用集群部署安装 大数据系列二-CSDN博客 一 下载:Downloads | Apache Spark 1 下载Maven – Welcome to Apache Maven # maven安装及配置教程 wget https://dlcdn.apache.org/maven/maven-3/3.8…

App UI 风格创新无限

App UI 风格创新无限

如何理解与学习数学分析——第二部分——数学分析中的基本概念——第8章——可微性

第2 部分&#xff1a;数学分析中的基本概念 (Concepts in Analysis) 8. 可微性(Differentiability) 本章讨论梯度(gradients)/斜率(slopes)和切线(tangent)&#xff0c;指出常见的误解并解释如何避免这些误解。将可微性的定义与图形表示联系起来&#xff0c;展示如何将其应用…

什么是2+1退休模式?什么是链动2+1模式?

21退休模式又称链动21模式&#xff0c;主要是建立团队模式&#xff0c;同时快速提升销量。是目前成熟模式中裂变速度最快的模式。21退休模式合理合规&#xff0c;同时激励用户公司的利润分享机制&#xff0c;让您在享受购物折扣的同时&#xff0c;也能促进并获得客观收益。 模…