如何将容器stdin、stdout与WebSocket连接?无输出命令阻塞求助
解决Docker Exec与WebSocket连接时无输出命令阻塞的问题
我帮你排查了代码里的问题,核心原因是同步等待无输出命令的响应,加上Docker Exec的Reader在没有数据时会一直阻塞,导致整个流程卡住。下面是具体的分析和修复方案:
问题根源
- 全局通道冲突:你用了全局的
inout和output通道,多个WebSocket连接会互相干扰,这是隐藏的bug。 - 同步主循环阻塞:主循环里执行
inout <- message后立刻data := <-output,如果命令(比如cd /)没有stdout输出,output通道永远不会收到数据,主goroutine就一直卡在这里。 - 非交互式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> `))
关键修改说明
- 交互式shell:把
/bin/sh改成/bin/sh -i,这样即使执行cd /这类无输出命令,shell也会返回提示符,保证bufin.Read不会一直阻塞。 - 局部通道替代全局通道:每个WebSocket连接创建自己的
inout和done通道,彻底解决多客户端冲突问题。 - 分离读写goroutine:将WebSocket读、Docker stdin写、Docker stdout读拆分成独立的goroutine,避免同步等待导致的阻塞。主goroutine只需要等待连接关闭信号,保持整个流程的异步性。
- 连接关闭信号:用
done通道通知所有goroutine在WebSocket连接关闭时退出,避免资源泄漏。
现在你可以测试一下:打开页面连接WebSocket后,先发送ls看正常输出,再发送cd /,这时候会收到shell的提示符,不会再出现阻塞问题了。
内容的提问来源于stack exchange,提问作者andy zhang
相关产品推荐
相关产品推荐

