Hashrocket.com / blog

Bg default article large

Development of a Simple Command Line Websocket Client

posted on and written by in

Image 100x100 jack christensen

While working on the Websocket Shootout, I frequently needed to connect to a websocket server and interact directly. None of the existing tools I was aware of had exactly the features I was wanted. That led to the creation of ws.

The goals for ws were as follows:

  • Telnet style interaction with remote websocket servers
  • Persistent, readline style history
  • Cross-platform
  • No or minimal dependencies

Example usage recording

Go was an obvious choice for meeting the cross-platform and dependency requirements. Thanks to excellent library support the rest of the features were easy to build.

The github.com/spf13/cobra is used for command line argument parsing. The Go standard library flag package would have been sufficient, but I prefer GNU style flags. cobra makes it easy to define arguments.

rootCmd := &cobra.Command{
  Use:   "ws URL",
  Short: "websocket tool",
  Run:   root,
}
rootCmd.Flags().StringVarP(&options.origin, "origin", "o", "", "websocket origin")
rootCmd.Flags().BoolVarP(&options.printVersion, "version", "v", false, "print version")

The websocket protocol is handled by the github.com/gorilla/websocket package. With one Dial function call, we get a connection that let's us work at the websocket message level.

headers := make(http.Header)
headers.Add("Origin", origin)

ws, _, err := websocket.DefaultDialer.Dial(url, headers)
if err != nil {
    return err
}

github.com/chzyer/readline provided GNU readline style functionality in pure Go. One command initializes the readline system.

rlConf := &readline.Config{
  Prompt:      "> ",
  HistoryFile: historyFile,
}

// ...

rl, err := readline.NewEx(rlConf)
if err != nil {
  return err
}

Reading a line is similarly easy.

line, err := s.rl.Readline()
if err != nil {
  s.errChan <- err
  return
}

I encountered one bug/issue, but the author pushed a fix in less than a day of my post.

The github.com/fatih/color library made it easy to output color text in a cross platform way. It lets you get a Sprintf style function that wraps the text in the appropriate escape characters for a given color.

rxSprintf := color.New(color.FgGreen).SprintfFunc()

Goroutines made handling keyboard input and websocket IO simple by treating them independently.

// ...
  go sess.readConsole()
  go sess.readWebsocket()
// ...

func (s *session) readConsole() {
  for {
    line, err := s.rl.Readline()
    if err != nil {
      s.errChan <- err
      return
    }

    _, err = io.WriteString(s.ws, line)
    if err != nil {
      s.errChan <- err
      return
    }
  }
}

func (s *session) readWebsocket() {
    rxSprintf := color.New(color.FgGreen).SprintfFunc()

    for {
        _, buf, err := s.ws.ReadMessage()
        if err != nil {
            s.errChan <- err
            return
        }
        fmt.Fprint(s.rl.Stdout(), rxSprintf("< %s\n", string(buf)))
    }
}

The entire program is just over 130 lines. You can check it out at github.com/hashrocket/ws.

If you have a working Go installation, you can build ws with a single command:

go get -u github.com/hashrocket/ws

Posted in Community and tagged with websocket go cli