Skip to content

Commit c40b702

Browse files
committed
Add automated test and documentation for echo example
Closes coder#223
1 parent 71a12fb commit c40b702

File tree

10 files changed

+191
-154
lines changed

10 files changed

+191
-154
lines changed

ci/test.mk

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ gotest:
1414
go test -timeout=30m -covermode=atomic -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./...
1515
sed -i '/stringer\.go/d' ci/out/coverage.prof
1616
sed -i '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof
17-
sed -i '/example/d' ci/out/coverage.prof
17+
sed -i '/examples/d' ci/out/coverage.prof

conn_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ func TestWasm(t *testing.T) {
295295
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
296296
defer cancel()
297297

298-
cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", "./...")
298+
cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", ".")
299299
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", s.URL))
300300

301301
b, err := cmd.CombinedOutput()

examples/chat/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This directory contains a full stack example of a simple chat webapp using nhooyr.io/websocket.
44

55
```bash
6-
$ cd chat-example
6+
$ cd examples/chat
77
$ go run . localhost:0
88
listening on http://127.0.0.1:51055
99
```

examples/chat/chat_test.go

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// +build !js
2-
31
package main
42

53
import (

examples/chat/go.sum

-18
This file was deleted.

examples/chat/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func main() {
2020
}
2121
}
2222

23-
// run initializes the chatServer and routes and then
23+
// run initializes the chatServer and then
2424
// starts a http.Server for the passed in address.
2525
func run() error {
2626
if len(os.Args) < 2 {

examples/echo/README.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Echo Example
2+
3+
This directory contains a echo server example using nhooyr.io/websocket.
4+
5+
```bash
6+
$ cd examples/echo
7+
$ go run . localhost:0
8+
listening on http://127.0.0.1:51055
9+
```
10+
11+
You can use a WebSocket client like https://github.com/hashrocket/ws to connect. All messages
12+
written will be echoed back.
13+
14+
## Structure
15+
16+
The server is in `server.go` and is implemented as a `http.HandlerFunc` that accepts the WebSocket
17+
and then reads all messages and writes them exactly as is back to the connection.
18+
19+
`server_test.go` contains a small unit test to verify it works correctly.
20+
21+
`main.go` brings it all together so that you can run it and play around with it.

examples/echo/main.go

+31-130
Original file line numberDiff line numberDiff line change
@@ -3,158 +3,59 @@ package main
33
import (
44
"context"
55
"errors"
6-
"fmt"
7-
"io"
86
"log"
97
"net"
108
"net/http"
9+
"os"
10+
"os/signal"
1111
"time"
12-
13-
"golang.org/x/time/rate"
14-
15-
"nhooyr.io/websocket"
16-
"nhooyr.io/websocket/wsjson"
1712
)
1813

19-
// This example starts a WebSocket echo server,
20-
// dials the server and then sends 5 different messages
21-
// and prints out the server's responses.
2214
func main() {
23-
// First we listen on port 0 which means the OS will
24-
// assign us a random free port. This is the listener
25-
// the server will serve on and the client will connect to.
26-
l, err := net.Listen("tcp", "localhost:0")
27-
if err != nil {
28-
log.Fatalf("failed to listen: %v", err)
29-
}
30-
defer l.Close()
31-
32-
s := &http.Server{
33-
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
34-
err := echoServer(w, r)
35-
if err != nil {
36-
log.Printf("echo server: %v", err)
37-
}
38-
}),
39-
ReadTimeout: time.Second * 15,
40-
WriteTimeout: time.Second * 15,
41-
}
42-
defer s.Close()
43-
44-
// This starts the echo server on the listener.
45-
go func() {
46-
err := s.Serve(l)
47-
if err != http.ErrServerClosed {
48-
log.Fatalf("failed to listen and serve: %v", err)
49-
}
50-
}()
15+
log.SetFlags(0)
5116

52-
// Now we dial the server, send the messages and echo the responses.
53-
err = client("ws://" + l.Addr().String())
17+
err := run()
5418
if err != nil {
55-
log.Fatalf("client failed: %v", err)
56-
}
57-
58-
// Output:
59-
// received: map[i:0]
60-
// received: map[i:1]
61-
// received: map[i:2]
62-
// received: map[i:3]
63-
// received: map[i:4]
64-
}
65-
66-
// echoServer is the WebSocket echo server implementation.
67-
// It ensures the client speaks the echo subprotocol and
68-
// only allows one message every 100ms with a 10 message burst.
69-
func echoServer(w http.ResponseWriter, r *http.Request) error {
70-
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
71-
Subprotocols: []string{"echo"},
72-
})
73-
if err != nil {
74-
return err
75-
}
76-
defer c.Close(websocket.StatusInternalError, "the sky is falling")
77-
78-
if c.Subprotocol() != "echo" {
79-
c.Close(websocket.StatusPolicyViolation, "client must speak the echo subprotocol")
80-
return errors.New("client does not speak echo sub protocol")
81-
}
82-
83-
l := rate.NewLimiter(rate.Every(time.Millisecond*100), 10)
84-
for {
85-
err = echo(r.Context(), c, l)
86-
if websocket.CloseStatus(err) == websocket.StatusNormalClosure {
87-
return nil
88-
}
89-
if err != nil {
90-
return fmt.Errorf("failed to echo with %v: %w", r.RemoteAddr, err)
91-
}
19+
log.Fatal(err)
9220
}
9321
}
9422

95-
// echo reads from the WebSocket connection and then writes
96-
// the received message back to it.
97-
// The entire function has 10s to complete.
98-
func echo(ctx context.Context, c *websocket.Conn, l *rate.Limiter) error {
99-
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
100-
defer cancel()
101-
102-
err := l.Wait(ctx)
103-
if err != nil {
104-
return err
23+
// run starts a http.Server for the passed in address
24+
// with all requests handled by echoServer.
25+
func run() error {
26+
if len(os.Args) < 2 {
27+
return errors.New("please provide an address to listen on as the first argument")
10528
}
10629

107-
typ, r, err := c.Reader(ctx)
30+
l, err := net.Listen("tcp", os.Args[1])
10831
if err != nil {
10932
return err
11033
}
34+
log.Printf("listening on http://%v", l.Addr())
11135

112-
w, err := c.Writer(ctx, typ)
113-
if err != nil {
114-
return err
36+
s := &http.Server{
37+
Handler: echoServer{
38+
logf: log.Printf,
39+
},
40+
ReadTimeout: time.Second * 10,
41+
WriteTimeout: time.Second * 10,
11542
}
43+
errc := make(chan error, 1)
44+
go func() {
45+
errc <- s.Serve(l)
46+
}()
11647

117-
_, err = io.Copy(w, r)
118-
if err != nil {
119-
return fmt.Errorf("failed to io.Copy: %w", err)
48+
sigs := make(chan os.Signal, 1)
49+
signal.Notify(sigs, os.Interrupt)
50+
select {
51+
case err := <-errc:
52+
log.Printf("failed to serve: %v", err)
53+
case sig := <-sigs:
54+
log.Printf("terminating: %v", sig)
12055
}
12156

122-
err = w.Close()
123-
return err
124-
}
125-
126-
// client dials the WebSocket echo server at the given url.
127-
// It then sends it 5 different messages and echo's the server's
128-
// response to each.
129-
func client(url string) error {
130-
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
57+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
13158
defer cancel()
13259

133-
c, _, err := websocket.Dial(ctx, url, &websocket.DialOptions{
134-
Subprotocols: []string{"echo"},
135-
})
136-
if err != nil {
137-
return err
138-
}
139-
defer c.Close(websocket.StatusInternalError, "the sky is falling")
140-
141-
for i := 0; i < 5; i++ {
142-
err = wsjson.Write(ctx, c, map[string]int{
143-
"i": i,
144-
})
145-
if err != nil {
146-
return err
147-
}
148-
149-
v := map[string]int{}
150-
err = wsjson.Read(ctx, c, &v)
151-
if err != nil {
152-
return err
153-
}
154-
155-
fmt.Printf("received: %v\n", v)
156-
}
157-
158-
c.Close(websocket.StatusNormalClosure, "")
159-
return nil
60+
return s.Shutdown(ctx)
16061
}

examples/echo/server.go

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"time"
9+
10+
"golang.org/x/time/rate"
11+
12+
"nhooyr.io/websocket"
13+
)
14+
15+
// echoServer is the WebSocket echo server implementation.
16+
// It ensures the client speaks the echo subprotocol and
17+
// only allows one message every 100ms with a 10 message burst.
18+
type echoServer struct {
19+
20+
// logf controls where logs are sent.
21+
logf func(f string, v ...interface{})
22+
}
23+
24+
func (s echoServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
25+
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
26+
Subprotocols: []string{"echo"},
27+
})
28+
if err != nil {
29+
s.logf("%v", err)
30+
return
31+
}
32+
defer c.Close(websocket.StatusInternalError, "the sky is falling")
33+
34+
if c.Subprotocol() != "echo" {
35+
c.Close(websocket.StatusPolicyViolation, "client must speak the echo subprotocol")
36+
return
37+
}
38+
39+
l := rate.NewLimiter(rate.Every(time.Millisecond*100), 10)
40+
for {
41+
err = echo(r.Context(), c, l)
42+
if websocket.CloseStatus(err) == websocket.StatusNormalClosure {
43+
return
44+
}
45+
if err != nil {
46+
s.logf("failed to echo with %v: %v", r.RemoteAddr, err)
47+
return
48+
}
49+
}
50+
}
51+
52+
// echo reads from the WebSocket connection and then writes
53+
// the received message back to it.
54+
// The entire function has 10s to complete.
55+
func echo(ctx context.Context, c *websocket.Conn, l *rate.Limiter) error {
56+
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
57+
defer cancel()
58+
59+
err := l.Wait(ctx)
60+
if err != nil {
61+
return err
62+
}
63+
64+
typ, r, err := c.Reader(ctx)
65+
if err != nil {
66+
return err
67+
}
68+
69+
w, err := c.Writer(ctx, typ)
70+
if err != nil {
71+
return err
72+
}
73+
74+
_, err = io.Copy(w, r)
75+
if err != nil {
76+
return fmt.Errorf("failed to io.Copy: %w", err)
77+
}
78+
79+
err = w.Close()
80+
return err
81+
}

0 commit comments

Comments
 (0)