基于有限状态机开发健壮的Nodejs/TCP客户端

news2025/2/26 14:38:25

有限状态机是一种数学计算模型,它描述了在任何给定时间只能处于一种状态的系统的行为。形式上,有限状态机有五个部分:

  • 初始状态值 (initial state)
  • 有限的一组状态 (states)
  • 有限的一组事件 (events)
  • 由事件驱动的一组状态转移关系 (transitions)
  • 有限的一组最终状态 (final states)

状态是指由状态机建模的系统中某种有限的定性的“模式”或“状态”,并不描述与该系统相关的所有(可能是无限的)数据。例如,水可以处于以下 4 种状态中的一种:冰、液体、气体或等离子体。然而,水的温度可以变化,所以其测量值是定量的和无限的。再比如管理TCP Socket连接时,其生命周期内存在明显的有限状态转换。

可能有相当多的同学在开发中没意识到有限状态机的作用,但是实际上,我们几乎无时不刻在有意无意间使用了有限状态机。当您在开发过程中能有意识地系统地进行有限状态分析并应用有限状态机,往往代表着您达到了较高的水平。

目前开源的有限状态机实现中比较知名的有:

  • xstate:堪称状态机航空母舰,功能太强大了,也太复杂了,学习成本非常高。
  • Javascript State Machine:功能较弱,在实际试用过程中发现在进行异步切换时存在问题。
  • jssm:特点是引入自己的DSL语法来描述状态机,使用起来比较别扭。

事实上,从功能完整度上看xstate是第一选择,但是其过于复杂了,在功能与易用平衡方面并不理想。

因此,我们开发了FlexState有限状态机,力求在功能性、易用性上达到平衡

FlexState是一款简单易用的有限状态机,具有以下特性:

  1. 支持基于Class构建有限状态机实例
  2. 支持状态enter/leave/resume/done钩子事件
  3. 状态切换完全支持异步操作
  4. 支持定义异步状态动作Action
  5. 支持状态切换生命周期事件订阅
  6. 支持错误处理和状态切换中止
  7. 基于TypeScript开发
  8. 支持子状态
  9. 核心代码90%+单元测试覆盖率

Github
官网

快速入门

下面我们以开发基于nodejs/net.socket的TCP客户端为例来说明FlexStateMachine的使用。

作为例子,我们为TCPClient设计以下几种状态:

  • Initial:初始状态,构建socket实例后处于该阶段。
  • Connecting:连接中,当调用Connect方法,触发connect事件前。
  • Connected:已连接,当触发connect事件后。
  • Disconnecting:正在断开,当调用destory或end方法后,end/close事件触发前。
  • Disconnected:被动断开,当触发end/close事件后。
  • AlwaysDisconnected: 主动断开状态
  • IDLE: 自动添加的空闲状态,状态机未启动时
  • ERROR: 自动添加的错误状态,特殊的FINAL状态

TCPClient的状态图如下:

第一步:构建状态机

推荐直接继承FlexStateMachine来创建一个TCPClient实例,该种方式更加简单易用。


import { state, FlexStateMachine } from "flexstate"

class TcpClient extends FlexStateMachine{
  // 定义状态
    static states = { 
        Initial : { value:0, title:"已初始化", next:["Connecting","Connected","Disconnected"],initial:true},
        Connecting	: { value:1, title:"正在连接...", next:["Connected","Disconnected"] },
        Connected : { value:2, title:"已连接", next:["Disconnecting","Disconnected"] },
        Disconnecting : { value:3, title:"正在断开连接...", next:["Disconnected"] },
        Disconnected : {value:4, title:"已断开连接", next:["Connecting"]},
        AlwaysDisconnected	: {value:5, title:"已主动断开连接", next:["Connecting"]}
    }                   

    constructor(options:FlexStateOptions){
      super(Object.assign({
        host:"",
        port:9000,
        autoStart:true,
        context           : null,                // 状态上下文对象,当执行动作或状态转换事件时的this指向
        autoStart         : true,                // 自动启动状态机
        timeout           : 30 * 1000            // 当执行状态切换回调时的超时,如enter、leave、done回调
        injectActionMethod: true,                // 将动作方法注入到当前实例中  
      },options)) 
    } 

    @state{
        when:["Initial","Disconnected","Error"],   // 代表只能当处于此三种状态时才允许调用连接方法    
        pending:"Connecting",						// 执行后进入正在连接中的状态
    }
    connect(){
        this._socket.connect(this.options)    
    } 
    @state({
        when:["Connected"],     	// 代表只有在已连接状态才允许执行断开方法
        pending:"Disconnecting"
    })  
    disconnect(){
        this._socket.destory()
    }
    // 当状态转换成功后会调用此方法
    ontTransition({error,from,to,done,timeConsuming}){
        console.log(`从<${previous}>转换到<current>,耗时:${timeConsuming}ms`)       // 例 ==> 从<Connecting>转换到<Connected,耗时12ms>
        console.log(this.current)                          // {name,value,....}
    }
    onData(data){....}
    }

说明:

  • 以上我们创建了一个继承自FlexStateMachine来创建一个TCPClient实例
  • 并且定义了InitialConnectingConnectedDisconnectingDisconnectedAlwaysDisconnected共六个状态以及状态之间的转换约束。同时,状态机还会自动添加一个ERRORIDLE状态。
  • 定义了connectdisconnect两个动作action,在这两个方法前添加@state代表了当执行这两个方法会导致状态变化。

第二步:初始化TCPSocket

当实例化TCPClient实例后,首先应该创建Socket实例。由于TCPClient实例继承自FlexStateMachine,并且我们指定了Initial为初始化状态。
状态机会在实例化并启动后自动转换到Initial状态。因此,我们可以在进入Initial状态前进行初始化操作。

class TcpClient extends FlexStateMachine{
  // 转换至Initial状态前会调用方法
  async onInitialEnter({retry,retryCount}){
    try{      
        this._socket = new net.Socket()
        // 当连接成功时,切换到Connected事件; 每一个状态均有一个大写的状态值实例成员
        // this.CONNECTED==this.states.Connected.value
        this._socket.on("connect",()=>this.transition(this.CONNECTED)) 
        this._socket.on("close",()=>{
            //....     详见后续重连说明
        }) 		
        // 套接字因不活动而超时则触发,这只是通知套接字已空闲,用户必须手动关闭连接。
        // 通过事件触发方式来执行disconnect动作
        this._socket.on("timeout",()=>this.emit("disconnect"))
        this._socket.on("error",()=>this.transition(this.ERROR))
        this._socket.on("data",this.onData.bind(this))
    }catch(e){
        if(retryCount<3){
            retry(1000)                                      // 1000ms后重试执行
        }else{				//
            throw e
        }      
    }
}

TCPClient实例化,状态机处于IDLE状态(<tcp实例>.current.name=='IDLE'),然后状态机自动启动(autoStart=true)将转换至Initial状态(initial状态)。

  • 状态机转换至Initial状态前会调用onInitialEnter。我们可以在此方法中创建TCP Socket实例以及其他相关的初始化。
  • onInitialEnter成功执行完毕后,状态机的状态将转换至Initial。(IDLE->Initial
  • 如果在onInitialEnter函数初始化失败或出错,则应该抛出错误。错误将导致状态机将无法转换至Initial状态,也就无法进行后续的所有操作了。一般在初始化失败时,会进行如下操作:
    • 进行重试操作,直至初始化成功(即成功创建好Socket并进行相应的事件绑定)。
    • 反复重试多次失败后,也可能会放弃重试,TCP Client将无法切换到Initial状态,而是保持在IDLE状态。
    • 当条件具备时,状态机需要重新运行(即调用tcp.start()来启动状态机),将重复上述过程。

第三步:连接服务器

TCPClient实例初始化完成后,就可以开始连接服务器。我们可以在类上创建状态机动作connect,启动连接操作。

import { state, FlexStateMachine } from "flexstate"

class TcpClient extends FlexStateMachine{
    // 通过装饰器来声明这是一个状态动作  
    @state({
        // 代表只能当处于此三种状态时才允许执行动作,即调用连接方法
        when:["Initial","Disconnected","Error"],      
        // 执行后进入正在连接中的状态
        pending:"Connecting"	
    })                
    async connect(){      
        this._socket.connect(this.options)          
    }
}
// 创建连接实例
let tcp = new TcpClient({...})
// 连接
tcp.connect()   
// 状态机状态将变化: Initial -> Connecting -> Connected
// 如果连接出错状态将变化:Initial -> Connecting -> Error

上述的@state({....})定义了一个状态机动作,代表当调用connect方法时会导致一系列的状态转换:

  • 动作名称为connect,会创建一个同名的实例方法tcp.connect替换掉原始的connect方法。
  • when参数代表了只有当前状态为[InitialDisconnectedError]其中一个时才允许执行connect动作。
  • pending="Connecting"代表,执行connect动作前,状态机的状态将暂时会切换至Connecting,也就是会显示正在连接中。由于连接操作可能是耗时的,所有设计一个正在连接中是比较符合实际业务逻辑的。
  • 如果执行socket.connect({...}) 出错,可以通过@state({retry,retryCount})来启用重试逻辑。需要注意的是 调用connect成功仅仅代表该方法在调用时没有出错,并不代表已经连接成功。是否连接成功需要由socket/connect事件来触发确认。
  • 在上述中,并没有显式指定当连接成功时的状态,原因是因为connect方法是一个异步方法,是否连接成功或失败是通过事件回调的方式转换状态的。在初始化阶段,我们订阅了closeend等回调。
    • this._socket.on("close",()=>this.transition(this.DISCONNECTED))
    • this._socket.on("end",()=>this.transition(this.DISCONNECTED))
    • this._socket.on("error",()=>this.transition(this.ERROR))

当执行socket.connect方法后,如果接收到close/end/error则会转换到对应的DISCONNECTEDERROR状态。

  • 至此,实现了当tcp.connect方法,状态转换到Connecting状态,连接成功转换至Connected状态,连接被断开转换至Disconnected状态,出现错误时转换到ERROR状态。并且在出错时会进行一定重试操作,更多关于重试的内容详见后续介绍。

第四步:侦听连接状态

在TCP连接生命周期内,状态机会在最后Initial/Connecting/Connected/Disconnecting/Disconnected/AlwaysDisconnected状态之间进行转换,我们希望可能侦听状态机的状态转换事件,以便在连接发生状态转换时进行一些操作,此时就可以侦听各种连接事件。

侦听连接状态有两种方法:

  • FlexStateMachine本身就是一个EventEmitter,可以通过订阅事方式进行侦听。
// *****侦听某个状态事件*****

tcp.on("Connected/enter",({from,to})=>{
    // 当准备进入连接前状态时触发此事件
})      

tcp.on("Connected/leave",({from,to})=>{
    // 当准备要离开连接状态时触发此事件
})  

tcp.on("Connected/done",({from,to})=>{
    // 当切换至连接状态后触发此事件
})   

  • 在类中也可以直接定义on<状态名>Enteron<状态名>on<状态名>Doneon<状态名>Leave类方法来侦听事件。
class TcpClient extends FlexStateMachine{
  
  onInitialEnter({from,to}){...}			// 进入Initial状态前
  onInitial({from,to}){...}					// 已切换至Initial状态
  onInitialDone({from,to}){...}				// ===onInitial
  onInitialLeave({from,to}){...}		  	// 离开Initial状态时
  
  
  onConnectingEnter({from,to}){...}			// 进入Connecting状态前
  onConnecting({from,to}){...}				// 已切换至Connecting状态
  onConnectingDone({from,to}){...}      	// === onConnecting
  onConnectingLeave({from,to}){...}		  	// 离开Connecting状态时
  
  onConnectedEnter({from,to}){...}			// 进入Connected状态前
  onConnected({from,to}){...}				// 已切换至Connected状态
  onConnectedDone({from,to}){...}			// ===onConnected
  onConnectedLeave({from,to}){...}		  	// 离开Connected状态时
  
  //...所有状态均可以定义on<状态名>Enter、on<状态名>、on<状态名>Leave事件 
  
}

第五步:断开重新连接

连接管理中的断开重连是非常重要的功能,要处理此逻辑,首先分析一下什么情况下会断开连接。

断开连接一般包括主动被动两种情况:

  • 服务器或网络问题等导致的连接断开

此种情况属于客户端被动断开连接,一般会需要进行自动重新连接。服务器主动断开时,客户端会侦听到end事件,直接进入断开状态。即状态机不会切换到Disconnecting,而是直接至Disconnected

  • 客户端主动断开连接

此种情况属性客户主动断开连接发,就是客户端主动调用disconnect方法,一般是不需要进行自动重连的。
主动断开时,需要调用socket.end方法,然后等待end事件的触发。状态机会经历从DisconnectingDisconnected的过程。

无论是主动断开连接还是被动断开连接,均会触发close事件,因此需要在close事件触发时区别是主动断开还是被动断开。
为了更好地区别主动断开被动断开,我们可以增加一个状态AlwaysDisconnected来代表是客户端主动断开,AlwaysDisconnected被设计为FINAL状态。
当状态机切换到Disconnected状态时调用connect动作方法来重新连接。当状态机切换到AlwaysDisconnected时,则不进行重新连接。
两者差别在于,如果是主动断开会经历Disconnecting状态,而被动断开则不会经过此状态,因此我们就可以在on("close")事件中处理将状态转换至AlwaysDisconnectedDISCONNECTED

class TcpClient extends FlexStateMachine{
  class TcpClient extends FlexStateMachine{
  ...
  // 转换至Initial状态前会调用方法
  async onInitialEnter({retry,retryCount}){
    // 在此需要确认该切换到Disconnected还是AlwaysDisconnected状态
    this._socket.on("close",()=>{
        // 主动调用disconnect方法时,状态机才会切换到Disconnecting
        if(this.current.name==="Disconnecting"){ 
            this.transition(this.ALWAYSDISCONNECTED)
        }else{
          this.transition(this.DISCONNECTED)
        }
    })
  }
  // 当切换至Disconnected状态的回调
  async onDisconnected({from,to}){
    await delay(3000)
    this.connect()								// 重新执行Connect动作
  }
  //
  async onConnectClosed({from,to}){
    
  }

  @state({
     when:"Connected",
     pending:"Disconnecting"
     // 由于调用end方法是异步操作,需要等待close事件触发后,才是真正的断开连接 
     // 因此,不能在调用disconnected返回后就将状态设置为AlwaysDisconnected
     // 也就是说不要在此配置rejected参数;
     // 假设执行this._socket.end没有出错,则状态将保持在Disconnecting状态,直至this._socket.on("close",callback)时才进行状态转换
     // rejected:""  
  })
  async disconnect(){
    // 注意:此操作是异步状态
    this._socket.end()   
  }
}

第六步:连接认证子状态

当tcp连接成功后,一般服务器会要求对客户连接进行认证才允许进行使用,而认证操作(login/logout)是一个耗时的异步操作,同样需要进行状态管理。当进入Connected状态后,状态将在未认证正在认证已认证三个状态间进行转换,并且在连接断开或者出错时马上退出这三个状态。因此,就有必要引入子状态的概念。

引入子状态后,对应的状态图更新如下:

在这里插入图片描述

class TcpClient extends FlexStateMachine{
    static states = { 
     Connected		: { 
       value:2, 
       title:"已连接", 
       next:["Disconnecting","Disconnected","Error"] 
       // 定义一个独立的状态机域
       scope:{
           states:{
              Unauthenticated : {value:0,title:"未认证",initial:true,next:["Authenticating"]},
              Authenticating  : {value:1,title:"正在认证",next:["Authenticated"]}
              Authenticated   : {value:2,title:"已认证",next:["Unauthenticated"]},
          }
       }
     },  
  }  
  ......
  // 当状态机进入Connected后会启动其子状态机
  // 子状态机会转换到其初始状态Unauthenticated,然后就可以在此执行登录动作
  async onUnauthenticatedEnter({from,to}){
      this.login()								// 
  }
  onAuthenticated({from,to}){
    
  }
  @state({
    when:["Authenticating"],
    pending:["Authenticating"]
  })
  async login(){
    await this.send({
        // 认证信息
    })
  }
  @state({
    when:["Authenticated"] 
  })
  async logout(){
    await this.send({
        // 注销信息
    })    
  }
}

推荐

以下是我的一大波开源项目推荐:

  • 全流程一健化React/Vue/Nodejs国际化方案 - VoerkaI18n
  • 无以伦比的React表单开发库 - speedform
  • 终端界面开发增强库 - Logsets
  • 简单的日志输出库 - VoerkaLogger
  • 装饰器开发 - FlexDecorators
  • 有限状态机库 - FlexState
  • 通用函数工具库 - FlexTools
  • 小巧优雅的CSS-IN-JS库 - Styledfc
  • 为JSON文件添加注释的VSCODE插件 - json_comments_extension
  • 开发交互式命令行程序库 - mixed-cli
  • 强大的字符串插值变量处理工具库 - flexvars
  • 前端link调试辅助工具 - yald
  • 异步信号 - asyncsignal

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Foxmail邮箱空间不够或邮件数太多

Foxmail邮箱空间不够或邮件数太多 解决办法&#xff1a; ①远程管理&#xff0c;删除远程邮件 ②删除本地邮件并同步服务器 ①远程管理&#xff0c;删除远程邮件 ②删除本地邮件并同步服务器 打开设置》》高级 此外&#xff0c;为了方便管理邮件可以设置过滤器 欢迎各位大佬…

51单片机LED8*8点阵显示坤坤跳舞打篮球画面

我们作为一名合格的 ikun&#xff0c;专业的小黑子&#xff0c;这个重要的知识必须学会。 先看效果&#xff1a; 51LED点阵_鸡你太美 这里我们首先要用到延时函数Delay&#xff1a; void Delay(unsigned int xms) {unsigned char i, j;while(xms--){ i 2;j 239;do{while (-…

物联网和工业物联网的区别——青创智通

工业物联网解决方案-工业IOT-青创智通 物联网&#xff08;IoT&#xff09;和工业物联网&#xff08;IIoT&#xff09;作为现代科技的重要分支&#xff0c;正在逐渐渗透到我们的日常生活和工业生产中。它们的应用范围广泛&#xff0c;涵盖了从智能家居到自动化工厂的多个领域。…

CTF题型 Http2降级走私原理分析例题分享

CTF题型 Http2降级走私原理分析&例题分享 文章目录 CTF题型 Http2降级走私原理分析&例题分享HTTP/2请求走私的产生回顾一下Http请求走私原理Http2请求的消息划分实际生产环境的限制如何利用H2.CL 实验环境准备例题分析H2.CL请求走私[GeekChanllenge 2023 Ez_Smuggling]…

http协议的历史与基本概念

文章目录 历史和发展起源&#xff1a;HTTP/0.9&#xff08;1991年&#xff09;&#xff1a;HTTP/1.0&#xff08;1996年&#xff0c;RFC 1945&#xff09;&#xff1a;HTTP/1.1&#xff08;1997年&#xff0c;RFC 2068&#xff1b;1999年更新为RFC 2616&#xff09;&#xff1a…

【动态规划】【 数位dp】2827. 范围中美丽整数的数目

本文涉及知识点 数位dp 动态规划汇总 LeetCode2827. 范围中美丽整数的数目 给你正整数 low &#xff0c;high 和 k 。 如果一个数满足以下两个条件&#xff0c;那么它是 美丽的 &#xff1a; 偶数数位的数目与奇数数位的数目相同。 这个整数可以被 k 整除。 请你返回范围 [l…

写作之路:寻找动力与认可的探索

✍️作者简介&#xff1a;小北编程&#xff08;专注于HarmonyOS、Android、Java、Web、TCP/IP等技术方向&#xff09; &#x1f433;博客主页&#xff1a; 开源中国、稀土掘金、51cto博客、博客园、知乎、简书、慕课网、CSDN &#x1f514;如果文章对您些帮助请&#x1f449;关…

Android14音频进阶:AudioFlinger究竟如何混音?(六十三)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒体系统工程师系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只…

R语言中的常用基础绘图函数 直方图,箱线图,条形图,散点图

目录 R语言中的绘图参数 绘图函数 1.plot函数绘制散点图 2.hist函数绘制直方图 如何修饰直方图? 如何在直方图上标注各组频数&#xff1f; 使用text函数把某些信息标注在直方图上 如何在直方图上添加概率密度曲线&#xff1f; 3.boxplot函数绘制箱线图 4.barplot函数…

实现安卓连接阿里云物联网平台(2)

完整工程链接 链接&#xff1a;https://pan.baidu.com/s/1ykcJHPBSKBXVMaMWKoVRvA?pwd8888 提取码&#xff1a;8888 &#xff08;1&#xff09;创建一个新工程 &#xff08;2&#xff09;添加mqtt包的依赖 implementation org.eclipse.paho:org.eclipse.paho.client.mqttv…

C语言技能数(知识点汇总)

C语言技能数&#xff08;知识点汇总&#xff09; C 语言概述特点不足之处 标准编程机制 数据类型变量数据类型字符类型整数类型符号位二进制的原码、反码和补码 浮点类型布尔类型 常量字面常量const 修饰的常变量#define定义的标识符常量枚举常量 sizeofsizeof(结构体)不要对 v…

代码随想录训练营Day26:● 93.复原IP地址 ● 78.子集 ● 90.子集II

93.复原IP地址 题目链接 https://leetcode.cn/problems/restore-ip-addresses/description/ 题目描述 思路 class Solution {List<String> list new ArrayList<>();public List<String> restoreIpAddresses(String s) {if(s.length()>12) return list…

并联谐振回路

并联谐振回路概述 串联谐振回路适用于低内阻电源(理想电压源)。而对于大内阻电源&#xff0c;则适合采用并联谐振回路。 并联谐振回路&#xff1a;指电感线圈L、电容器C和外加信号源相互并联的振荡回路。同样&#xff0c;由于电容器C的损耗很小&#xff0c;可以认为损耗电阻集…

JavaScript中a++和++a的区别是什么?

在JS中&#xff0c;a 和 a 都是对变量 a进行自增操作&#xff0c;但是他们在执行的行为和结果上有些不同&#xff1a; a (前缀自增)&#xff1a; 首先是将a的值增加1&#xff0c;然后返回 a 增加后的值。这意味着&#xff0c;表达式的其余部分使用 a的时候&#xff0c; a 的值…

【办公类-22-15】周计划系列(5-6)“周计划-06 周计划打印pdf(docx删除内容转PDF)“ (2024年调整版本)

作品展示 背景需求&#xff1a; 前期用docx&#xff08;删除第一页反思部分内容&#xff09;转PDF转png&#xff08;第一页&#xff09;的方式获得上传网页用的图片。 【办公类-22-14】周计划系列&#xff08;5-5&#xff09;“周计划-05 上传周计划png&#xff08;docx转PDF…

KTV点歌系统|基于JSP技术+ Mysql+Java+ B/S结构的KTV点歌系统设计与实现(可运行源码+数据库+设计文档)

推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含java&#xff0c;ssm&#xff0c;springboot的平台设计与实现项目系统开发资源&#xff08;可…

议题揭晓 | 3月23日武汉 Linux 用户组线下沙龙邀您参与!

内容来源&#xff1a;deepin 社区 WHLUG&#xff08;武汉Linux用户组&#xff09;线下沙龙将于 2024 年 3 月 23 日下午举办&#xff0c; 欢迎大家来到现场&#xff0c;一起交流最新产品动态与前沿技术&#xff0c;聆听社区成员的共建经验与收获。 本次活动由 deepin&#xff…

发票OCR-国税可进行的发票查验种类-接口文档

发票查验内容包括发票种类名称、发票代码、发票号码、金额、销售方名称、购买方名称等信息。可以在国家税务总局全国增值税发票查验平台上进行查验&#xff0c;也可以进入发票所属省、直辖市税务局官方网站的“我要查询-发票查询”模块进行查验&#xff0c;企业也可以通过发…

python基础——字符串的常见操作方法【下标索引,index,count,len,replace,split,strip】

&#x1f4dd;前言&#xff1a; 字符串是一种有序的&#xff0c;允许重复字符串存在的&#xff0c;不可修改的序列 这篇文章主要总结一下python中有关字符串的部分相关知识&#xff0c;以及字符串的常见操作方法&#xff1a; 1&#xff0c;和其他序列极其类似的操作方法 2&…

遥感深度学习:CNN-LSTM模型用于NDVI的预测(Pytorch代码深度剖析)

代码上传至Github库&#xff1a;https://github.com/ChaoQiezi/CNN-LSTM-model-is-used-to-predict-NDVI 01 前言 这是一次完整的关于时空遥感影像预测相关的深度学习项目&#xff0c;后续有时间更新后续部分。 通过这次项目&#xff0c;你可以了解&#xff1a; pytroch的模…