You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将容器stdin、stdout与WebSocket连接?无输出命令阻塞求助

解决Docker Exec与WebSocket连接时无输出命令阻塞的问题

我帮你排查了代码里的问题,核心原因是同步等待无输出命令的响应,加上Docker Exec的Reader在没有数据时会一直阻塞,导致整个流程卡住。下面是具体的分析和修复方案:

问题根源

  1. 全局通道冲突:你用了全局的inout和output通道,多个WebSocket连接会互相干扰,这是隐藏的bug。
  2. 同步主循环阻塞:主循环里执行inout <- message后立刻data := <-output,如果命令(比如cd /)没有stdout输出,output通道永远不会收到数据,主goroutine就一直卡在这里。
  3. 非交互式shell无提示符:用/bin/sh启动的非交互式shell,执行无输出命令后不会返回任何内容,导致bufin.Read(buffer)一直阻塞等待数据。

修复方案

下面是修改后的完整代码,我标注了关键修改点:

package main

import (
	dcl "github.com/docker/docker/client"
	"context"
	"html/template"
	"github.com/docker/docker/api/types"
	"log"
	"net/http"
	"flag"
	"github.com/gorilla/websocket"
	"fmt"
	"io"
	"bufio"
)

var addr = flag.String("addr", "localhost:8080", "http service address")
var upgrader = websocket.Upgrader{}

func echo(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Print(err)
		return
	}
	defer conn.Close()

	cli, err := dcl.NewEnvClient()
	if err != nil {
		log.Print(err)
		conn.WriteMessage(websocket.TextMessage, []byte(err.Error()))
		return
	}

	ctx := context.Background()
	execConfig := types.ExecConfig{
		AttachStderr: true,
		AttachStdin:  true,
		AttachStdout: true,
		Cmd:          []string{"/bin/sh", "-i"}, // 🔴 改成交互式shell,保证执行完命令后返回提示符
		Tty:          false,
		Detach:       false,
	}

	// set target container
	exec, err := cli.ContainerExecCreate(ctx, "ubuntu", execConfig)
	if err != nil {
		log.Print(err)
		conn.WriteMessage(websocket.TextMessage, []byte(err.Error()))
		return
	}

	execAttachConfig := types.ExecStartCheck{
		Detach: false,
		Tty:    false,
	}

	containerConn, err := cli.ContainerExecAttach(ctx, exec.ID, execAttachConfig)
	if err != nil {
		log.Print(err)
		conn.WriteMessage(websocket.TextMessage, []byte(err.Error()))
		return
	}
	defer containerConn.Close()

	// 🔴 每个连接使用局部通道,避免全局变量冲突
	inout := make(chan []byte)
	done := make(chan struct{})

	// 🔴 单独goroutine处理WebSocket读操作,发送命令到Docker
	go func() {
		defer close(inout)
		defer close(done)
		for {
			mt, message, err := conn.ReadMessage()
			if err != nil {
				log.Println("read from websocket:", err)
				break
			}
			log.Printf("recv: %s", message)
			// 给命令加上换行符,模拟终端输入
			inout <- append(message, '\n')
		}
	}()

	// 🔴 单独goroutine处理Docker stdin写入
	go func(w io.WriteCloser) {
		defer w.Close()
		for data := range inout {
			fmt.Println("send to container:", string(data))
			_, err := w.Write(data)
			if err != nil {
				log.Println("write to container:", err)
				break
			}
		}
	}(containerConn.Conn)

	// 🔴 单独goroutine处理Docker stdout/stderr读取,实时发送到WebSocket
	go func(r io.Reader) {
		bufin := bufio.NewReader(r)
		for {
			select {
			case <-done: // 收到连接关闭信号,退出goroutine
				return
			default:
				buffer := make([]byte, 4096)
				c, err := bufin.Read(buffer)
				if err != nil {
					if err != io.EOF {
						log.Println("read from container:", err)
					}
					break
				}
				if c > 0 {
					err := conn.WriteMessage(websocket.TextMessage, buffer[:c])
					if err != nil {
						log.Println("write to websocket:", err)
						break
					}
				}
			}
		}
	}(containerConn.Reader)

	// 🔴 等待连接关闭信号,保持goroutine存活
	<-done
}

func home(w http.ResponseWriter, r *http.Request) {
	homeTemplate.Execute(w, "ws://"+r.Host+"/echo")
}

func main() {
	http.HandleFunc("/echo", echo)
	http.HandleFunc("/", home)
	log.Fatal(http.ListenAndServe(*addr, nil))
}

var homeTemplate = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
window.addEventListener("load", function(evt) {
	var output = document.getElementById("output");
	var input = document.getElementById("input");
	var ws;
	var print = function(message) {
		var d = document.createElement("div");
		d.innerHTML = message;
		output.appendChild(d);
	};
	document.getElementById("open").onclick = function(evt) {
		if (ws) {
			return false;
		}
		ws = new WebSocket("{{.}}");
		ws.onopen = function(evt) {
			print("OPEN");
		}
		ws.onclose = function(evt) {
			print("CLOSE");
			ws = null;
		}
		ws.onmessage = function(evt) {
			print("RESPONSE: " + evt.data);
		}
		ws.onerror = function(evt) {
			print("ERROR: " + evt.data);
		}
		return false;
	};
	document.getElementById("send").onclick = function(evt) {
		if (!ws) {
			return false;
		}
		print("SEND: " + input.value);
		ws.send(input.value);
		return false;
	};
	document.getElementById("close").onclick = function(evt) {
		if (!ws) {
			return false;
		}
		ws.close();
		return false;
	};
});
</script>
</head>
<body>
<table>
<tr><td valign="top" width="50%">
<p>Click "Open" to create a connection to the server, "Send" to send a message to the server and "Close" to close the connection. You can change the message and send multiple times.<p>
<form>
<button id="open">Open</button>
<button id="close">Close</button>
<p><input id="input" type="text" value="ls">
<button id="send">Send</button>
</form>
</td><td valign="top" width="50%">
<div id="output"></div>
</td></tr></table>
</body>
</html>
`))

关键修改说明

  1. 交互式shell:把/bin/sh改成/bin/sh -i,这样即使执行cd /这类无输出命令,shell也会返回提示符,保证bufin.Read不会一直阻塞。
  2. 局部通道替代全局通道:每个WebSocket连接创建自己的inout和done通道,彻底解决多客户端冲突问题。
  3. 分离读写goroutine:将WebSocket读、Docker stdin写、Docker stdout读拆分成独立的goroutine,避免同步等待导致的阻塞。主goroutine只需要等待连接关闭信号,保持整个流程的异步性。
  4. 连接关闭信号:用done通道通知所有goroutine在WebSocket连接关闭时退出,避免资源泄漏。

现在你可以测试一下:打开页面连接WebSocket后,先发送ls看正常输出,再发送cd /,这时候会收到shell的提示符,不会再出现阻塞问题了。

内容的提问来源于stack exchange,提问作者andy zhang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:43:59