如何将容器stdin和stdout与websocket连接?

At present I want to connect container stdin and stdout with websocket. But I can not read stdout if there are no output. e.g. "cd /"

Here is my code:

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 inout chan []byte
var output chan []byte

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"},
        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()

    bufin := bufio.NewReader(containerConn.Reader)

    go func(w io.WriteCloser) {
        for {
            data, ok := <-inout
            if !ok {
                fmt.Println("!ok")
                w.Close()
                return
            }

            fmt.Println(string(data))
            w.Write(append(data, '
'))
        }
    }(containerConn.Conn)

    go func() {
        for {
            buffer := make([]byte, 4096, 4096)
            c, err := bufin.Read(buffer)
            if err != nil{
                fmt.Println(err)
            }
            //c, err := containerConn.Reader.Read(buffer)
            if c > 0 {
                output <- buffer[:c]
            }
            if c == 0{
                output <- []byte{' '}
            }
            if err != nil {
                break
            }
        }
    }()

    for {

        mt, message, err := conn.ReadMessage()
        if err != nil {
            log.Println("read:", err)
            break
        }

        log.Printf("recv: %s", message)
        inout <- message
        data := <-output
        err = conn.WriteMessage(mt, data)
        if err != nil {
            log.Println("write:", err)
            break
        }
    }
}

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

func main() {

    inout = make(chan []byte)
    output = make(chan []byte)

    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>
`))

I try many ways, but no way can work :(

You need to implement the timeout on the call because in this part:

log.Printf("recv: %s", message)
    inout <- message
    data := <-output
    err = conn.WriteMessage(mt, data)
    if err != nil {
        log.Println("write:", err)
        break
    }

You are waiting always to get the response for the server.

Here are you code working properly with the timeout implemented and an issue on the socket because needs to send utf8 and needs to be parsed to utf8 before send to the client.

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"
    "time"
    "unicode/utf8"
)

var inout chan []byte
var output chan []byte

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"},
        Tty:          false,
        Detach:       false,
    }

    //set target container
    exec, err := cli.ContainerExecCreate(ctx, "vigorous_mclean", 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()

    bufin := bufio.NewReader(containerConn.Reader)

    // Write to docker container
    go func(w io.WriteCloser) {
        for {
            data, ok := <-inout
            log.Println("Received to send to docker", data)
            if !ok {
                fmt.Println("!ok")
                w.Close()
                return
            }

            w.Write(append(data, '
'))
        }
    }(containerConn.Conn)

    // Received of Container Docker
    go func() {
        for {
            buffer := make([]byte, 4096, 4096)
            c, err := bufin.Read(buffer)
            if err != nil {
                fmt.Println(err)
            }
            //c, err := containerConn.Reader.Read(buffer)
            if c > 0 {
                output <- buffer[:c]
            }
            if c == 0 {
                output <- []byte{' '}
            }
            if err != nil {
                break
            }
        }
    }()

    for {
        conn.CloseHandler()
        mt, message, err := conn.ReadMessage()
        log.Println(mt)
        if err != nil {
            log.Println("read:", err)
            break
        } else {
            log.Printf("recv: %s
", message)
            inout <- message
            select {
            case data := <-output:
                stringData := string(data[:])
                if !utf8.ValidString(stringData) {
                    v := make([]rune, 0, len(stringData))
                    for i, r := range stringData {
                        if r == utf8.RuneError {
                            _, size := utf8.DecodeRuneInString(stringData[i:])
                            if size == 1 {
                                continue
                            }
                        }
                        v = append(v, r)
                    }
                    stringData = string(v)
                }
                err = conn.WriteMessage(mt, []byte(stringData))
                if err != nil {
                    log.Println("write:", err)
                }

            case <-time.After(time.Second * 1):
                log.Println("Timeout")

            }
        }

    }
}

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

func main() {

    inout = make(chan []byte)
    output = make(chan []byte)

    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>
`))

Another method to connect directly the stdout to websocket without any timeout is create a goroutine that when you receive something from the docker send to the client directly and the code can be this

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"
    "unicode/utf8"
)

var inout chan []byte
var output chan []byte

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"},
        Tty:          false,
        Detach:       false,
    }

    //set target container
    exec, err := cli.ContainerExecCreate(ctx, "sharp_goldwasser", 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()

    bufin := bufio.NewReader(containerConn.Reader)

    // Write to docker container
    go func(w io.WriteCloser) {
        for {
            data, ok := <-inout
            log.Println("Received to send to docker", data)
            if !ok {
                fmt.Println("!ok")
                w.Close()
                return
            }

            w.Write(append(data, '
'))
        }
    }(containerConn.Conn)

    // Received of Container Docker
    go func() {
        for {
            buffer := make([]byte, 4096, 4096)
            c, err := bufin.Read(buffer)
            if err != nil {
                fmt.Println(err)
            }
            //c, err := containerConn.Reader.Read(buffer)
            if c > 0 {
                output <- buffer[:c]
            }
            if c == 0 {
                output <- []byte{' '}
            }
            if err != nil {
                break
            }
        }
    }()
    // Connect the STDOUT to the Socket
    go func () {
        data := <-output
        stringData := string(data[:])
        if !utf8.ValidString(stringData) {
            v := make([]rune, 0, len(stringData))
            for i, r := range stringData {
                if r == utf8.RuneError {
                    _, size := utf8.DecodeRuneInString(stringData[i:])
                    if size == 1 {
                        continue
                    }
                }
                v = append(v, r)
            }
            stringData = string(v)
        }
        err = conn.WriteMessage(1, []byte(stringData))
        if err != nil {
            log.Println("write:", err)
        }
    }()

    for {
        conn.CloseHandler()
        _, message, err := conn.ReadMessage()
        if err != nil {
            log.Println("read:", err)
            break
        } else {
            log.Printf("recv: %s
", message)
            inout <- message

        }

    }
}

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

func main() {

    inout = make(chan []byte)
    output = make(chan []byte)

    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>
`))