diff --git a/ci/test.mk b/ci/test.mk index b2f92b7c..553a05c5 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -14,4 +14,4 @@ gotest: go test -timeout=30m -covermode=atomic -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... sed -i '/stringer\.go/d' ci/out/coverage.prof sed -i '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof - sed -i '/example/d' ci/out/coverage.prof + sed -i '/examples/d' ci/out/coverage.prof diff --git a/conn_test.go b/conn_test.go index 451d093a..28e8d59d 100644 --- a/conn_test.go +++ b/conn_test.go @@ -295,7 +295,7 @@ func TestWasm(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", "./...") + cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", ".") cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", s.URL)) b, err := cmd.CombinedOutput() diff --git a/examples/chat/README.md b/examples/chat/README.md index a4c99a93..57424220 100644 --- a/examples/chat/README.md +++ b/examples/chat/README.md @@ -3,7 +3,7 @@ This directory contains a full stack example of a simple chat webapp using nhooyr.io/websocket. ```bash -$ cd chat-example +$ cd examples/chat $ go run . localhost:0 listening on http://127.0.0.1:51055 ``` diff --git a/examples/chat/chat_test.go b/examples/chat/chat_test.go index eae18580..f80f1de1 100644 --- a/examples/chat/chat_test.go +++ b/examples/chat/chat_test.go @@ -1,5 +1,3 @@ -// +build !js - package main import ( diff --git a/examples/chat/go.sum b/examples/chat/go.sum deleted file mode 100644 index e4bbd62d..00000000 --- a/examples/chat/go.sum +++ /dev/null @@ -1,18 +0,0 @@ -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/klauspost/compress v1.10.0 h1:92XGj1AcYzA6UrVdd4qIIBrT8OroryvRvdmg/IfmC7Y= -github.com/klauspost/compress v1.10.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/examples/chat/main.go b/examples/chat/main.go index 7f3cf6f3..3fcec6be 100644 --- a/examples/chat/main.go +++ b/examples/chat/main.go @@ -20,7 +20,7 @@ func main() { } } -// run initializes the chatServer and routes and then +// run initializes the chatServer and then // starts a http.Server for the passed in address. func run() error { if len(os.Args) < 2 { diff --git a/examples/echo/README.md b/examples/echo/README.md new file mode 100644 index 00000000..7f42c3c5 --- /dev/null +++ b/examples/echo/README.md @@ -0,0 +1,21 @@ +# Echo Example + +This directory contains a echo server example using nhooyr.io/websocket. + +```bash +$ cd examples/echo +$ go run . localhost:0 +listening on http://127.0.0.1:51055 +``` + +You can use a WebSocket client like https://github.com/hashrocket/ws to connect. All messages +written will be echoed back. + +## Structure + +The server is in `server.go` and is implemented as a `http.HandlerFunc` that accepts the WebSocket +and then reads all messages and writes them exactly as is back to the connection. + +`server_test.go` contains a small unit test to verify it works correctly. + +`main.go` brings it all together so that you can run it and play around with it. diff --git a/examples/echo/main.go b/examples/echo/main.go index f1771752..16d78a79 100644 --- a/examples/echo/main.go +++ b/examples/echo/main.go @@ -3,158 +3,59 @@ package main import ( "context" "errors" - "fmt" - "io" "log" "net" "net/http" + "os" + "os/signal" "time" - - "golang.org/x/time/rate" - - "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" ) -// This example starts a WebSocket echo server, -// dials the server and then sends 5 different messages -// and prints out the server's responses. func main() { - // First we listen on port 0 which means the OS will - // assign us a random free port. This is the listener - // the server will serve on and the client will connect to. - l, err := net.Listen("tcp", "localhost:0") - if err != nil { - log.Fatalf("failed to listen: %v", err) - } - defer l.Close() - - s := &http.Server{ - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := echoServer(w, r) - if err != nil { - log.Printf("echo server: %v", err) - } - }), - ReadTimeout: time.Second * 15, - WriteTimeout: time.Second * 15, - } - defer s.Close() - - // This starts the echo server on the listener. - go func() { - err := s.Serve(l) - if err != http.ErrServerClosed { - log.Fatalf("failed to listen and serve: %v", err) - } - }() + log.SetFlags(0) - // Now we dial the server, send the messages and echo the responses. - err = client("ws://" + l.Addr().String()) + err := run() if err != nil { - log.Fatalf("client failed: %v", err) - } - - // Output: - // received: map[i:0] - // received: map[i:1] - // received: map[i:2] - // received: map[i:3] - // received: map[i:4] -} - -// echoServer is the WebSocket echo server implementation. -// It ensures the client speaks the echo subprotocol and -// only allows one message every 100ms with a 10 message burst. -func echoServer(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"echo"}, - }) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "the sky is falling") - - if c.Subprotocol() != "echo" { - c.Close(websocket.StatusPolicyViolation, "client must speak the echo subprotocol") - return errors.New("client does not speak echo sub protocol") - } - - l := rate.NewLimiter(rate.Every(time.Millisecond*100), 10) - for { - err = echo(r.Context(), c, l) - if websocket.CloseStatus(err) == websocket.StatusNormalClosure { - return nil - } - if err != nil { - return fmt.Errorf("failed to echo with %v: %w", r.RemoteAddr, err) - } + log.Fatal(err) } } -// echo reads from the WebSocket connection and then writes -// the received message back to it. -// The entire function has 10s to complete. -func echo(ctx context.Context, c *websocket.Conn, l *rate.Limiter) error { - ctx, cancel := context.WithTimeout(ctx, time.Second*10) - defer cancel() - - err := l.Wait(ctx) - if err != nil { - return err +// run starts a http.Server for the passed in address +// with all requests handled by echoServer. +func run() error { + if len(os.Args) < 2 { + return errors.New("please provide an address to listen on as the first argument") } - typ, r, err := c.Reader(ctx) + l, err := net.Listen("tcp", os.Args[1]) if err != nil { return err } + log.Printf("listening on http://%v", l.Addr()) - w, err := c.Writer(ctx, typ) - if err != nil { - return err + s := &http.Server{ + Handler: echoServer{ + logf: log.Printf, + }, + ReadTimeout: time.Second * 10, + WriteTimeout: time.Second * 10, } + errc := make(chan error, 1) + go func() { + errc <- s.Serve(l) + }() - _, err = io.Copy(w, r) - if err != nil { - return fmt.Errorf("failed to io.Copy: %w", err) + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt) + select { + case err := <-errc: + log.Printf("failed to serve: %v", err) + case sig := <-sigs: + log.Printf("terminating: %v", sig) } - err = w.Close() - return err -} - -// client dials the WebSocket echo server at the given url. -// It then sends it 5 different messages and echo's the server's -// response to each. -func client(url string) error { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - c, _, err := websocket.Dial(ctx, url, &websocket.DialOptions{ - Subprotocols: []string{"echo"}, - }) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "the sky is falling") - - for i := 0; i < 5; i++ { - err = wsjson.Write(ctx, c, map[string]int{ - "i": i, - }) - if err != nil { - return err - } - - v := map[string]int{} - err = wsjson.Read(ctx, c, &v) - if err != nil { - return err - } - - fmt.Printf("received: %v\n", v) - } - - c.Close(websocket.StatusNormalClosure, "") - return nil + return s.Shutdown(ctx) } diff --git a/examples/echo/server.go b/examples/echo/server.go new file mode 100644 index 00000000..308c4a5e --- /dev/null +++ b/examples/echo/server.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "time" + + "golang.org/x/time/rate" + + "nhooyr.io/websocket" +) + +// echoServer is the WebSocket echo server implementation. +// It ensures the client speaks the echo subprotocol and +// only allows one message every 100ms with a 10 message burst. +type echoServer struct { + + // logf controls where logs are sent. + logf func(f string, v ...interface{}) +} + +func (s echoServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"echo"}, + }) + if err != nil { + s.logf("%v", err) + return + } + defer c.Close(websocket.StatusInternalError, "the sky is falling") + + if c.Subprotocol() != "echo" { + c.Close(websocket.StatusPolicyViolation, "client must speak the echo subprotocol") + return + } + + l := rate.NewLimiter(rate.Every(time.Millisecond*100), 10) + for { + err = echo(r.Context(), c, l) + if websocket.CloseStatus(err) == websocket.StatusNormalClosure { + return + } + if err != nil { + s.logf("failed to echo with %v: %v", r.RemoteAddr, err) + return + } + } +} + +// echo reads from the WebSocket connection and then writes +// the received message back to it. +// The entire function has 10s to complete. +func echo(ctx context.Context, c *websocket.Conn, l *rate.Limiter) error { + ctx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + err := l.Wait(ctx) + if err != nil { + return err + } + + typ, r, err := c.Reader(ctx) + if err != nil { + return err + } + + w, err := c.Writer(ctx, typ) + if err != nil { + return err + } + + _, err = io.Copy(w, r) + if err != nil { + return fmt.Errorf("failed to io.Copy: %w", err) + } + + err = w.Close() + return err +} diff --git a/examples/echo/server_test.go b/examples/echo/server_test.go new file mode 100644 index 00000000..9b608301 --- /dev/null +++ b/examples/echo/server_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "context" + "net/http/httptest" + "testing" + "time" + + "nhooyr.io/websocket" + "nhooyr.io/websocket/wsjson" +) + +// Test_echoServer tests the echoServer by sending it 5 different messages +// and ensuring the responses all match. +func Test_echoServer(t *testing.T) { + t.Parallel() + + s := httptest.NewServer(echoServer{ + logf: t.Logf, + }) + defer s.Close() + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + c, _, err := websocket.Dial(ctx, s.URL, &websocket.DialOptions{ + Subprotocols: []string{"echo"}, + }) + if err != nil { + t.Fatal(err) + } + defer c.Close(websocket.StatusInternalError, "the sky is falling") + + for i := 0; i < 5; i++ { + err = wsjson.Write(ctx, c, map[string]int{ + "i": i, + }) + if err != nil { + t.Fatal(err) + } + + v := map[string]int{} + err = wsjson.Read(ctx, c, &v) + if err != nil { + t.Fatal(err) + } + + if v["i"] != i { + t.Fatalf("expected %v but got %v", i, v) + } + } + + c.Close(websocket.StatusNormalClosure, "") +}