
本文详解如何基于 go 语言构建一个简易 tcp 远程命令执行服务,支持客户端发送命令、服务端执行并实时回传 stdout,同时为后续扩展 stdin 交互打下基础。
本文详解如何基于 go 语言构建一个简易 tcp 远程命令执行服务,支持客户端发送命令、服务端执行并实时回传 stdout,同时为后续扩展 stdin 交互打下基础。
在构建轻量级远程命令执行系统时,核心挑战在于:如何将子进程的标准输出(stdout)实时转发给 TCP 客户端,同时未来支持从客户端读取输入(stdin)注入到子进程中。原始代码中 c.Write([]byte(b.String() + "\n")) 失效,根本原因在于 bytes.Buffer 是一次性写入缓冲区,且 cmd.Run() 是同步阻塞调用——它只捕获执行结束后的全部输出,无法实现“流式”交互;更严重的是,当前架构完全未处理子进程的 stdin 接入,也无法应对多行输出、空字节、超长响应等边界情况。
下面是一个可运行、结构清晰、具备生产基础的改进方案:
✅ 正确做法:使用 io.Copy 实现流式管道桥接
关键改进点:
- 使用
io.MultiWriter和io.Pipe可扩展地连接进程 I/O 与网络连接; -
cmd.Stdout和cmd.Stdin直接绑定conn(或其副本),实现零拷贝流式传输; - 添加基础错误处理与连接生命周期管理;
- 客户端采用
bufio.Scanner避免粘包,服务端使用io.Copy自动处理分块读写。
? 服务端(server.go)
package main
import (
"fmt"
"io"
"net"
"os/exec"
"strings"
"time"
)
func main() {
listener, err := net.Listen("tcp", ":8888")
if err != nil {
panic(fmt.Sprintf("failed to listen: %v", err))
}
defer listener.Close()
fmt.Println("Server listening on :8888")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Printf("accept error: %v\n", err)
continue
}
go handleConnection(conn)
}
}
func handleConnection(conn net.Conn) {
defer conn.Close()
conn.SetDeadline(time.Now().Add(30 * time.Second)) // 防呆超时
// 读取一行命令(支持带参数,如 "ls -la /tmp")
reader := bufio.NewReader(conn)
line, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("read command failed: %v\n", err)
return
}
cmdName := strings.TrimSpace(line)
if cmdName == "" {
conn.Write([]byte("Error: empty command\n"))
return
}
// 解析命令和参数(简单空格分割,生产环境建议用 shellwords)
args := strings.Fields(cmdName)
if len(args) == 0 {
conn.Write([]byte("Error: invalid command format\n"))
return
}
cmd := exec.Command(args[0], args[1:]...)
// 关键:将 conn 的读写流直接桥接到子进程
cmd.Stdin = conn
cmd.Stdout = conn
cmd.Stderr = conn // 同时透出 stderr,便于调试
// 启动命令(非阻塞)
if err := cmd.Start(); err != nil {
conn.Write([]byte(fmt.Sprintf("Failed to start '%s': %v\n", cmdName, err)))
return
}
// 等待命令结束,并确保所有 I/O 完成
if err := cmd.Wait(); err != nil {
conn.Write([]byte(fmt.Sprintf("Command exited with error: %v\n", err)))
}
}
?️ 客户端(client.go)
package main
import (
"bufio"
"fmt"
"net"
"os"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
panic(fmt.Sprintf("dial failed: %v", err))
}
defer conn.Close()
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("Enter command (e.g., 'date' or 'ls -l'): ")
for scanner.Scan() {
cmd := scanner.Text()
if cmd == "" {
break
}
// 发送命令(自动换行)
_, _ = conn.Write([]byte(cmd + "\n"))
// 流式读取响应(直到连接关闭或超时)
response, err := io.ReadAll(conn)
if err != nil && err != io.EOF {
fmt.Printf("Read error: %v\n", err)
break
}
if len(response) > 0 {
fmt.Print("Response:\n")
fmt.Print(string(response))
} else {
fmt.Println("(No output)")
}
fmt.Print("\nEnter next command: ")
}
}
⚠️ 注意事项与进阶方向
- 安全性警告:此示例绝不适用于公网!未做身份认证、命令白名单、资源限制(CPU/内存/超时)、沙箱隔离,存在严重 RCE 风险。
-
stdin 支持已就绪:服务端中
cmd.Stdin = conn已启用双向流——只要客户端持续写入,子进程即可实时读取(例如cat或nc交互场景)。 -
改进 stdin 体验:若需“客户端输入 → 进程 stdin → 进程 stdout → 客户端显示”闭环,应使用 goroutine 分别处理
io.Copy(conn, cmd.Stdin)和io.Copy(cmd.Stdout, conn),避免单向阻塞。 -
生产建议:替换为
golang.org/x/sys/unix创建伪终端(pty),或集成github.com/creack/pty实现真正 shell 会话;配合 JWT 认证、cgroup 限流、日志审计。
通过以上重构,你获得了一个真正流式、双向、可扩展的 TCP 命令通道——不仅是“能跑”,更是为构建安全远程 shell、CI 执行代理或嵌入式调试网关打下了坚实基础。










