golang标准库SSH操作示例
文章目录
- 前言
- 一、了解SSH
- 二、重要知识点
- 1.安装ssh库
- 2.ssh库重要知识牢记
- 三、模拟连接远程服务器并执行命令
- 四、SSH与os/exec标准库下执行命令的几种方式对比
- 五、SSH库下三种执行命令方式演示
- 5.1. session.CombinedOutput()示例
- 5.2. session.Run()示例
- 5.3. session.Start()、session.Wait()示例
- 六、两种捕获标准输出和标准错误的方法:`StdoutPipe` / `StderrPipe` 和 `session.Stdout` / `session.Stderr`之间的区别
- 6.1. 使用 `StdoutPipe` 和 `StderrPipe`捕获标准输出和标准错误
- 6.2. 使用 重定向 `session.Stdout` 和 `session.Stderr` 捕获标准输出和标准错误
- 6.3.两种方式的区别
- 七、示例: 连接到多台服务器并执行多个命令返回命令执行结果
- 总结
前言
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中运维可以使用到的相关库,向运维逐渐靠拢