Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add automated test and documentation for echo example #224

Merged
merged 1 commit into from
Apr 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ci/test.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion conn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion examples/chat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
2 changes: 0 additions & 2 deletions examples/chat/chat_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// +build !js

package main

import (
Expand Down
18 changes: 0 additions & 18 deletions examples/chat/go.sum

This file was deleted.

2 changes: 1 addition & 1 deletion examples/chat/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions examples/echo/README.md
Original file line number Diff line number Diff line change
@@ -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.
161 changes: 31 additions & 130 deletions examples/echo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
81 changes: 81 additions & 0 deletions examples/echo/server.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading