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

golang平滑重启库overseer实现原理

overseer主要完成了三部分功能:

1、连接的无损关闭,2、连接的平滑重启,3、文件变更的自动重启。

下面依次讲一下:

一、连接的无损关闭

golang官方的net包是不支持连接的无损关闭的,当主监听协程退出时,并不会等待各个实际work协程的处理完成。

以下是golang官方代码:

Go/src/net/http/server.go

func (srv *Server) Serve(l net.Listener) error {if fn := testHookServerServe; fn != nil {fn(srv, l) // call hook with unwrapped listener}origListener := ll = &onceCloseListener{Listener: l}defer l.Close()if err := srv.setupHTTP2_Serve(); err != nil {return err}if !srv.trackListener(&l, true) {return ErrServerClosed}defer srv.trackListener(&l, false)baseCtx := context.Background()if srv.BaseContext != nil {baseCtx = srv.BaseContext(origListener)if baseCtx == nil {panic("BaseContext returned a nil context")}}var tempDelay time.Duration // how long to sleep on accept failurectx := context.WithValue(baseCtx, ServerContextKey, srv)for {rw, err := l.Accept()if err != nil {if srv.shuttingDown() {return ErrServerClosed}if ne, ok := err.(net.Error); ok && ne.Temporary() {if tempDelay == 0 {tempDelay = 5 * time.Millisecond} else {tempDelay *= 2}if max := 1 * time.Second; tempDelay > max {tempDelay = max}srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)time.Sleep(tempDelay)continue}return err}connCtx := ctxif cc := srv.ConnContext; cc != nil {connCtx = cc(connCtx, rw)if connCtx == nil {panic("ConnContext returned nil")}}tempDelay = 0c := srv.newConn(rw)c.setState(c.rwc, StateNew, runHooks) // before Serve can returngo c.serve(connCtx)}
}

当监听套接字关闭,l.Accept()退出循环时,并不会等待go c.serve(connCtx)协程的处理完成。

overseer的处理方式是,包装了golang的监听套接字和连接套接字,通过sync.WaitGroup提供了对主协程异步等待work协程处理完成的支持。

overseer代码如下:

overseer-v1.1.6\graceful.go

func (l *overseerListener) Accept() (net.Conn, error) {conn, err := l.Listener.(*net.TCPListener).AcceptTCP()if err != nil {return nil, err}conn.SetKeepAlive(true)                  // see http.tcpKeepAliveListenerconn.SetKeepAlivePeriod(3 * time.Minute) // see http.tcpKeepAliveListeneruconn := overseerConn{Conn:   conn,wg:     &l.wg,closed: make(chan bool),}go func() {//connection watcherselect {case <-l.closeByForce:uconn.Close()case <-uconn.closed://closed manually}}()l.wg.Add(1)return uconn, nil
}//non-blocking trigger close
func (l *overseerListener) release(timeout time.Duration) {//stop accepting connections - release fdl.closeError = l.Listener.Close()//start timer, close by force if deadline not metwaited := make(chan bool)go func() {l.wg.Wait()waited <- true}()go func() {select {case <-time.After(timeout):close(l.closeByForce)case <-waited://no need to force close}}()
}//blocking wait for close
func (l *overseerListener) Close() error {l.wg.Wait()return l.closeError
}func (o overseerConn) Close() error {err := o.Conn.Close()if err == nil {o.wg.Done()o.closed <- true}return err
}

在(l *overseerListener) Accept函数中,每生成一个work连接,执行l.wg.Add(1),在(o overseerConn) Close函数中,每关闭一个work连接,执行o.wg.Done()。

在异步关闭模式(l *overseerListener) release函数中和在同步关闭模式(l *overseerListener) Close函数中都会调用l.wg.Wait()以等待work协程的处理完成。

监听套接字关闭流程:

1、work进程收到重启信号,或者master进程收到重启信号然后转发到work进程。

2、work进程的信号处理里包含对(l *overseerListener) release的调用。

3、在(l *overseerListener) release里关闭监听套接字,并异步l.wg.Wait()。

4、在官方包net/http/server.go的 (srv *Server) Serve里l.Accept()出错返回,退出监听循环,然后执行defer l.Close(),即(l *overseerListener) Close。

5、在(l *overseerListener) Close里同步执行l.wg.Wait(),等待work连接处理完成。

6、work连接处理完成时,会调用(o overseerConn) Close(),进而调用o.wg.Done()。

7、所有work连接处理完成后,向master进程发送SIGUSR1信号。

8、master进程收到SIGUSR1信号后,将true写入mp.descriptorsReleased管道。

9、master进程的(mp *master) fork里,收到mp.descriptorsReleased后,结束本次fork,进入下一次fork。

二、连接的平滑重启

所谓平滑重启,就是重启不会造成客户端的断连,对客户端无感知,比如原有的排队连接不会被丢弃,所以监听套接字通过master进程在新旧work进程间传递,而不是新启的work进程重新创建监听连接。

监听套接字由master进程创建:

overseer-v1.1.6/proc_master.go

func (mp *master) retreiveFileDescriptors() error {mp.slaveExtraFiles = make([]*os.File, len(mp.Config.Addresses))for i, addr := range mp.Config.Addresses {a, err := net.ResolveTCPAddr("tcp", addr)if err != nil {return fmt.Errorf("Invalid address %s (%s)", addr, err)}l, err := net.ListenTCP("tcp", a)if err != nil {return err}f, err := l.File()if err != nil {return fmt.Errorf("Failed to retreive fd for: %s (%s)", addr, err)}if err := l.Close(); err != nil {return fmt.Errorf("Failed to close listener for: %s (%s)", addr, err)}mp.slaveExtraFiles[i] = f}return nil
}

从mp.Config.Addresses中拿到地址,建立监听连接,最后把文件句柄存入mp.slaveExtraFiles。

在这个过程中调用了(l *TCPListener) Close,但其实对work进程无影响,影响的只是master进程自己不能读写监听套接字。

这里引用下对网络套接字close和shutdown的区别:

close ---- 关闭本进程的socket id,但连接还是开着的,用这个socket id的其它进程还能用这个连接,能读或写这个socket id。
shutdown ---- 则破坏了socket 连接,读的时候可能侦探到EOF结束符,写的时候可能会收到一个SIGPIPE信号,这个信号可能直到socket buffer被填充了才收到,shutdown还有一个关闭方式的参数,0 不能再读,1不能再写,2 读写都不能。

将mp.slaveExtraFiles传递给子进程即work进程:

overseer-v1.1.6/proc_master.go

func (mp *master) fork() error {mp.debugf("starting %s", mp.binPath)cmd := exec.Command(mp.binPath)//mark this new process as the "active" slave process.//this process is assumed to be holding the socket files.mp.slaveCmd = cmdmp.slaveID++//provide the slave process with some statee := os.Environ()e = append(e, envBinID+"="+hex.EncodeToString(mp.binHash))e = append(e, envBinPath+"="+mp.binPath)e = append(e, envSlaveID+"="+strconv.Itoa(mp.slaveID))e = append(e, envIsSlave+"=1")e = append(e, envNumFDs+"="+strconv.Itoa(len(mp.slaveExtraFiles)))cmd.Env = e//inherit master args/stdfilescmd.Args = os.Argscmd.Stdin = os.Stdincmd.Stdout = os.Stdoutcmd.Stderr = os.Stderr//include socket filescmd.ExtraFiles = mp.slaveExtraFilesif err := cmd.Start(); err != nil {return fmt.Errorf("Failed to start slave process: %s", err)}//was scheduled to restart, notify successif mp.restarting {mp.restartedAt = time.Now()mp.restarting = falsemp.restarted <- true}//convert wait into channelcmdwait := make(chan error)go func() {cmdwait <- cmd.Wait()}()//wait....select {case err := <-cmdwait://program exited before releasing descriptors//proxy exit code out to mastercode := 0if err != nil {code = 1if exiterr, ok := err.(*exec.ExitError); ok {if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {code = status.ExitStatus()}}}mp.debugf("prog exited with %d", code)//if a restarts are disabled or if it was an//unexpected crash, proxy this exit straight//through to the main processif mp.NoRestart || !mp.restarting {os.Exit(code)}case <-mp.descriptorsReleased://if descriptors are released, the program//has yielded control of its sockets and//a parallel instance of the program can be//started safely. it should serve state.Listeners//to ensure downtime is kept at <1sec. The previous//cmd.Wait() will still be consumed though the//result will be discarded.}return nil
}

通过cmd.ExtraFiles = mp.slaveExtraFiles语句向子进程传递套接字,这个参数最终传递给fork系统调用,传递的fd会被子进程继承。

子进程即work进程处理继承的套接字:

overseer-v1.1.6/proc_slave.go

func (sp *slave) run() error {sp.id = os.Getenv(envSlaveID)sp.debugf("run")sp.state.Enabled = truesp.state.ID = os.Getenv(envBinID)sp.state.StartedAt = time.Now()sp.state.Address = sp.Config.Addresssp.state.Addresses = sp.Config.Addressessp.state.GracefulShutdown = make(chan bool, 1)sp.state.BinPath = os.Getenv(envBinPath)if err := sp.watchParent(); err != nil {return err}if err := sp.initFileDescriptors(); err != nil {return err}sp.watchSignal()//run program with statesp.debugf("start program")sp.Config.Program(sp.state)return nil
}func (sp *slave) initFileDescriptors() error {//inspect file descriptorsnumFDs, err := strconv.Atoi(os.Getenv(envNumFDs))if err != nil {return fmt.Errorf("invalid %s integer", envNumFDs)}sp.listeners = make([]*overseerListener, numFDs)sp.state.Listeners = make([]net.Listener, numFDs)for i := 0; i < numFDs; i++ {f := os.NewFile(uintptr(3+i), "")l, err := net.FileListener(f)if err != nil {return fmt.Errorf("failed to inherit file descriptor: %d", i)}u := newOverseerListener(l)sp.listeners[i] = usp.state.Listeners[i] = u}if len(sp.state.Listeners) > 0 {sp.state.Listener = sp.state.Listeners[0]}return nil
}

子进程只是重新包装套接字,并没有新建监听连接,包装成u := newOverseerListener(l)类型,这些监听套接字最后传递给sp.Config.Program(sp.state),即用户的启动程序:

overseer-v1.1.6/example/main.go

// convert your 'main()' into a 'prog(state)'
// 'prog()' is run in a child process
func prog(state overseer.State) {fmt.Printf("app#%s (%s) listening...\n", BuildID, state.ID)http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {d, _ := time.ParseDuration(r.URL.Query().Get("d"))time.Sleep(d)fmt.Fprintf(w, "app#%s (%s) %v says hello\n", BuildID, state.ID, state.StartedAt)}))http.Serve(state.Listener, nil)fmt.Printf("app#%s (%s) exiting...\n", BuildID, state.ID)
}// then create another 'main' which runs the upgrades
// 'main()' is run in the initial process
func main() {overseer.Run(overseer.Config{Program:          prog,Address:          ":5001",Fetcher:          &fetcher.File{Path: "my_app_next"},Debug:            true, //display log of overseer actionsTerminateTimeout: 10 * time.Minute,})
}

在用户程序中http.Serve(state.Listener, nil)调用:

1、使用的accept方式是包装后的(l *overseerListener) Accept()。

2、defer l.Close()使用也是包装后的(l *overseerListener) Close()。

3、由(l *overseerListener) Accept()创建的work连接也都包装成了overseerConn连接,在关闭时会调用(o overseerConn) Close()

三、文件变更的自动重启

能够自动监视文件变化,有变更时自动触发重启流程。

在master进程启动时检查配置,如果设置了mp.Config.Fetcher则进入fetchLoop:

overseer-v1.1.6/proc_master.go

// fetchLoop is run in a goroutine
func (mp *master) fetchLoop() {min := mp.Config.MinFetchIntervaltime.Sleep(min)for {t0 := time.Now()mp.fetch()//duration fetch of fetchdiff := time.Now().Sub(t0)if diff < min {delay := min - diff//ensures at least MinFetchInterval delay.//should be throttled by the fetcher!time.Sleep(delay)}}
}

mp.Config.MinFetchInterval默认是1秒,也就是每秒检查一次变更。time.Duration类型,可以设置更小的粒度。

已经支持的fetcher包括:fetcher_file.go、fetcher_github.go、fetcher_http.go、fetcher_s3.go。

以fetcher_file.go为例说明。

1、文件变更的判断:

overseer-v1.1.6/proc_master.go

	//tee off to sha1hash := sha1.New()reader = io.TeeReader(reader, hash)//write to a temp file_, err = io.Copy(tmpBin, reader)if err != nil {mp.warnf("failed to write temp binary: %s", err)return}//compare hashnewHash := hash.Sum(nil)if bytes.Equal(mp.binHash, newHash) {mp.debugf("hash match - skip")return}

通过sha1算法实现,比较新旧hash值,并没有关注文件时间戳。

2、验证是可执行文件,且是支持overseer的:

overseer-v1.1.6/proc_master.go

	tokenIn := token()cmd := exec.Command(tmpBinPath)cmd.Env = append(os.Environ(), []string{envBinCheck + "=" + tokenIn}...)cmd.Args = os.Argsreturned := falsego func() {time.Sleep(5 * time.Second)if !returned {mp.warnf("sanity check against fetched executable timed-out, check overseer is running")if cmd.Process != nil {cmd.Process.Kill()}}}()tokenOut, err := cmd.CombinedOutput()returned = trueif err != nil {mp.warnf("failed to run temp binary: %s (%s) output \"%s\"", err, tmpBinPath, tokenOut)return}if tokenIn != string(tokenOut) {mp.warnf("sanity check failed")return}

这是通过overseer预埋的代码实现的:

overseer-v1.1.6/overseer.go

//sanityCheck returns true if a check was performed
func sanityCheck() bool {//sanity checkif token := os.Getenv(envBinCheck); token != "" {fmt.Fprint(os.Stdout, token)return true}//legacy sanity check using old env varif token := os.Getenv(envBinCheckLegacy); token != "" {fmt.Fprint(os.Stdout, token)return true}return false
}

这段代码在main启动时在overseer.Run里会调用到,传递固定的环境变量,然后命令行输出会原样显示出来即为成功。

3、覆盖旧文件,并触发重启。

overseer-v1.1.6/proc_master.go

	//overwrite!if err := overwrite(mp.binPath, tmpBinPath); err != nil {mp.warnf("failed to overwrite binary: %s", err)return}mp.debugf("upgraded binary (%x -> %x)", mp.binHash[:12], newHash[:12])mp.binHash = newHash//binary successfully replacedif !mp.Config.NoRestartAfterFetch {mp.triggerRestart()}

由(mp *master) triggerRestart进入重启流程:

overseer-v1.1.6/proc_master.go

func (mp *master) triggerRestart() {if mp.restarting {mp.debugf("already graceful restarting")return //skip} else if mp.slaveCmd == nil || mp.restarting {mp.debugf("no slave process")return //skip}mp.debugf("graceful restart triggered")mp.restarting = truemp.awaitingUSR1 = truemp.signalledAt = time.Now()mp.sendSignal(mp.Config.RestartSignal) //ask nicely to terminateselect {case <-mp.restarted://successmp.debugf("restart success")case <-time.After(mp.TerminateTimeout)://times up mr. process, we did ask nicely!mp.debugf("graceful timeout, forcing exit")mp.sendSignal(os.Kill)}
}

向子进程发送mp.Config.RestartSignal信号,子进程收到信号后,关闭监听套接字然后向父进程发送SIGUSR1信号:

overseer-v1.1.6/proc_slave.go

		if len(sp.listeners) > 0 {//perform graceful shutdownfor _, l := range sp.listeners {l.release(sp.Config.TerminateTimeout)}//signal release of held sockets, allows master to start//a new process before this child has actually exited.//early restarts not supported with restarts disabled.if !sp.NoRestart {sp.masterProc.Signal(SIGUSR1)}//listeners should be waiting on connections to close...}

父进程收到SIGUSR1信号后,通知mp.descriptorsReleased管道监听套接字已经关闭:

overseer-v1.1.6/proc_master.go

	//**during a restart** a SIGUSR1 signals//to the master process that, the file//descriptors have been releasedif mp.awaitingUSR1 && s == SIGUSR1 {mp.debugf("signaled, sockets ready")mp.awaitingUSR1 = falsemp.descriptorsReleased <- true} else

最终回到(mp *master) fork函数,fork函数一直在等待mp.descriptorsReleased通知或者cmd.Wait子进程退出,收到管道通知后fork退出,进入下一轮fork循环。

overseer-v1.1.6/proc_master.go

func (mp *master) fork() error {//... ...//... ...//... ...//convert wait into channelcmdwait := make(chan error)go func() {cmdwait <- cmd.Wait()}()//wait....select {case err := <-cmdwait://program exited before releasing descriptors//proxy exit code out to mastercode := 0if err != nil {code = 1if exiterr, ok := err.(*exec.ExitError); ok {if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {code = status.ExitStatus()}}}mp.debugf("prog exited with %d", code)//if a restarts are disabled or if it was an//unexpected crash, proxy this exit straight//through to the main processif mp.NoRestart || !mp.restarting {os.Exit(code)}case <-mp.descriptorsReleased://if descriptors are released, the program//has yielded control of its sockets and//a parallel instance of the program can be//started safely. it should serve state.Listeners//to ensure downtime is kept at <1sec. The previous//cmd.Wait() will still be consumed though the//result will be discarded.}return nil
}

--end--

 

 

相关文章:

golang平滑重启库overseer实现原理

overseer主要完成了三部分功能&#xff1a; 1、连接的无损关闭&#xff0c;2、连接的平滑重启&#xff0c;3、文件变更的自动重启。 下面依次讲一下&#xff1a; 一、连接的无损关闭 golang官方的net包是不支持连接的无损关闭的&#xff0c;当主监听协程退出时&#xff0c;…...

用Python定义一个函数,用递归的方式模拟汉诺塔问题

【任务需求】 定义一个函数&#xff0c;用递归的方式模拟汉诺塔问题&#xff0c;三个柱子&#xff0c;分别为A、B、C&#xff0c;其中A柱子上有N个盘子&#xff0c;从小到大编号为1到N&#xff0c;盘子大小不同。现在要将这N个盘子从A柱子移动到C柱子上&#xff0c;但移动的过…...

二手的需求

案例1030 某天项目经理小王&#xff0c;从用户现场带回了需求&#xff0c;以图形的方式&#xff0c;交给了产品经理。告诉他就照这样设计&#xff0c;结果是项目经理放弃让产品经理出效果图。 原因是产品经理觉得项目经理带回来的需求有问题。项目经理解释产品经理不接受&…...

大厂面试题-JVM为什么使用元空间替换了永久代?

目录 面试解析 问题答案 面试解析 我们都知道Java8以及以后的版本中&#xff0c;JVM运行时数据区的结构都在慢慢调整和优化。但实际上这些变化&#xff0c;对于业务开发的小伙伴来说&#xff0c;没有任何影响。 因此我可以说&#xff0c;99%的人都回答不出这个问题。 但是…...

基本微信小程序的驾校宝典系统-驾照考试系统

项目介绍 系统模块分析是对系统的各个模块做出相应的说明以及解释。此系统的模块分别有用户模块、服务端模块和管理端模块这两大基本模块&#xff0c;其中服务端模块包括了首页、教练信息、教练咨讯、考试预约、我的等&#xff1b;而管理端模块则包括了个人中心、用户管理、教…...

02、SpringCloud -- Redis和Cookie过期时间刷新功能

目录 需求:代码流程过滤器类工具类过滤判断远程调用feign接口gitee 配置接口实现过滤器run方法测试:问题:秒杀功能完整分析图 需求: cookie应该写在网关中,网关中可以自定义filter过滤器,用来实现cookie的刷新和redis中key的刷新,延长用户的操作时间。 就是让用户每操…...

【报错】kali安装ngrok报错解决办法(zsh: exec format error: ./ngrok)

问题描述 kali安装ngrok令牌授权失败 在安装配置文件的时候报错&#xff1a;zsh: exec format error: ./ngrok 原因分析&#xff1a; 在Kali Linux上执行./ngrok时出现zsh exec格式错误的问题可能是由于未安装正确版本的ngrok或操作系统不兼容ngrok导致的。以下是一些可能的解…...

<学习笔记>从零开始自学Python-之-常用库篇(十三)内置小型数据库shelve

一、shelve简介&#xff1a; shelve是Python当中数据储存的方案&#xff0c;类似key-value数据库&#xff0c;便于保存Python对象&#xff0c;shelve只有一个open&#xff08;&#xff09;函数&#xff0c;用来打开指定的文件&#xff08;字典&#xff09;&#xff0c;会返回一…...

Redis快速上手篇七(集群-六台虚拟机)

Redis集群 主从复制的场景无法吗满足主机单点故障时需要引入集群配置 一般数据库要处理的读请求远大于写请求 &#xff0c;针对这种情况&#xff0c;我们优化数据库可以采用读写分离的策略。我们可以部 署一台主服务器主要用来处理写请求&#xff0c;部署多台从服务器 &#…...

LeetCode 301. 删除无效的括号【字符串,回溯或BFS】困难

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…...

面试经典159题——Day25

文章目录 一、题目二、题解 一、题目 125. Valid Palindrome A phrase is a palindrome if, after converting all uppercase letters into lowercase letters and removing all non-alphanumeric characters, it reads the same forward and backward. Alphanumeric charact…...

C# OpenCvSharp DNN 部署L2CS-Net人脸朝向估计

效果 项目 代码 using OpenCvSharp; using OpenCvSharp.Dnn; using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Text; using System.Windows.Forms;namespace OpenCvSharp_DNN_Demo …...

Windows环境下MosQuitto服务器搭建,安装mqtt服务端软件

1、下载、安装MosQuitto服务器 下载地址&#xff1a;http://mosquitto.org/files/binary/ 根据平台选择相应的代码下载。 安装完成后&#xff0c;安装文件夹下部分文件的功能...

web前端JS基础-----制作进度条

1&#xff0c;参考代码 <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title></head><body><progress id"pro" max"100" value"0"></progress><scrip…...

Linux命令解压多个tar.gz包

命令行解压单个tar.gz包&#xff1a; tar zxvf package.tar.gz 命令行解压多个tar.gz包&#xff1a; for f in *.tar.gz; do tar zxvf "$f"; done 这个命令会循环遍历当前目录下的所有tar.gz包&#xff0c;然后逐个解压。 注&#xff1a;如果想要解压到指定的目…...

Java基于SpringBoot+Vue的网上图书商城管理系统(附源码,教程)

文章目录 1. 简介2 技术栈3 系统功能4系统设计4.1数据库设计 5系统详细设计5.1系统功能模块5.1系统功能模块5.2管理员功能模块 源码下载地址 1. 简介 本次设计任务是要设计一个网上图书商城&#xff0c;通过这个系统能够满足网上图书商城的管理功能。系统的主要功能包括首页、…...

Visual Studio Code的下载与安装

Visual Studio Code&#xff08;简称 VS Code&#xff09;是由 Microsoft 开发的免费、开源的文本编辑器&#xff0c;适用于多种操作系统&#xff0c;包括 Windows、macOS 和 Linux。它的设计目标是成为一款轻量级、高效的代码编辑工具&#xff0c;同时提供丰富的扩展和功能&am…...

23种设计模式在SpringCloud源码里的应用

单例模式&#xff08;Singleton&#xff09;&#xff1a;Spring 中的 Bean 默认都是单例模式&#xff0c;保证在整个应用中只有一个实例。 工厂方法模式&#xff08;Factory Method&#xff09;&#xff1a;Spring 中的 BeanFactory 和 ApplicationContext 都实现了工厂方法模…...

几个精致的Linux命令

说到Linux命令&#xff0c;一些基础的简单的单个命令我就不说了&#xff0c;咱今天来点复杂的组合命令&#xff0c;比较长&#xff0c;但觉对很酷&#xff1a; 打印业务服务异常日志&#xff1a; tail -f business-service.log | grep -i exception --color 或者 grep --…...

CoDeSys系列-3、Windows运行时软PLC主站和p-net从站IO设备组网测试

CoDeSys系列-3、Windows运行时软PLC主站和p-net从站IO设备组网测试 文章目录 CoDeSys系列-3、Windows运行时软PLC主站和p-net从站IO设备组网测试一、前言二、Windows运行时软plc配置编程1、安装Windows下的运行时扩展包&#xff08;非必要&#xff09;2、创建项目2.1、创建标准…...

挑战杯推荐项目

“人工智能”创意赛 - 智能艺术创作助手&#xff1a;借助大模型技术&#xff0c;开发能根据用户输入的主题、风格等要求&#xff0c;生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用&#xff0c;帮助艺术家和创意爱好者激发创意、提高创作效率。 ​ - 个性化梦境…...

HTML 语义化

目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案&#xff1a; 语义化标签&#xff1a; <header>&#xff1a;页头<nav>&#xff1a;导航<main>&#xff1a;主要内容<article>&#x…...

Spark 之 入门讲解详细版(1)

1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室&#xff08;Algorithms, Machines, and People Lab&#xff09;开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目&#xff0c;8个月后成为Apache顶级项目&#xff0c;速度之快足见过人之处&…...

无法与IP建立连接,未能下载VSCode服务器

如题&#xff0c;在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈&#xff0c;发现是VSCode版本自动更新惹的祸&#xff01;&#xff01;&#xff01; 在VSCode的帮助->关于这里发现前几天VSCode自动更新了&#xff0c;我的版本号变成了1.100.3 才导致了远程连接出…...

java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别

UnsatisfiedLinkError 在对接硬件设备中&#xff0c;我们会遇到使用 java 调用 dll文件 的情况&#xff0c;此时大概率出现UnsatisfiedLinkError链接错误&#xff0c;原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用&#xff0c;结果 dll 未实现 JNI 协…...

Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)

参考官方文档&#xff1a;https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java&#xff08;供 Kotlin 使用&#xff09; 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...

排序算法总结(C++)

目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指&#xff1a;同样大小的样本 **&#xff08;同样大小的数据&#xff09;**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...

push [特殊字符] present

push &#x1f19a; present 前言present和dismiss特点代码演示 push和pop特点代码演示 前言 在 iOS 开发中&#xff0c;push 和 present 是两种不同的视图控制器切换方式&#xff0c;它们有着显著的区别。 present和dismiss 特点 在当前控制器上方新建视图层级需要手动调用…...

OD 算法题 B卷【正整数到Excel编号之间的转换】

文章目录 正整数到Excel编号之间的转换 正整数到Excel编号之间的转换 excel的列编号是这样的&#xff1a;a b c … z aa ab ac… az ba bb bc…yz za zb zc …zz aaa aab aac…; 分别代表以下的编号1 2 3 … 26 27 28 29… 52 53 54 55… 676 677 678 679 … 702 703 704 705;…...

Modbus RTU与Modbus TCP详解指南

目录 1. Modbus协议基础 1.1 什么是Modbus? 1.2 Modbus协议历史 1.3 Modbus协议族 1.4 Modbus通信模型 🎭 主从架构 🔄 请求响应模式 2. Modbus RTU详解 2.1 RTU是什么? 2.2 RTU物理层 🔌 连接方式 ⚡ 通信参数 2.3 RTU数据帧格式 📦 帧结构详解 🔍…...