Merge branch '19-websocket-task' into 'master'

Resolve "websocket task"

Closes #19

See merge request slon/shad-go-private!25
This commit is contained in:
verytable 2020-04-04 13:27:53 +00:00
commit 1696ee67c2
5 changed files with 184 additions and 0 deletions

2
go.mod
View file

@ -9,11 +9,13 @@ require (
github.com/google/go-cmp v0.4.0
github.com/gorilla/handlers v1.4.2
github.com/gorilla/mux v1.7.4
github.com/gorilla/websocket v1.4.2
github.com/spf13/cobra v0.0.5
github.com/stretchr/testify v1.4.0
go.uber.org/goleak v1.0.0
golang.org/x/net v0.0.0-20190628185345-da137c7871d7
golang.org/x/perf v0.0.0-20191209155426-36b577b0eb03
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/tools v0.0.0-20200125223703-d33eef8e6825
gopkg.in/yaml.v2 v2.2.8
)

2
go.sum
View file

@ -32,6 +32,8 @@ github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YAR
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=

33
wscat/README.md Normal file
View file

@ -0,0 +1,33 @@
## wscat
wscat - примитивный аналог npm пакета [wscat](https://www.npmjs.com/package/wscat).
Websocket - это двусторониий канал поверх tcp. wscat - это websocket клиент.
wscat принимает на вход единственный аргумент `-addr` - адрес websocket сервера.
После подключения программа начинает читать с stdin'а и отправлять пользовательские строки на сервер,
печатая все сообщения от сервера в stdout.
Клиент должен обрабатывать SIGINT и SIGTERM и плавно завершаться с кодом 0 дожидаясь горутин.
Для этого может пригодиться [context](https://golang.org/pkg/context/).
Обратите внимание на то, что exit code `go run` - это не exit code исполняемого файла.
## Пример
Публичный echo сервер:
```
✗ $GOPATH/bin/wscat -addr ws://echo.websocket.org
abc
abcdef
def^C2020/04/04 05:01:32 received signal interrupt
```
```
✗ echo $?
0
```
## Ссылки
1. websocket: https://en.wikipedia.org/wiki/WebSocket
2. signal shutdown: https://p.go.manytask.org/06-http/lecture.slide#20

7
wscat/main.go Normal file
View file

@ -0,0 +1,7 @@
// +build !solution
package main
func main() {
}

140
wscat/main_test.go Normal file
View file

@ -0,0 +1,140 @@
package main
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"strings"
"syscall"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gitlab.com/slon/shad-go/tools/testtool"
)
const importPath = "gitlab.com/slon/shad-go/wscat"
var binCache testtool.BinCache
func TestMain(m *testing.M) {
os.Exit(func() int {
var teardown testtool.CloseFunc
binCache, teardown = testtool.NewBinCache()
defer teardown()
return m.Run()
}())
}
type Conn struct {
in io.WriteCloser
out *bytes.Buffer
}
func startCommand(t *testing.T, addr string) (conn *Conn, stop func()) {
t.Helper()
binary, err := binCache.GetBinary(importPath)
require.NoError(t, err)
cmd := exec.Command(binary, "-addr", addr)
cmd.Stderr = os.Stderr
out := &bytes.Buffer{}
cmd.Stdout = out
stdin, err := cmd.StdinPipe()
require.NoError(t, err)
require.NoError(t, cmd.Start())
conn = &Conn{
in: stdin,
out: out,
}
done := make(chan struct{})
go func() {
assert.NoError(t, cmd.Wait())
close(done)
}()
stop = func() {
defer func() {
_ = cmd.Process.Kill()
<-done
}()
// try killing softly
_ = cmd.Process.Signal(syscall.SIGTERM)
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
defer cancel()
select {
case <-done:
case <-ctx.Done():
t.Fatalf("client shutdown timed out")
}
}
return
}
func TestWScat(t *testing.T) {
upgrader := websocket.Upgrader{}
var received, sent [][]byte
h := func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
require.NoError(t, err)
defer func() { _ = c.Close() }()
for {
_, message, err := c.ReadMessage()
if err != nil {
t.Logf("error reading message: %s", err)
break
}
received = append(received, message)
resp := []byte(testtool.RandomName())
err = c.WriteMessage(websocket.TextMessage, resp)
require.NoError(t, err)
sent = append(sent, resp)
}
}
s := httptest.NewServer(http.HandlerFunc(h))
defer s.Close()
wsURL := strings.Replace(s.URL, "http", "ws", 1)
t.Logf("starting ws server %s", wsURL)
conn, stop := startCommand(t, wsURL)
defer stop()
var in [][]byte
for i := 0; i < 100; i++ {
msg := []byte(testtool.RandomName())
in = append(in, msg)
_, err := conn.in.Write(append(msg, '\n'))
require.NoError(t, err)
}
// give client time to make a request
time.Sleep(time.Millisecond * 100)
stop()
require.Equal(t, bytes.Join(in, nil), bytes.Join(received, nil))
require.Equal(t, bytes.Join(sent, nil), conn.out.Bytes())
}