使用crypto/ssh在golang中进行SCP文件传输

4
我正在尝试通过ssh下载远程文件,以下方法在shell中可以正常使用。
ssh hostname "tar cz /opt/local/folder" > folder.tar.gz

然而,对于Golang的同样方法,输出的构件大小有所不同。例如,使用纯Shell的相同文件夹会产生179B的构件压缩文件,而使用Go脚本相同的文件夹只有178B。

我猜测是io.Reader中漏掉了一些东西或会话(session)被提前关闭了。恳请大家帮忙解决。

这是我的脚本示例:

func executeCmd(cmd, hostname string, config *ssh.ClientConfig, path string) error {
    conn, _ := ssh.Dial("tcp", hostname+":22", config)
    session, err := conn.NewSession()
    if err != nil {
        panic("Failed to create session: " + err.Error())
    }

    r, _ := session.StdoutPipe()
    scanner := bufio.NewScanner(r)

    go func() {
        defer session.Close()

        name := fmt.Sprintf("%s/backup_folder_%v.tar.gz", path, time.Now().Unix())
        file, err := os.OpenFile(name, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
        if err != nil {
            panic(err)
        }
        defer file.Close()
        for scanner.Scan() {
            fmt.Println(scanner.Bytes())
            if err := scanner.Err(); err != nil {
                fmt.Println(err)
            }

            if _, err = file.Write(scanner.Bytes()); err != nil {
                log.Fatal(err)

            }
        }
    }()

    if err := session.Run(cmd); err != nil {
        fmt.Println(err.Error())
        panic("Failed to run: " + err.Error())
    }

    return nil
}

谢谢!


1
一个扫描器适用于以换行符分隔的文本,不要用它处理二进制数据(因为它会移除换行符)。 - JimB
谢谢JimB,您能指导一下什么可以用于二进制数据吗? - neveragny
1
此外,为了避免程序在快速执行或同时运行时发生冲突,最好使用比秒更独特的东西来创建唯一的文件名,比如 ioutil.TempFile 或随机字符串。 - JimB
2个回答

5
bufio.Scanner 适用于换行分隔的文本。根据文档,扫描器将移除换行符,从您的二进制文件中剥离任何10
您不需要 goroutine 来进行复制,因为您可以使用 session.Start 异步启动该进程。
您可能也不需要使用 bufio。您应该使用 io.Copy 来复制文件,它已经在 ssh 客户端本身所做的任何缓冲之上具有内部缓冲区。如果需要额外的缓冲区以提高性能,请使用 bufio.Reader 包装会话输出。
最后,您返回一个错误值,因此请在常规错误条件下使用它,而不是 panic。
conn, err := ssh.Dial("tcp", hostname+":22", config)
if err != nil {
    return err
}

session, err := conn.NewSession()
if err != nil {
    return err
}
defer session.Close()

r, err := session.StdoutPipe()
if err != nil {
    return err
}

name := fmt.Sprintf("%s/backup_folder_%v.tar.gz", path, time.Now().Unix())
file, err := os.OpenFile(name, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
    return err
}
defer file.Close()

if err := session.Start(cmd); err != nil {
    return err
}

n, err := io.Copy(file, r)
if err != nil {
    return err
}

if err := session.Wait(); err != nil {
    return err
}

return nil

谢谢您的回复,它对我很有帮助。 - neveragny
关于 io.Copy 的一个快速问题。我使用 io.Reader 的原因是需要传输每个15-30GB的大型 tar 包,据我所知,io.Copy 会尝试将其保存在内存中直到找到 EOF?是这样吗? - neveragny
@neveragny:不,我不确定是什么让你有这样的印象。io.Copy直接从src复制到dst。 - JimB
@neveragny:你可以查看io包源代码,很快就会发现它是先读取再立即写入的。这些写入操作由操作系统进行缓冲,直到文件同步之前不会写入磁盘,而这可能要等到文件关闭时才会进行同步。 - JimB
代码并没有什么不同。我在Windows上运行这个脚本,使用我的最大连接速度下载一个tarball文件,大约是200,然后我编译Linux的go并运行脚本,速度大约是6-10 MB。需要很长时间。我应该从哪里开始调查问题? - DarkMoria
显示剩余2条评论

-1
你可以尝试像这样做:
r, _ := session.StdoutPipe()
reader := bufio.NewReader(r)

go func() {
    defer session.Close()
    // open file etc

    // 10 is the number of bytes you'd like to copy in one write operation
    p := make([]byte, 10)
    for {
        n, err := reader.Read(p)
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatal("err", err)
        }

        if _, err = file.Write(p[:n]); err != nil {
            log.Fatal(err)
        }
    }
}()

确保你的 goroutines 同步正确,以便输出完全写入文件。


10 不是一个有用的缓冲区大小,它太小了,也没有对齐到任何2的幂。您还需要在检查 EOF 之前检查已读取的字节,以便不会意外截断流。 - JimB
是的,这更像是一个占位符。但是什么尺寸会比较好呢? - abhink
1
io.Copy默认使用32k。只需使用io.Copy(或者如果需要不同的缓冲区大小,则使用CopyBuffer),因为它会正确处理各种错误组合。 - JimB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接