利用Golang pipe实现远程交互

本文介绍Golang pipe,以及在不同场景下的应用。

Pipe介绍

pipe实现从一个进程重定向至另一个进程,它是双向数据通道,用于实现进行间通信。
io.Pipe函数创建内存同步通道,用于连接io.Reader和io.Writer. 本文示例使用环境为:

go version
go version go1.19.3 linux/amd64

Go pipe 简单示例

在实现远程交互之前,先看下面简单示例,演示如何使用io.Pipe函数:

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

func main() {
    r, w := io.Pipe()

    go func() {
        fmt.Fprint(w, "Hello there\n")
        w.Close()
    }()

    _, err := io.Copy(os.Stdout, r)

    if err != nil {
        log.Fatal(err)
    }
}

首先创建pipe,然后在协程中给管道的writer写数据,然后使用io.Copy函数从管道Reader中拷贝数据至标准输出:

go func() {
    fmt.Fprint(w, "Hello there\n")
    w.Close()
}()

在协程中写数据是因为每次写PipeWriter都阻塞直到PipeReader完全消费了数据。
运行程序:

go run main.go 
Hello there

通过这个简单示例,展示了管道重定向能力,了解这个基本原理后,下面先看Shell命令的管道,最终我们的目标是通过WEB方式实现远程命令行交互。

Go cmd StdoutPipe

当命令启动时,Cmd的StdoutPipe返回管道连接命令的标准输出:

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
    "os/exec"
)

func main() {

    cmd := exec.Command("ping", "www.baidu.com")
    stdout, err := cmd.StdoutPipe()

    if err != nil {
        log.Fatal(err)
    }

    cmd.Start()

    buf := bufio.NewReader(stdout) 
    num := 0

    for {
        line, _, _ := buf.ReadLine()
        if num > 3 {
            os.Exit(0)
        }
        num += 1
        fmt.Println(string(line))
    }
}

上面代码启动ping命令,然后从其输出中读取4行. 这行代码启动ping命令:

    cmd := exec.Command("ping", "www.baidu.com")
    stdout, err := cmd.StdoutPipe()
    buf := bufio.NewReader(stdout) 

接着获取命令的标准输出,并保存输出之buf中。下面从缓冲中读取4行:

for {
    line, _, _ := buf.ReadLine()
    if num > 3 {
        os.Exit(0)
    }
    num += 1
    fmt.Println(string(line))
}

读取4行并输出到控制台,运行程序,输出结果如下:

go run main.go
PING www.a.shifen.com (180.101.50.188) 56(84) bytes of data.
64 bytes from 180.101.50.188 (180.101.50.188): icmp_seq=1 ttl=53 time=12.0 ms
64 bytes from 180.101.50.188 (180.101.50.188): icmp_seq=2 ttl=53 time=11.2 ms
64 bytes from 180.101.50.188 (180.101.50.188): icmp_seq=3 ttl=53 time=10.5 ms

通过这个示例,成功地把命令的执行结果捕获到buffer中,并能够增加处理逻辑再输出到控制台。

http请求处理中使用管道

下面示例展示在http请求处理中使用管道。运行date命令,通过HTTP输出结果,可以实现元从查看命令执行结果。

package main

import (
    "fmt"
    "io"
    "net/http"
    "os/exec"
)

func handler(w http.ResponseWriter, r *http.Request) {

    cmd := exec.Command("date")

    pr, pw := io.Pipe()
    defer pw.Close()

    cmd.Stdout = pw
    cmd.Stderr = pw
    go io.Copy(w, pr)

    cmd.Run()
}

func main() {

    http.HandleFunc("/", handler)
    fmt.Println("server started on port 8080")
    http.ListenAndServe(":8080", nil)
}

关键代码为创建管道,并把PipeWriter赋给命令的标准输出和标准错误。

cmd := exec.Command("date")
pr, pw := io.Pipe()
defer pw.Close()
cmd.Stdout = pw
cmd.Stderr = pw

go io.Copy(w, pr)

然后在协程中拷贝PipeReader至http.ResponseWriter.最后运行程序查看结果:

$ go run handler.go 
server started on port 8080

使用curl或浏览器访问地址:localhost:8080,可以看到:

2023年 02月 22日 星期三 17:06:11 CST

修改上面程序,把命令作为参数,即可实现远程交互。下面我们看看如何利用管道给输入端写入数据,包括http请求和命令的标准输入。

利用管道提交post请求json数据

下面示例给https://httpbin.org/post请求地址提交json数据作为请求体。

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "net/http"
)

type PayLoad struct {
    Content string
}

func main() {

    r, w := io.Pipe()

    go func() {
        defer w.Close()

        err := json.NewEncoder(w).Encode(&PayLoad{Content: "Hello there!"})

        if err != nil {
            log.Fatal(err)
        }
    }()

    resp, err := http.Post("https://httpbin.org/post", "application/json", r)

    if err != nil {
        log.Fatal(err)
    }

    body, err := ioutil.ReadAll(resp.Body)

    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(body))
}

上面示例实现给post请求提交json数据,并读取响应内容。

首先定义管道,然后在协程中给管道Writer写入json数据:

go func() {
    defer w.Close()

    err := json.NewEncoder(w).Encode(&PayLoad{Content: "Hello there!"})

    if err != nil {
        log.Fatal(err)
    }
}()

然后把管道Reader作为参数传入请求:

resp, err := http.Post("https://httpbin.org/post", "application/json", r)

最后读取响应内容:

body, err := ioutil.ReadAll(resp.Body)

if err != nil {
    log.Fatal(err)
}

fmt.Println(string(body))

运行程序输出结果:

go run main.go
{
  "args": {}, 
  "data": "{\"Content\":\"Hello there!\"}\n", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Content-Type": "application/json", 
    "Host": "httpbin.org", 
    "Transfer-Encoding": "chunked", 
    "User-Agent": "Go-http-client/2.0", 
    "X-Amzn-Trace-Id": "Root=1-63f5c8c6-4a14ee9a2dc14e352f234fae"
  }, 
  // 省略...
}

通过管道读标准输入

下面示例利用管道从标准输入读取数据,并打印数据及数据字节数、块数:

package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "os"
)

func main() {

    nBytes, nChunks := int64(0), int64(0)
    r := bufio.NewReader(os.Stdin)
    buf := make([]byte, 0, 4*1024)

    for {

        n, err := r.Read(buf[:cap(buf)])
        buf = buf[:n]

        if n == 0 {

            if err == nil {
                continue
            }

            if err == io.EOF {
                break
            }

            log.Fatal(err)
        }

        nChunks++
        nBytes += int64(len(buf))

        fmt.Println(string(buf))

        if err != nil && err != io.EOF {
            log.Fatal(err)
        }
    }

    fmt.Println("Bytes:", nBytes, "Chunks:", nChunks)
}

首先定义包装标准输入Reader:

r := bufio.NewReader(os.Stdin)

buf := make([]byte, 0, 4*1024)
n, err := r.Read(buf[:cap(buf)])
buf = buf[:n]

nChunks++
nBytes += int64(len(buf))
fmt.Println(string(buf))

然后创建4kb缓冲区,从标准输入读数据至缓冲区。然后计算块数和字节数,最后答应缓冲区内容。

date | go run main.go
2023年 02月 22日 星期三 16:08:17 CST

Bytes: 43 Chunks: 1

这里通过|操作传递date命令的输出结果,显示内容与预期一致。

Go Stat

Stat函数返回FileInfo结构体,描述文件信息。我们可以利用其检查数据是否来自终端。

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
)

func main() {
    stat, _ := os.Stdin.Stat()

    if (stat.Mode() & os.ModeCharDevice) == 0 {

        var buf []byte
        scanner := bufio.NewScanner(os.Stdin)

        for scanner.Scan() {
            buf = append(buf, scanner.Bytes()...)
        }

        if err := scanner.Err(); err != nil {
            log.Fatal(err)
        }

        fmt.Printf("Hello %s!\n", buf)

    } else {
        fmt.Print("Enter your name: ")

        var name string
        fmt.Scanf("%s", &name)
        fmt.Printf("Hello %s!\n", name)
    }
}

这个示例数据可能来自终端或管道。为了判断,通过下面代码获取stat:

stat, _ := os.Stdin.Stat()

获取到标准输入的FileInfo结构体后进行判断:

if (stat.Mode() & os.ModeCharDevice) == 0 {

这行判断数据来自管道,反之则为终端。即如果没有管道提供数据,则提示用户输入数据。运行程序:

$ echo "golang" | go run main.go
Hello golang!

$go run main.go
Enter your name: java
Hello java!

总结

本文介绍了Golang管道的使用,除了实现远程命令交互,还介绍了获取标准输入内容、判断标准输入数据来源。读者组合这些简单示例,一定能够编写出炫酷的应用。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 你可以使用 `os/exec` 包来在 Go 中实现与 Shell 的交互。下面是一个简单的示例代码: ```go package main import ( "bufio" "fmt" "os/exec" ) func main() { cmd := exec.Command("sh") stdin, err := cmd.StdinPipe() if err != nil { panic(err) } stdout, err := cmd.StdoutPipe() if err != nil { panic(err) } scanner := bufio.NewScanner(stdout) if err := cmd.Start(); err != nil { panic(err) } go func() { defer stdin.Close() stdin.Write([]byte("echo hello world\n")) }() for scanner.Scan() { fmt.Println(scanner.Text()) } if err := cmd.Wait(); err != nil { panic(err) } } ``` 这个例子创建了一个 `sh` 进程,并通过管道向其输入了一个命令 `echo hello world`,然后读取了输出并打印到控制台。你可以根据自己的需求修改这个例子。 ### 回答2: golang的Shell交互是指在Golang程序中执行Shell命令并与其进行交互的功能。 在Golang实现Shell交互可以使用os/exec包。该包提供了执行Shell命令的函数和方法,可以通过它们来执行Shell命令并获取命令的标准输出、标准错误和执行结果。 首先,我们需要引入os/exec包。然后,使用exec.Command函数创建一个Cmd类型的对象,该对象表示要执行的Shell命令。可以通过Cmd对象的方法设置命令的参数、工作目录等。 接下来,使用Cmd对象的CombinedOutput方法来执行Shell命令,并获取命令的输出结果。CombinedOutput方法会返回一个字节数组,其中存储了命令的标准输出和标准错误。 最后,我们可以将字节数组转换为字符串,并进行相应的处理,如打印输出或进一步解析。 以下是一个简单的示例代码,演示了如何在Golang实现Shell交互的功能: ``` package main import ( "fmt" "log" "os/exec" ) func main() { cmd := exec.Command("ls", "-l") // 创建Cmd对象,执行ls -l命令 output, err := cmd.CombinedOutput() // 执行命令并获取输出结果 if err != nil { log.Fatal(err) // 如果执行命令出错,打印错误信息并退出 } fmt.Println(string(output)) // 将字节数组转换为字符串并打印输出 } ``` 以上代码会执行`ls -l`命令,并将结果打印到控制台。 需要注意的是,在使用Golang执行Shell命令时,需要保证可执行的命令位于环境变量`$PATH`所指定的路径中,否则需要指定完整的命令路径。示例中的`ls`命令是一个常见的Shell命令,因此不需要指定路径。 总结来说,通过使用os/exec包,我们可以在Golang程序中实现Shell交互的功能,从而执行Shell命令,并获取其输出结果。 ### 回答3: Go语言本身并不提供直接的Shell交互功能,但我们可以使用一些第三方库来实现类似的功能。 一个常用的库是“os/exec”,它允许我们执行外部 Shell 命令并与其进行交互。下面是一个简单的示例代码: ```go package main import ( "bufio" "fmt" "os" "os/exec" ) func main() { // 创建一个命令对象 cmd := exec.Command("/bin/sh") // 获取命令的标准输入和输出管道 stdin, _ := cmd.StdinPipe() stdout, _ := cmd.StdoutPipe() // 启动命令 cmd.Start() // 准备一个输入读取器,用于读取用户输入 reader := bufio.NewReader(os.Stdin) for { // 读取用户输入 input, _ := reader.ReadString('\n') // 将用户输入写入命令的标准输入管道 stdin.Write([]byte(input)) // 读取命令的标准输出 output := make([]byte, 1024) stdout.Read(output) // 打印输出结果 fmt.Println(string(output)) } } ``` 这段代码会启动一个新的Shell,并不断读取用户的输入并将其发送给Shell的标准输入,然后读取Shell的标准输出并打印出来。 请注意,这里的示例代码没有处理错误情况,为了简单起见,省略了错误处理部分。在实际应用中,我们应该对错误进行适当处理。 希望这个示例能够帮助你实现Go语言下的Shell交互功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值