Simple Websocket Echo Server in Go

Prompt: A bunch of metal gears and pulleys on a futuristic manufacturing machine, realistic

Recently I was debugging some issues with a Websocket server. The server was running serverless via knative eventing in Kubernetes, with Istio as the gateway and some virtual service rules. The connection was failing with a 503 status and no real information. The server itself provided no information about its connections or incoming messages, so I had no way to determine whether it was the problem or something along the network path was causing problems. Instead of blindly making changes I went looking for a simple websocket server to run but found most testing tools too complex for my use case. I ended up building my own echo server in Go which was incredibly simple! I also found this great websocket client tool which was one of the few I was able to use as simply as it said on the readme.

Building a Websocket Server in Go

I am a big fan of Go, so it was the tool I picked for this echo server. I wanted a no frills solution: All it needed to do was print to stdout when a message was received, and spit that message back to the connected client. I also created an http listener so that I could respond to knative liveness and readiness probes. Both of these things were simple, here is the code:

package main

import (
    "golang.org/x/net/websocket"
    "golang.org/x/net/html"
    "fmt"
    "log"
    "net/http"
)

func Echo(ws *websocket.Conn) {
    var err error

    for {
        var reply string

        if err = websocket.Message.Receive(ws, &reply); err != nil {
            fmt.Println("Can't receive")
            break
        }

        fmt.Println("Received back from client: " + reply)

        msg := "Received:  " + reply
        fmt.Println("Sending to client: " + msg)

        if err = websocket.Message.Send(ws, msg); err != nil {
            fmt.Println("Can't send")
            break
        }
    }
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
    })
    http.Handle("/comp_ws", websocket.Handler(Echo))

    if err := http.ListenAndServe(":80", nil); err != nil {
        log.Fatal("ListenAndServe:", err)
    }
}

That’s really all there is to it! All it does is create two handles: the first one(looking at the main func) listens on / and just prints back the path and the text hello. That one is used by the readiness/liveness probes, as long as it responds with a 200 status code our application will stay running. The next one is listening on /comp_ws and is our websocket handler. It uses the golang x (non core) net library to manage all of the special “websocket” stuff, leaving us to just do what we want to do once we receive a message.

In the Echo function we print anytime we receive a reply (or “Can’t receive” on error) and then immediately send the same message back. This makes it easy to see that 1. the message has been received by the server (via watching stdout) and 2. the client can receive its message back. Writing this very very simple Websocket server made debugging much easier and eliminated the server itself as a possible source of the problems.

If you just need a websocket, that’s it! Because my server was running in Kubernetes I needed a Docker image as well. Fortunately with Go that’s also very simple.

Docker Image

To start we need to build an executable binary to include in the image. Just run go build to build the binary. Next, create the Dockerfile file:

FROM alpine:latest
RUN mkdir /app

WORKDIR /app
COPY ./test-ws .
CMD ["/app/test-ws"]

You could skip creating the /app directory but I think it’s nice to keep things separated. That’s it! Now we have our docker definition. In my case I am using skaffold so I just changed out the dockerfile directive and it built and pushed the docker image.

WSD

Now that I had my server running in the Kubernetes cluster I could try to connect to it with the wsd tool I mentioned earlier. I liked this tool since it was also Go (but also very simple to get working – I tried several others and could not get them to cooperate). One note is that go get is no longer used the way that it was when that README was written, and now you must use go install instead. Once I had run go get github.com/alexanderGugel/wsd I was able to test with wsd -url=ws://localhost. I was able to do this from pods inside the cluster and connect directly to the pod running my new server, and then also from my local machine to the cluster.

Leave a Reply

Your email address will not be published. Required fields are marked *