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, "") +}
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: