媒介
SSH 全称为 Secure Shell,是一种用于安全地远程登录到网络上的其他盘算机的网络协议。相信做运维的同砚没有不了解 SSH的,比较常用的登录服务器的 shell 工具例如 Xshell、SecureCRT、iTerm2 等都是基于 SSH 协议实现的。Golang 中的的 crypto/ssh 库提供了实现 SSH 客户端的功能,本文接下来详细讲解下怎样使用 Golang 实现操作 SSH 客户端,为后续运维开发的门路上使用golang编写脚本先夯实一下基础
一、了解SSH
- 在Golang中,有几个常用的SSH库,如golang.org/x/crypto/ssh和github.com/go-ssh/ssh。
- 本次将重点介绍golang.org/x/crypto/ssh,因为它是由Go官方维护的.
- SSH库功能分类:
- SSH客户端: 允许用户通过SSH协议连接到远程服务器。
- SSH服务器: 允许远程用户通过SSH协议连接到本地服务器。
- 命令执行: 在远程服务器上执行命令。
- 文件传输: 在本地和远程服务器之间传输文件。
- 交会时会话: 类比xshell,当代码执行后,如同在操作真实的xshell一样
复制代码 二、紧张知识点
1.安装ssh库
代码如下(示例):
- go get golang.org/x/crypto/ssh
复制代码 2.ssh库紧张知识牢记
联合演示代码一起更好明确
如下(示例):
- 1、client 对象(SSH 客户端)在整个程序中只创建一次
- 2、可以通过 client.NewSession() 多次创建多个 session 对象.每个 session 是一个独立的会话,每次执行命令时都会创建一个新的会话
- 3、每次 session.Run() 或 session.Start() 执行命令时,都会用新的会话来执行不同的命令
- 这些会话共享底层的 SSH 连接,但是它们独立执行命令
-
- 4、当某个会话的命令执行完毕,必须调用session.Close() 释放相关资源。
- 5、切记不能在同一个 session 上并行执行多个命令。如果需要并行执行多个命令,应该创建多个 session
复制代码 演示代码(示例):
- package main
- import (
- "fmt"
- "golang.org/x/crypto/ssh"
- "log"
- )
- func main() {
- // SSH 配置
- config := &ssh.ClientConfig{
- User: "root", // 替换为远程服务器的用户名
- Auth: []ssh.AuthMethod{
- ssh.Password("1"), // 替换为远程服务器密码
- },
- HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 忽略主机密钥验证
- }
- // 连接远程服务器
- client, err := ssh.Dial("tcp", "192.168.56.160:22", config) // 替换为远程服务器的IP地址
- if err != nil {
- log.Fatalf("Failed to dial: %v", err)
- }
- defer client.Close()
- // 创建第一个会话
- session1, err := client.NewSession()
- if err != nil {
- log.Fatalf("Failed to create session 1: %v", err)
- }
- defer session1.Close()
- // 执行第一个命令
- fmt.Println("Executing command on session 1-1")
- err = session1.Run("echo Hello from session 1-1")
- if err != nil {
- log.Fatalf("Failed to run command on session 1-1: %v", err)
- }
- // 演示在第一个会话中执行第二个命令看是否能成功
- fmt.Println("Executing command on session 1-2")
- err = session1.Run("echo Hello from session 1-2")
- if err != nil {
- log.Fatalf("Failed to run command on session 1-2: %v", err)
- }
-
- // 创建第二个会话
- session2, err := client.NewSession()
- if err != nil {
- log.Fatalf("Failed to create session 2: %v", err)
- }
- defer session2.Close()
- // 执行第二个命令
- fmt.Println("Executing command on session 2")
- err = session2.Run("echo Hello from session 2")
- if err != nil {
- log.Fatalf("Failed to run command on session 2: %v", err)
- }
- // 创建第三个会话
- session3, err := client.NewSession()
- if err != nil {
- log.Fatalf("Failed to create session 3: %v", err)
- }
- defer session3.Close()
- // 执行第三个命令
- fmt.Println("Executing command on session 3")
- err = session3.Run("echo Hello from session 3")
- if err != nil {
- log.Fatalf("Failed to run command on session 3: %v", err)
- }
- fmt.Println("All commands executed successfully")
- }
复制代码 执行这段代码,返回如下所示,在同一个会话下并行的运行两条下令,发现运行失败
当将1-2这段代码注释掉后,再次运行代码可以成功运行,跟上述的描述一致
三、模拟连接远程服务器并执行下令
演示怎么在golang中使用SSH库连接服务器并执行相应的linux下令
- package main
- import (
- "golang.org/x/crypto/ssh"
- "log"
- )
- func main() {
- // 创建SSH配置--密码认证
- config := &ssh.ClientConfig{
- User: "root",
- Auth: []ssh.AuthMethod{
- ssh.Password("1"), //密码认证
- },
- HostKeyCallback: ssh.InsecureIgnoreHostKey(),
- }
- // 创建SSH配置--SSH密钥认证(生产环境下建议采用该方式) 二选一即可
- //config := &ssh.ClientConfig{
- //User: "username",
- //Auth: []ssh.AuthMethod{
- // ssh.PublicKeysFromFile("path/to/private/key", "path/to/public/key"),
- //},
- // HostKeyCallback: ssh.FixedHostKey(hostKey),
- //}
-
- // 连接到远程服务器,并返回一个ssh客户端实例,
- /*
- 返回值类型:
- *ssh.Client
- error
- */
- client, err := ssh.Dial("tcp", "192.168.56.160:22", config)
- if err != nil {
- log.Fatalf("Failed to dial: %v", err)
- }
- defer client.Close()
- // 使用客户端创建一个ssh会话
- session, err := client.NewSession()
- if err != nil {
- log.Fatalf("Failed to create session: %v", err)
- }
- defer session.Close()
- // 在ssh会话中执行命令并输出命令结果
- out, err := session.CombinedOutput("ls /export")
- if err != nil {
- log.Fatalf("Failed to run: %v", err)
- }
- log.Printf("%s", out)
- }
复制代码
四、SSH与os/exec标准库下执行下令的几种方式对比
方法功能描述阻塞/非阻塞输出捕获使用场景cmd:=exec.Command(“xx”,“x”)
err:=cmd.Run()执行本地下令并等候下令完成,返回错误阻塞不捕获输出(需用 Output/CombinedOutput 捕获)本地下令执行,等候下令完成err:=newsession.Run("xxx")执行远程下令并等候下令完成,返回错误阻塞不捕获输出(需手动捕获)远程 SSH 下令执行,等候完成cmd:=exec.Command(“xx”,“xx”)
cmd.Start()启动本地下令异步执行,不等候下令完成非阻塞,假如要阻塞,使用exec.Command().Wait()实现可通过 Stdout、Stderr 获取输出本地下令异步执行,非阻塞err:=newsession.Start("xx")启动远程下令异步执行,不等候下令完成非阻塞,适用于需要启动后台进程的场景,假如要阻塞使用,newsession.Wait()实现可通过 Stdout、Stderr 获取输出远程下令异步执行,非阻塞cmd:=exec.Command(“xx”,“x”)
out,err:=cmd.CombinedOutput()执行本地下令并捕获标准输出和标准错误的合并输出阻塞捕获标准输出和标准错误的合并输出本地下令执行,捕获全部输出out,err:=newsession.CombinedOutput("xx")执行远程下令并捕获标准输出和标准错误的合并输出阻塞捕获标准输出和标准错误的合并输出远程下令执行,捕获全部输出 五、SSH库下三种执行下令方式演示
5.1. session.CombinedOutput()示例
连接192.168.56.160服务器,并执行ls /var/log/下令查看目录下的文件
- 注意事项:
- 1、CombinedOutput()函数剖析
- func (s *ssh.Session) CombinedOutput(cmd string) ([]byte, error)
- 接收参数类型 string
- 返回值类型[]byte,error
- 将[]byte转换为string类型输出的结果为命令的执行结果
-
- 2、在一个session会话中执行多条命令的操作
- 将多条命令保存在切片中,然后for循环将命令(value)传递给CombinedOutput()函数即可
- // 示例命令
- commands := []string{"ls -l /tmp", "uptime", "df -h"}
- for _, command := range commands {
- executeCommand(client, command, ip, resultChan, &mu)
- }
- out, err := session.CombinedOutput(commands)
复制代码- package main
- import (
- "golang.org/x/crypto/ssh"
- "log"
- )
- func main() {
- // 创建SSH配置--密码认证
- config := &ssh.ClientConfig{
- User: "root",
- Auth: []ssh.AuthMethod{
- ssh.Password("1"), //密码认证
- },
- HostKeyCallback: ssh.InsecureIgnoreHostKey(),
- }
- // 连接到远程服务器,并返回一个ssh客户端实例
- client, err := ssh.Dial("tcp", "192.168.56.160:22", config)
- if err != nil {
- log.Fatalf("Failed to dial: %v", err)
- }
- defer client.Close()
- // 使用客户端创建一个ssh会话
- session, err := client.NewSession()
- if err != nil {
- log.Fatalf("Failed to create session: %v", err)
- }
- defer session.Close()
- // 在ssh会话中执行命令并输出命令结果。
- out, err := session.CombinedOutput("ls /var/log/")
- if err != nil {
- log.Fatalf("Failed to run: %v", err)
- }
- log.Printf("out:%s\n", out)
- }
复制代码
5.2. session.Run()示例
- 注意事项:
- session.Run(cmd string )error
- func (s *ssh.Session) Run(cmd string) error
- 接收参数类型 string
- 返回类型 error
-
- <如果要想获取到执行的结果和错误,即区分标准输出和标准错误,则使用下方的方法>
复制代码- package main
- import (
- "bytes"
- "fmt"
- "log"
- "golang.org/x/crypto/ssh"
- )
- // setupSSHClient 配置并返回一个SSH客户端
- func setupSSHClient(user, password, host string, port int) (*ssh.Client, error) {
- config := &ssh.ClientConfig{
- User: user,
- Auth: []ssh.AuthMethod{
- ssh.Password(password),
- },
- HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 注意:这里使用了不安全的回调,仅用于示例。在实际应用中,你应该验证主机密钥。
- }
- client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", host, port), config)
- if err != nil {
- return nil, err
- }
- return client, nil
- }
- func main() {
- user := "root"
- password := "1"
- host := "192.168.56.162"
- port := 22 // 默认SSH端口是22
- client, err := setupSSHClient(user, password, host, port)
- if err != nil {
- log.Fatalf("Failed to setup SSH client: %v", err)
- }
- defer client.Close()
- if host == "192.168.56.162" {
- newsession, _ := client.NewSession()
- defer newsession.Close()
- //Run()
- // 创建一个缓冲区来捕获命令的输出
- var outputBuf bytes.Buffer
- // 将标准输出和标准错误都重定向到同一个缓冲区
- newsession.Stdout = &outputBuf
- newsession.Stderr = &outputBuf
- err := newsession.Run("ls /var/log/audit/")
- if err != nil {
- // 输出执行命令时的错误
- fmt.Printf("Error executing command: %v\n", err)
- }
- // 打印命令的输出(包括标准输出和标准错误)
- fmt.Printf("Command output:\n%s\n", outputBuf.String())
- }
- }
复制代码
5.3. session.Start()、session.Wait()示例
- 注意事项:
- func (s *ssh.Session) Start(cmd string) error
- 接收参数类型 string
- 返回类型 error
- 如果要想获取到执行的结果和错误,即区分标准输出和标准错误,则使用下方的方法
- func (s *ssh.Session) Wait() error
- 返回类型 error
- 等待
-
- session.Start() 单独使用时,命令会在后台执行,程序不会等待命令的完成,立即继续执行后续代码。
- session.Start() 和 session.Wait() 一起使用时,程序会在 Wait() 处等待命令执行完成,之后才会继续执行后续的代码。
复制代码- package main
- import (
- "fmt"
- "golang.org/x/crypto/ssh"
- "log"
- "time"
- )
- func main() {
- // SSH 配置
- config := &ssh.ClientConfig{
- User: "root", // 替换为远程服务器的用户名
- Auth: []ssh.AuthMethod{
- ssh.Password("1"), // 替换为远程服务器密码
- },
- HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 忽略主机密钥验证
- }
- // 连接远程服务器
- client, err := ssh.Dial("tcp", "192.168.56.160:22", config) // 替换为远程服务器的IP地址
- if err != nil {
- log.Fatalf("Failed to dial: %v", err)
- }
- defer client.Close()
- // 创建会话
- session, err := client.NewSession()
- if err != nil {
- log.Fatalf("Failed to create session: %v", err)
- }
- defer session.Close()
- // 示例 1:使用 session.Start() 启动命令,但不等待
- fmt.Println("=== 示例 1: 使用 session.Start() 启动命令,不等待 ===")
- err = session.Start("sleep 5") // 启动一个后台命令
- if err != nil {
- log.Fatalf("Failed to start command: %v", err)
- }
- // 程序不会等待 sleep 5 执行完成,立即继续执行下一行
- fmt.Println("命令已启动,程序继续执行,不等待命令结束")
- // 等待一段时间,观察命令是否执行完
- time.Sleep(2 * time.Second)
- fmt.Println("程序在等待2秒后继续执行。")
- // 示例 2:使用 session.Start() 启动命令,并等待命令执行完毕
- // 创建新的会话用于第二个命令
- session2, err := client.NewSession()
- if err != nil {
- log.Fatalf("Failed to create session for second command: %v", err)
- }
- defer session2.Close()
- fmt.Println("\n=== 示例 2: 使用 session.Start() 启动命令,并调用 session.Wait() 等待 ===")
- err = session2.Start("sleep 5") // 启动一个后台命令
- if err != nil {
- log.Fatalf("Failed to start second command: %v", err)
- }
- // 程序会在这里等待命令执行完成
- err = session2.Wait() // 等待命令完成
- if err != nil {
- log.Fatalf("Failed to wait for command to finish: %v", err)
- }
- fmt.Println("命令执行完成,程序继续执行")
- // 结束
- fmt.Println("\n所有命令已执行完毕")
- }
复制代码
六、两种捕获标准输出和标准错误的方法:StdoutPipe / StderrPipe 和 session.Stdout / session.Stderr之间的区别
6.1. 使用 StdoutPipe 和 StderrPipe捕获标准输出和标准错误
紧张代码示例
- // 获取 标准输出和标准错误
- stdout, _ := session.StdoutPipe()
- output := make([]byte, 1024)
- for {
- n, err := stdout.Read(output)
- if err != nil {
- break
- }
- fmt.Sprintf("STDOUT from %s: %s", ip, string(output[:n]))
- }
- stderr, err := session.StderrPipe()
- output := make([]byte, 1024)
- for {
- n, err := stderr.Read(output)
- if err != nil {
- break
- }
- fmt.Sprintf("STDERR from %s: %s", ip, string(output[:n]))
- }
复制代码 解释
- 1. 使用 `StdoutPipe` 和 `StderrPipe`:
- - `StdoutPipe()` 和 `StderrPipe()` 返回一个 `io.Reader`,可以用来读取远程命令的标准输出(stdout)和标准错误输出(stderr)
- - 可以通过从这些管道中读取数据来获取命令的输出,通常会使用协程来异步读取这些管道中的数据
- 2. 工作原理:
- - 首先通过 `session.StdoutPipe()` 和 `session.StderrPipe()` 获取输出的管道(`io.Reader`)
- - 然后在程序中手动读取这些管道的内容,通常通过 `io.Copy` 或者 `bufio.Reader` 来处理流。
- - 这种方式适用于需要处理较大输出或需要实时读取命令输出的场景。
-
- 3. 优点:
- - 可以实时读取输出,因为管道是持续开放的,适合需要处理大量数据或逐行输出的情况。
- - 可以分别处理标准输出和标准错误,提供更多灵活性。
- 4. 缺点:
- - 需要异步读取标准输出和标准错误,可能需要更多的代码来确保并发处理和同步。
- - 适用于需要实时处理输出的场景,不适合简单的命令输出捕获。
复制代码 6.2. 使用 重定向 session.Stdout 和 session.Stderr 捕获标准输出和标准错误
紧张代码示例
- ....
- // 创建一个缓冲区来捕获命令的输出
- var outputBuf bytes.Buffer
- // 将标准输出和标准错误都重定向到同一个缓冲区
- session.Stdout = &outputBuf
- session.Stderr = &outputBuf
- err := newsession.Run("ls /var/log/audit/")
- if err != nil {
- // 输出执行命令时的错误
- fmt.Printf("Error executing command: %v\n", err)
- }
- // 打印命令的输出(包括标准输出和标准错误)
- fmt.Printf("Command output:\n%s\n", outputBuf.String())
- ...
复制代码 解释
- 1. 使用 `newsession.Stdout` 和 `newsession.Stderr`:
- - `session.Stdout` 和 `session.Stderr` 分别是 `io.Writer` 类型,允许将命令的标准输出和标准错误直接写入一个缓冲区(如 `bytes.Buffer`)。
- - 可以通过 `outputBuf.String()` 获取完整的命令输出。这里,`Stdout` 和 `Stderr` 都被重定向到同一个 `bytes.Buffer`,
- - 这样就能捕获命令的所有输出(无论是标准输出还是标准错误)。
-
- 2. 工作原理:
- - `session.Run()` 会直接执行命令并把标准输出和标准错误都写入到指定的缓冲区。
- - 不需要异步读取输出,命令执行完成后,只需要读取 `outputBuf` 即可获取所有输出。
-
- 3. 优点:
- - 代码简单,易于实现,适合捕获简单的命令输出。
- - 不需要显式地管理异步读取标准输出和错误流,适用于不需要实时处理输出的场景。
- - 适合于简单的任务(例如调试、输出日志等)并且输出数据量较小的情况。
- 4. 缺点:
- - 如果命令输出量大或者需要实时处理输出,可能会遇到缓冲区的限制或延迟。
- - 不能实时读取输出,必须等命令执行完毕才能获取所有输出。
复制代码 6.3.两种方式的区别
- 1. 实时性:
- - `StdoutPipe` 和 `StderrPipe`:
- 适合实时读取标准输出和标准错误。可以在命令执行的过程中动态处理输出数据。
- - `Stdout` 和 `Stderr`:
- 适合捕获命令执行后的完整输出,并不实时读取。如果需要完整的命令输出,一次性获取比较简单。
- 2. 使用场景:
- - `StdoutPipe` 和 `StderrPipe`:
- 适合输出较大、需要流式处理的场景,比如你需要逐行读取或实时处理命令输出的场景。
- - `Stdout` 和 `Stderr`:
- 适合捕获命令的完整输出并一次性处理,代码简单,适合小规模的输出捕获。
- 3. 复杂性:
- - `StdoutPipe` 和 `StderrPipe`:
- 稍微复杂,因为需要处理并发读取输出流,可能涉及协程。
- - `Stdout` 和 `Stderr`:
- 简单易懂,适合不需要实时读取输出的情况。
- 根据实际需求,可以选择适合的方式:
- 如果需要并发处理或实时处理输出流,使用 `StdoutPipe` 和 `StderrPipe`
- 如果需要一次性获取完整输出,使用 `Stdout` 和 `Stderr` 会更加简洁。
复制代码 七、示例: 连接到多台服务器并执行多个下令返回下令执行结果
先看代码再分析
- package main
- import (
- "bufio"
- "fmt"
- "log"
- "os"
- "strings"
- "sync"
- "golang.org/x/crypto/ssh"
- )
- func executeCommand(client *ssh.Client, command string, ip string, resultChan chan<- string, mu *sync.Mutex) {
- // 创建一个新的 SSH 会话
- session, err := client.NewSession()
- if err != nil {
- log.Println("Failed to create session:", err)
- resultChan <- fmt.Sprintf("Error on %s: Failed to create session", ip)
- return
- }
- defer session.Close()
- // 获取 Stdout 和 Stderr 输出
- stdout, err := session.StdoutPipe()
- if err != nil {
- log.Println("Failed to get StdoutPipe:", err)
- resultChan <- fmt.Sprintf("Error on %s: Failed to get StdoutPipe", ip)
- return
- }
- stderr, err := session.StderrPipe()
- if err != nil {
- log.Println("Failed to get StderrPipe:", err)
- resultChan <- fmt.Sprintf("Error on %s: Failed to get StderrPipe", ip)
- return
- }
- // 启动命令
- err = session.Start(command)
- if err != nil {
- log.Println("Failed to start command:", err)
- resultChan <- fmt.Sprintf("Error on %s: Failed to start command", ip)
- return
- }
- // 使用锁来确保对共享资源(如输出的打印)是串行的
- mu.Lock()
- defer mu.Unlock()
- // 读取命令输出并打印到管道
- go func() {
- output := make([]byte, 1024)
- for {
- n, err := stdout.Read(output)
- if err != nil {
- break
- }
- resultChan <- fmt.Sprintf("STDOUT from %s: %s", ip, string(output[:n]))
- }
- }()
- go func() {
- output := make([]byte, 1024)
- for {
- n, err := stderr.Read(output)
- if err != nil {
- break
- }
- resultChan <- fmt.Sprintf("STDERR from %s: %s", ip, string(output[:n]))
- }
- }()
- // 等待命令执行完毕
- err = session.Wait()
- if err != nil {
- log.Println("Error executing command:", err)
- resultChan <- fmt.Sprintf("Error on %s: %v", ip, err)
- } else {
- resultChan <- fmt.Sprintf("Command executed successfully on %s", ip)
- }
- }
- func main() {
- // 加载 IP 地址文件
- file, err := os.Open("/export/test/ips.txt")
- if err != nil {
- log.Fatal("Failed to open file:", err)
- }
- defer file.Close()
- // 读取 IP 地址
- var ips []string
- scanner := bufio.NewScanner(file)
- for scanner.Scan() {
- ip := strings.TrimSpace(scanner.Text())
- if ip != "" {
- ips = append(ips, ip)
- }
- }
- if err := scanner.Err(); err != nil {
- log.Fatal("Failed to read file:", err)
- }
- // 设置 SSH 客户端配置,使用密码认证
- sshConfig := &ssh.ClientConfig{
- User: "root", // SSH 用户名
- Auth: []ssh.AuthMethod{
- ssh.Password("1"), // 密码认证
- },
- HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 注意:生产环境中不建议使用此选项
- }
- // 创建一个管道用于接收结果
- resultChan := make(chan string, len(ips)*3) // 每台机器执行多个命令,调整管道容量
- var wg sync.WaitGroup
- var mu sync.Mutex // 创建锁
- // 遍历 IP 地址,并为每个 IP 地址启动一个 goroutine
- for _, ip := range ips {
- wg.Add(1)
- go func(ip string) {
- defer wg.Done()
- // 建立 SSH 连接
- client, err := ssh.Dial("tcp", ip+":22", sshConfig)
- if err != nil {
- log.Printf("Failed to connect to %s: %v", ip, err)
- resultChan <- fmt.Sprintf("Failed to connect to %s", ip)
- return
- }
- defer client.Close()
- // 对每台机器执行多个命令
- commands := []string{"ls -l /tmp", "uptime", "df -h"} // 示例命令
- for _, command := range commands {
- executeCommand(client, command, ip, resultChan, &mu)
- }
- }(ip)
- }
- // 在所有任务完成之后关闭 resultChan
- go func() {
- wg.Wait()
- close(resultChan)
- }()
- // 输出所有结果
- for result := range resultChan {
- fmt.Println(result)
- }
- }
复制代码
- 涉及到的知识点:
- 1、管道
- 2、互斥锁
- 3、goroutine并发
- 4、SSH
- 5、session.Start/Wait
- 6、分开捕获标准输出和标准错误
- 7、按行读取文件内容
-
- 上述代码示例演示了如何在多台机器上并发执行多个命令,并使用 sync.Mutex 来保护共享资源(如管道)的访问
- 具体流程:
- 1、从文件中按行读取IP并保存到切片ips中
-
- 2、设置ssh配置,从管道中读取IP,将每个服务器连接和每个要执行的命令都放在一个 goroutine中。
- 主程序继续启动新的 goroutine 执行任务,而不会因为某一台服务器的命令执行而导致整个程序阻塞
-
- 3、将连接信息和捕获的标准输出和标准错误信息都写入到管道中
-
- 4、当服务器连接成功后,调用执行命令函数executeCommand,再该代码中的锁用于保护共享资源(resultChan)的访问
- 因为如果多个 goroutine 同时向通道发送数据(比如日志输出)
- 没有锁会导致输出混乱(多个 goroutine 的日志可能会交错,难以看清)
- 使用 sync.Mutex 来确保每次只有一个 goroutine 向通道发送数据,从而保证输出日志的顺序和一致性
- 保证了多个 goroutine 在写入 resultChan 时不会互相干扰,避免了并发写入导致的数据不一致或错乱
-
- 5、当所有远程机器的命令执行完成后,关闭会话、关闭通道,最终再打印出通道中所有的日志信息
复制代码 总结
以上就是SSH标准库本身整理的知识,故不积跬步,无以至千里;不积小流,无以成江海,慢慢整理golang中运维可以使用到的相干库,向运维徐徐靠拢
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |