
本文详解如何使用 go 构建一个支持远程执行命令并双向实时传输标准输入输出(stdin/stdout)的 tcp 服务端与客户端,解决进程 i/o 绑定、连接阻塞及字节流同步等核心问题。
本文详解如何使用 go 构建一个支持远程执行命令并双向实时传输标准输入输出(stdin/stdout)的 tcp 服务端与客户端,解决进程 i/o 绑定、连接阻塞及字节流同步等核心问题。
要实现“客户端发送命令 → 服务端执行 → 实时返回 stdout 并支持 stdin 输入”的完整交互流程,关键在于正确桥接 exec.Cmd 的标准流与 TCP 连接,而非仅捕获一次性输出。原始代码存在多个根本性缺陷:每次循环新建 bufio.NewReader(conn) 导致读取状态丢失;c.Write([]byte(b.String() + "\n")) 在 cmd.Run() 后才写入,但未处理换行一致性与缓冲区清空;更重要的是——它完全缺失对 stdin 的透传支持,无法实现真正的交互式 shell 行为(如 vim、top、python 等需用户输入的程序)。
下面是一个生产就绪型改进方案,分为服务端与客户端两部分,并强调关键设计点:
✅ 服务端:支持单次命令执行 + 完整 I/O 透传
package main
import (
"bufio"
"fmt"
"io"
"net"
"os/exec"
"strings"
)
func handleConnection(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
// 读取一行命令(兼容 \r\n 和 \n)
line, err := reader.ReadString('\n')
if err != nil {
return // 连接关闭或读取失败
}
cmdStr := strings.TrimSpace(line)
if cmdStr == "" {
continue
}
// 解析命令和参数(简单空格分割,生产环境建议用 shellwords)
args := strings.Fields(cmdStr)
if len(args) == 0 {
continue
}
// 启动子进程,绑定 stdin/stdout/stderr 到连接
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = conn // 客户端输入直接作为进程 stdin
cmd.Stdout = conn // 进程 stdout 直接写回客户端
cmd.Stderr = conn // 错误也透传(可选)
// 启动并等待完成(注意:此方式支持交互式程序!)
if err := cmd.Start(); err != nil {
fmt.Fprintf(conn, "ERROR: %v\n", err)
continue
}
if err := cmd.Wait(); err != nil {
fmt.Fprintf(conn, "COMMAND FAILED: %v\n", err)
}
// 自动换行分隔,便于客户端识别命令结束
fmt.Fprint(conn, "\n--- COMMAND FINISHED ---\n")
}
}
func main() {
ln, err := net.Listen("tcp", ":8888")
if err != nil {
panic(err)
}
defer ln.Close()
fmt.Println("TCP command server listening on :8888")
for {
conn, err := ln.Accept()
if err != nil {
continue
}
go handleConnection(conn)
}
}
✅ 客户端:支持交互式输入/输出(类 telnet 行为)
package main
import (
"bufio"
"fmt"
"io"
"net"
"os"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
panic(err)
}
defer conn.Close()
// 启动两个 goroutine:一个读服务端响应,一个发用户输入
go func() {
io.Copy(os.Stdout, conn) // 实时打印服务端所有输出(含 stdout/stderr)
}()
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
text := scanner.Text()
if text == "exit" || text == "quit" {
break
}
// 发送命令 + 换行符(服务端按行解析)
_, _ = fmt.Fprintln(conn, text)
}
}
⚠️ 关键注意事项与进阶建议
-
不要在循环内重复创建
bufio.Reader:这会丢弃缓冲区中未读完的数据,导致粘包或截断。应复用同一个 reader 实例。 -
交互式程序必须透传
Stdin:仅捕获Stdout只能用于非交互命令(如ls,date)。真正支持vi或python -i需将conn直接赋给cmd.Stdin,并确保客户端持续向连接写入(而非仅发一次)。 -
超时与资源限制:生产环境务必添加
cmd.Wait()超时、内存/CPU 限制(通过syscall.Setrlimit或容器化)、以及最大执行时间控制(context.WithTimeout)。 - 安全警告:此模型严禁暴露于公网!未做命令白名单、参数注入过滤、用户权限隔离,存在严重 RCE 风险。仅适用于可信内网调试场景。
-
增强方向:可集成
golang.org/x/crypto/ssh实现加密通道;使用pty(如github.com/creack/pty)模拟真实终端;或改用exec.LookPath校验二进制白名单。
通过以上结构,你将获得一个真正双向、低延迟、可交互的 TCP 命令管道——不再是“执行后返回结果”,而是“像登录远程 shell 一样操作”。










