当前位置: 代码网 > it编程>前端脚本>Golang > golang标准库SSH操作示例详解

golang标准库SSH操作示例详解

2025年02月14日 Golang 我要评论
前言ssh 全称为 secure shell,是一种用于安全地远程登录到网络上的其他计算机的网络协议。相信做运维的同学没有不了解 ssh的,比较常用的登录服务器的 shell 工具例如 xshell、

前言

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()实现可通过 stdoutstderr 获取输出本地命令异步执行,非阻塞
err:=newsession.start("xx")启动远程命令异步执行,不等待命令完成非阻塞,适用于需要启动后台进程的场景,如果要阻塞使用,newsession.wait()实现可通过 stdoutstderr 获取输出远程命令异步执行,非阻塞
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 / stderrpipesession.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.stdoutsession.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中运维可以使用到的相关库,向运维逐渐靠拢

到此这篇关于golang标准库ssh操作示例的文章就介绍到这了,更多相关golang标准库ssh内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com