From ba1c24d1c11237e77e6817b63f1203dfa18402ad Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 21:03:43 -0500 Subject: [PATCH 1/8] Add chat example Closes #174 --- ci/fmt.mk | 2 +- example-chat/chat.go | 94 +++++++++++++++++++++++++++++++++++++++++ example-chat/go.mod | 5 +++ example-chat/go.sum | 12 ++++++ example-chat/index.css | 67 +++++++++++++++++++++++++++++ example-chat/index.html | 23 ++++++++++ example-chat/index.js | 52 +++++++++++++++++++++++ example-chat/main.go | 38 +++++++++++++++++ 8 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 example-chat/chat.go create mode 100644 example-chat/go.mod create mode 100644 example-chat/go.sum create mode 100644 example-chat/index.css create mode 100644 example-chat/index.html create mode 100644 example-chat/index.js create mode 100644 example-chat/main.go diff --git a/ci/fmt.mk b/ci/fmt.mk index f313562c..3512d02f 100644 --- a/ci/fmt.mk +++ b/ci/fmt.mk @@ -13,7 +13,7 @@ goimports: gen goimports -w "-local=$$(go list -m)" . prettier: - prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn $$(git ls-files "*.yml" "*.md") + prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn $$(git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html") gen: stringer -type=opcode,MessageType,StatusCode -output=stringer.go diff --git a/example-chat/chat.go b/example-chat/chat.go new file mode 100644 index 00000000..fcd6290b --- /dev/null +++ b/example-chat/chat.go @@ -0,0 +1,94 @@ +package main + +import ( + "context" + "io" + "io/ioutil" + "log" + "net/http" + "sync" + "time" + + "nhooyr.io/websocket" +) + +type chatServer struct { + subscribersMu sync.RWMutex + subscribers map[chan []byte]struct{} +} + +func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { + println("HELLO") + + c, err := websocket.Accept(w, r, nil) + if err != nil { + log.Print(err) + return + } + + cs.subscribe(r.Context(), c) +} + +func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { + body := io.LimitReader(r.Body, 8192) + msg, err := ioutil.ReadAll(body) + if err != nil { + return + } + + cs.publish(msg) +} + +func (cs *chatServer) publish(msg []byte) { + cs.subscribersMu.RLock() + defer cs.subscribersMu.RUnlock() + + for c := range cs.subscribers { + select { + case c <- msg: + default: + } + } +} + +func (cs *chatServer) addSubscriber(msgs chan []byte) { + cs.subscribersMu.Lock() + if cs.subscribers == nil { + cs.subscribers = make(map[chan []byte]struct{}) + } + cs.subscribers[msgs] = struct{}{} + cs.subscribersMu.Unlock() +} + +func (cs *chatServer) deleteSubscriber(msgs chan []byte) { + cs.subscribersMu.Lock() + delete(cs.subscribers, msgs) + cs.subscribersMu.Unlock() +} + +func (cs *chatServer) subscribe(ctx context.Context, c *websocket.Conn) error { + ctx = c.CloseRead(ctx) + + msgs := make(chan []byte, 16) + cs.addSubscriber(msgs) + defer cs.deleteSubscriber(msgs) + + for { + select { + case msg := <-msgs: + err := writeTimeout(ctx, time.Second*5, c, msg) + if err != nil { + return err + } + case <-ctx.Done(): + return ctx.Err() + } + } +} + +func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + return c.Write(ctx, websocket.MessageText, msg) +} diff --git a/example-chat/go.mod b/example-chat/go.mod new file mode 100644 index 00000000..34fa5a69 --- /dev/null +++ b/example-chat/go.mod @@ -0,0 +1,5 @@ +module nhooyr.io/websocket/example-chat + +go 1.13 + +require nhooyr.io/websocket v1.8.2 diff --git a/example-chat/go.sum b/example-chat/go.sum new file mode 100644 index 00000000..0755fca5 --- /dev/null +++ b/example-chat/go.sum @@ -0,0 +1,12 @@ +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +nhooyr.io/websocket v1.8.2 h1:LwdzfyyOZKtVFoXay6A39Acu03KmidSZ3YUUvPa13PA= +nhooyr.io/websocket v1.8.2/go.mod h1:LiqdCg1Cu7TPWxEvPjPa0TGYxCsy4pHNTN9gGluwBpQ= diff --git a/example-chat/index.css b/example-chat/index.css new file mode 100644 index 00000000..2c2b0419 --- /dev/null +++ b/example-chat/index.css @@ -0,0 +1,67 @@ +body { + width: 100vw; + height: 100vh; + min-width: 320px; +} + +#root { + padding: 20px; + max-width: 500px; + margin: auto; + max-height: 100vh; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +#root > * + * { + margin: 20px 0 0 0; +} + +#message-log { + width: 100%; + height: 100vh; + flex-grow: 1; + overflow: auto; +} + +#message-log p:first-child { + margin-top: 0; + margin-bottom: 0; +} + +#message-log > * + * { + margin: 10px 0 0 0; +} + +#publish-form { + appearance: none; + + display: flex; + align-items: center; + justify-content: center; + width: 100%; +} + +#publish-form input[type="text"] { + flex-grow: 1; + word-break: normal; + border-radius: 5px; +} + +#publish-form input[type="submit"] { + color: white; + background-color: black; + border-radius: 5px; + margin-left: 10px; +} + +#publish-form input[type="submit"]:hover { + background-color: red; +} + +#publish-form input[type="submit"]:active { + background-color: red; +} diff --git a/example-chat/index.html b/example-chat/index.html new file mode 100644 index 00000000..3c69dd64 --- /dev/null +++ b/example-chat/index.html @@ -0,0 +1,23 @@ + + + + + nhooyr.io/websocket - Chat Example + + + + + + + + +
+
+
+ + +
+
+ + + diff --git a/example-chat/index.js b/example-chat/index.js new file mode 100644 index 00000000..2ea64db8 --- /dev/null +++ b/example-chat/index.js @@ -0,0 +1,52 @@ +;(() => { + let conn + let submitted = false + function dial() { + conn = new WebSocket(`ws://${location.host}/subscribe`) + + conn.addEventListener("close", () => { + conn = undefined + setTimeout(dial, 1000) + }) + conn.addEventListener("message", ev => { + if (typeof ev.data !== "string") { + return + } + appendLog(ev.data) + if (submitted) { + messageLog.scrollTo(0, messageLog.scrollHeight) + submitted = false + } + }) + + return conn + } + dial() + + const messageLog = document.getElementById("message-log") + const publishForm = document.getElementById("publish-form") + const messageInput = document.getElementById("message-input") + + function appendLog(text) { + const p = document.createElement("p") + p.innerText = `${new Date().toLocaleTimeString()}: ${text}` + messageLog.append(p) + } + appendLog("Submit a message to get started!") + + publishForm.onsubmit = ev => { + ev.preventDefault() + + const msg = messageInput.value + if (msg === "") { + return + } + messageInput.value = "" + + submitted = true + fetch("/publish", { + method: "POST", + body: msg, + }) + } +})() diff --git a/example-chat/main.go b/example-chat/main.go new file mode 100644 index 00000000..7a517242 --- /dev/null +++ b/example-chat/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "log" + "net" + "net/http" + "time" +) + +func main() { + err := run() + if err != nil { + log.Fatal(err) + } +} + +func run() error { + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + return err + } + fmt.Printf("listening on http://%v\n", l.Addr()) + + var ws chatServer + + m := http.NewServeMux() + m.Handle("/", http.FileServer(http.Dir("."))) + m.HandleFunc("/subscribe", ws.subscribeHandler) + m.HandleFunc("/publish", ws.publishHandler) + + s := http.Server{ + Handler: m, + ReadTimeout: time.Second * 10, + WriteTimeout: time.Second * 10, + } + return s.Serve(l) +} From bdae16ee1ec348292b42f8b288d541d1ce3be77f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 21:17:34 -0500 Subject: [PATCH 2/8] Add docs to chat example --- README.md | 2 ++ example/README.md | 25 +++++++++++++++++++++++++ {example-chat => example}/chat.go | 6 ++++-- {example-chat => example}/go.mod | 0 {example-chat => example}/go.sum | 0 {example-chat => example}/index.css | 0 {example-chat => example}/index.html | 0 {example-chat => example}/index.js | 20 +++++++++++--------- {example-chat => example}/main.go | 0 9 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 example/README.md rename {example-chat => example}/chat.go (93%) rename {example-chat => example}/go.mod (100%) rename {example-chat => example}/go.sum (100%) rename {example-chat => example}/index.css (100%) rename {example-chat => example}/index.html (100%) rename {example-chat => example}/index.js (68%) rename {example-chat => example}/main.go (100%) diff --git a/README.md b/README.md index 5dddf84a..305ccddb 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ go get nhooyr.io/websocket For a production quality example that demonstrates the complete API, see the [echo example](https://godoc.org/nhooyr.io/websocket#example-package--Echo). +For a full stack example, see the [./example](./example) subdirectory which contains a full chat example. + ### Server ```go diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000..ce5a1f1c --- /dev/null +++ b/example/README.md @@ -0,0 +1,25 @@ +# Chat Example + +This directory contains a full stack example +of a simple chat webapp using nhooyr.io/websocket. + +```bash +$ cd example +$ go run . +listening on http://127.0.0.1:51055 +``` + +Visit the printed URL to submit and view broadcasted messages in a browser. + +![Image of Example](https://i.imgur.com/iSdpZFT.png) + +## Structure + +The frontend is contained in `index.html`, `index.js` and `index.css`. It setups the +DOM with a form at the buttom to submit messages and at the top is a scrollable div +that is populated with new messages as they are broadcast. The messages are received +via a WebSocket and messages are published via a POST HTTP endpoint. + +The server portion is `main.go` and `chat.go` and implements serving the static frontend +assets as well as the `/subscribe` WebSocket endpoint for subscribing to +broadcast messages and `/publish` for publishing messages. diff --git a/example-chat/chat.go b/example/chat.go similarity index 93% rename from example-chat/chat.go rename to example/chat.go index fcd6290b..9aa36886 100644 --- a/example-chat/chat.go +++ b/example/chat.go @@ -18,8 +18,6 @@ type chatServer struct { } func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { - println("HELLO") - c, err := websocket.Accept(w, r, nil) if err != nil { log.Print(err) @@ -30,6 +28,10 @@ func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { } func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } body := io.LimitReader(r.Body, 8192) msg, err := ioutil.ReadAll(body) if err != nil { diff --git a/example-chat/go.mod b/example/go.mod similarity index 100% rename from example-chat/go.mod rename to example/go.mod diff --git a/example-chat/go.sum b/example/go.sum similarity index 100% rename from example-chat/go.sum rename to example/go.sum diff --git a/example-chat/index.css b/example/index.css similarity index 100% rename from example-chat/index.css rename to example/index.css diff --git a/example-chat/index.html b/example/index.html similarity index 100% rename from example-chat/index.html rename to example/index.html diff --git a/example-chat/index.js b/example/index.js similarity index 68% rename from example-chat/index.js rename to example/index.js index 2ea64db8..32e13c90 100644 --- a/example-chat/index.js +++ b/example/index.js @@ -1,25 +1,25 @@ ;(() => { let conn - let submitted = false + let expectingMessage = false function dial() { conn = new WebSocket(`ws://${location.host}/subscribe`) - conn.addEventListener("close", () => { - conn = undefined + conn.addEventListener("close", (ev) => { + console.error("subscribe WebSocket closed", ev) + console.info("reconnecting in 1000ms", ev) setTimeout(dial, 1000) }) conn.addEventListener("message", ev => { if (typeof ev.data !== "string") { + console.error("unexpected message type", typeof ev.data) return } appendLog(ev.data) - if (submitted) { + if (expectingMessage) { messageLog.scrollTo(0, messageLog.scrollHeight) - submitted = false + expectingMessage = false } }) - - return conn } dial() @@ -34,7 +34,7 @@ } appendLog("Submit a message to get started!") - publishForm.onsubmit = ev => { + publishForm.onsubmit = async ev => { ev.preventDefault() const msg = messageInput.value @@ -43,10 +43,12 @@ } messageInput.value = "" - submitted = true + expectingMessage = true fetch("/publish", { method: "POST", body: msg, + }).catch(err => { + console.error("failed to publish", err) }) } })() diff --git a/example-chat/main.go b/example/main.go similarity index 100% rename from example-chat/main.go rename to example/main.go From 2c8283379a032055e9057b83cd7f0ef3f54b7361 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 22:15:14 -0500 Subject: [PATCH 3/8] Make chat example responsive --- example/README.md | 2 +- example/index.css | 40 +++++++++++++++++++++++++++------------- example/index.html | 12 +++++++----- example/index.js | 15 ++++++++------- example/main.go | 13 ++++++++++--- 5 files changed, 53 insertions(+), 29 deletions(-) diff --git a/example/README.md b/example/README.md index ce5a1f1c..24544b44 100644 --- a/example/README.md +++ b/example/README.md @@ -5,7 +5,7 @@ of a simple chat webapp using nhooyr.io/websocket. ```bash $ cd example -$ go run . +$ go run . localhost:0 listening on http://127.0.0.1:51055 ``` diff --git a/example/index.css b/example/index.css index 2c2b0419..29804662 100644 --- a/example/index.css +++ b/example/index.css @@ -1,14 +1,13 @@ body { width: 100vw; - height: 100vh; min-width: 320px; } #root { - padding: 20px; - max-width: 500px; + padding: 40px 20px; + max-width: 480px; margin: auto; - max-height: 100vh; + height: 100vh; display: flex; flex-direction: column; @@ -20,42 +19,57 @@ body { margin: 20px 0 0 0; } +/* 100vh on safari does not include the bottom bar. */ +@supports (-webkit-overflow-scrolling: touch) { + #root { + height: 85vh; + } +} + #message-log { width: 100%; - height: 100vh; flex-grow: 1; overflow: auto; } #message-log p:first-child { - margin-top: 0; - margin-bottom: 0; + margin: 0; } #message-log > * + * { margin: 10px 0 0 0; } -#publish-form { - appearance: none; +#publish-form-container { + width: 100%; +} - display: flex; - align-items: center; - justify-content: center; +#publish-form { width: 100%; + display: flex; + height: 40px; +} + +#publish-form > * + * { + margin: 0 0 0 10px; } #publish-form input[type="text"] { flex-grow: 1; + + -moz-appearance: none; + -webkit-appearance: none; word-break: normal; border-radius: 5px; + border: 1px solid #ccc; } #publish-form input[type="submit"] { color: white; background-color: black; border-radius: 5px; - margin-left: 10px; + padding: 5px 10px; + border: none; } #publish-form input[type="submit"]:hover { diff --git a/example/index.html b/example/index.html index 3c69dd64..3b6e75c5 100644 --- a/example/index.html +++ b/example/index.html @@ -5,18 +5,20 @@ nhooyr.io/websocket - Chat Example - +
-
- - -
+
+
+ + +
+
diff --git a/example/index.js b/example/index.js index 32e13c90..61e938c1 100644 --- a/example/index.js +++ b/example/index.js @@ -4,19 +4,21 @@ function dial() { conn = new WebSocket(`ws://${location.host}/subscribe`) - conn.addEventListener("close", (ev) => { - console.error("subscribe WebSocket closed", ev) - console.info("reconnecting in 1000ms", ev) + conn.addEventListener("close", ev => { + console.info("websocket disconnected, reconnecting in 1000ms", ev) setTimeout(dial, 1000) }) + conn.addEventListener("open", ev => { + console.info("websocket connected") + }) conn.addEventListener("message", ev => { if (typeof ev.data !== "string") { console.error("unexpected message type", typeof ev.data) return } - appendLog(ev.data) + const p = appendLog(ev.data) if (expectingMessage) { - messageLog.scrollTo(0, messageLog.scrollHeight) + p.scrollIntoView() expectingMessage = false } }) @@ -31,6 +33,7 @@ const p = document.createElement("p") p.innerText = `${new Date().toLocaleTimeString()}: ${text}` messageLog.append(p) + return p } appendLog("Submit a message to get started!") @@ -47,8 +50,6 @@ fetch("/publish", { method: "POST", body: msg, - }).catch(err => { - console.error("failed to publish", err) }) } })() diff --git a/example/main.go b/example/main.go index 7a517242..af643f26 100644 --- a/example/main.go +++ b/example/main.go @@ -1,14 +1,17 @@ package main import ( - "fmt" + "errors" "log" "net" "net/http" + "os" "time" ) func main() { + log.SetFlags(0) + err := run() if err != nil { log.Fatal(err) @@ -16,11 +19,15 @@ func main() { } func run() error { - l, err := net.Listen("tcp", "localhost:0") + if len(os.Args) < 2 { + return errors.New("please provide an address to listen on as the first argument") + } + + l, err := net.Listen("tcp", os.Args[1]) if err != nil { return err } - fmt.Printf("listening on http://%v\n", l.Addr()) + log.Printf("listening on http://%v", l.Addr()) var ws chatServer From 4908f7846fcd785df64d1ed1070826ea282568ab Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 22:26:04 -0500 Subject: [PATCH 4/8] Cleanup example README --- example/README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/example/README.md b/example/README.md index 24544b44..8fb2b45b 100644 --- a/example/README.md +++ b/example/README.md @@ -1,7 +1,6 @@ # Chat Example -This directory contains a full stack example -of a simple chat webapp using nhooyr.io/websocket. +This directory contains a full stack example of a simple chat webapp using nhooyr.io/websocket. ```bash $ cd example @@ -15,11 +14,11 @@ Visit the printed URL to submit and view broadcasted messages in a browser. ## Structure -The frontend is contained in `index.html`, `index.js` and `index.css`. It setups the -DOM with a form at the buttom to submit messages and at the top is a scrollable div -that is populated with new messages as they are broadcast. The messages are received -via a WebSocket and messages are published via a POST HTTP endpoint. +The frontend is contained in `index.html`, `index.js` and `index.css`. It sets up the +DOM with a scrollable div at the top that is populated with new messages as they are broadcast. +At the bottom it adds a form to submit messages. +The messages are received via the WebSocket `/subscribe` endpoint and published via +the HTTP POST `/publish` endpoint. The server portion is `main.go` and `chat.go` and implements serving the static frontend -assets as well as the `/subscribe` WebSocket endpoint for subscribing to -broadcast messages and `/publish` for publishing messages. +assets, the `/subscribe` WebSocket endpoint and the HTTP POST `/publish` endpoint. From 8c89e66f640c5f857f65d89f9db374c34d1489d5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 22:28:05 -0500 Subject: [PATCH 5/8] Shorten example screenshot --- example/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/README.md b/example/README.md index 8fb2b45b..abc1ac8f 100644 --- a/example/README.md +++ b/example/README.md @@ -10,7 +10,7 @@ listening on http://127.0.0.1:51055 Visit the printed URL to submit and view broadcasted messages in a browser. -![Image of Example](https://i.imgur.com/iSdpZFT.png) +![Image of Example](https://i.imgur.com/VwJl9Bh.png) ## Structure From f8afe038afe6c68cdf4f1cafe044f75f43a5946b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 22:54:18 -0500 Subject: [PATCH 6/8] Comment chat example --- README.md | 2 +- conn_test.go | 2 +- example/README.md | 3 ++ example/chat.go | 79 +++++++++++++++++++++++++++++++++-------------- example/index.js | 13 ++++++-- example/main.go | 2 ++ 6 files changed, 72 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 305ccddb..109d0b04 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ go get nhooyr.io/websocket For a production quality example that demonstrates the complete API, see the [echo example](https://godoc.org/nhooyr.io/websocket#example-package--Echo). -For a full stack example, see the [./example](./example) subdirectory which contains a full chat example. +For a full stack example, see the [./example](./example) subdirectory which contains a chat example with a browser client. ### Server diff --git a/conn_test.go b/conn_test.go index a7bfba0a..14b7efc4 100644 --- a/conn_test.go +++ b/conn_test.go @@ -329,7 +329,7 @@ func newConnTest(t testing.TB, dialOpts *websocket.DialOptions, acceptOpts *webs } t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), time.Second * 30) tt = &connTest{t: t, ctx: ctx} tt.appendDone(cancel) diff --git a/example/README.md b/example/README.md index abc1ac8f..a0ae0cca 100644 --- a/example/README.md +++ b/example/README.md @@ -22,3 +22,6 @@ the HTTP POST `/publish` endpoint. The server portion is `main.go` and `chat.go` and implements serving the static frontend assets, the `/subscribe` WebSocket endpoint and the HTTP POST `/publish` endpoint. + +The code is well commented. I would recommend starting in `main.go` and then `chat.go` followed by +`index.html` and then `index.js`. diff --git a/example/chat.go b/example/chat.go index 9aa36886..b6910053 100644 --- a/example/chat.go +++ b/example/chat.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "io" "io/ioutil" "log" @@ -12,11 +13,14 @@ import ( "nhooyr.io/websocket" ) +// chatServer enables broadcasting to a set of subscribers. type chatServer struct { subscribersMu sync.RWMutex - subscribers map[chan []byte]struct{} + subscribers map[chan<- []byte]struct{} } +// subscribeHandler accepts the WebSocket connection and then subscribes +// it to all future messages. func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, nil) if err != nil { @@ -24,9 +28,21 @@ func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { return } - cs.subscribe(r.Context(), c) + err = cs.subscribe(r.Context(), c) + if errors.Is(err, context.Canceled) { + return + } + if websocket.CloseStatus(err) == websocket.StatusNormalClosure || + websocket.CloseStatus(err) == websocket.StatusGoingAway { + return + } + if err != nil { + log.Print(err) + } } +// publishHandler reads the request body with a limit of 8192 bytes and then publishes +// the received message. func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) @@ -35,12 +51,44 @@ func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { body := io.LimitReader(r.Body, 8192) msg, err := ioutil.ReadAll(body) if err != nil { + http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge) return } cs.publish(msg) } +// subscribe subscribes the given WebSocket to all broadcast messages. +// It creates a msgs chan with a buffer of 16 to give some room to slower +// connections and then registers it. It then listens for all messages +// and writes them to the WebSocket. If the context is cancelled or +// an error occurs, it returns and deletes the subscription. +// +// It uses CloseRead to keep reading from the connection to process control +// messages and cancel the context if the connection drops. +func (cs *chatServer) subscribe(ctx context.Context, c *websocket.Conn) error { + ctx = c.CloseRead(ctx) + + msgs := make(chan []byte, 16) + cs.addSubscriber(msgs) + defer cs.deleteSubscriber(msgs) + + for { + select { + case msg := <-msgs: + err := writeTimeout(ctx, time.Second*5, c, msg) + if err != nil { + return err + } + case <-ctx.Done(): + return ctx.Err() + } + } +} + +// publish publishes the msg to all subscribers. +// It never blocks and so messages to slow subscribers +// are dropped. func (cs *chatServer) publish(msg []byte) { cs.subscribersMu.RLock() defer cs.subscribersMu.RUnlock() @@ -53,41 +101,24 @@ func (cs *chatServer) publish(msg []byte) { } } -func (cs *chatServer) addSubscriber(msgs chan []byte) { +// addSubscriber registers a subscriber with a channel +// on which to send messages. +func (cs *chatServer) addSubscriber(msgs chan<- []byte) { cs.subscribersMu.Lock() if cs.subscribers == nil { - cs.subscribers = make(map[chan []byte]struct{}) + cs.subscribers = make(map[chan<- []byte]struct{}) } cs.subscribers[msgs] = struct{}{} cs.subscribersMu.Unlock() } +// deleteSubscriber deletes the subscriber with the given msgs channel. func (cs *chatServer) deleteSubscriber(msgs chan []byte) { cs.subscribersMu.Lock() delete(cs.subscribers, msgs) cs.subscribersMu.Unlock() } -func (cs *chatServer) subscribe(ctx context.Context, c *websocket.Conn) error { - ctx = c.CloseRead(ctx) - - msgs := make(chan []byte, 16) - cs.addSubscriber(msgs) - defer cs.deleteSubscriber(msgs) - - for { - select { - case msg := <-msgs: - err := writeTimeout(ctx, time.Second*5, c, msg) - if err != nil { - return err - } - case <-ctx.Done(): - return ctx.Err() - } - } -} - func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() diff --git a/example/index.js b/example/index.js index 61e938c1..8fb3dfb8 100644 --- a/example/index.js +++ b/example/index.js @@ -1,8 +1,10 @@ ;(() => { - let conn + // expectingMessage is set to true + // if the user has just submitted a message + // and so we should scroll the next message into view when received. let expectingMessage = false function dial() { - conn = new WebSocket(`ws://${location.host}/subscribe`) + const conn = new WebSocket(`ws://${location.host}/subscribe`) conn.addEventListener("close", ev => { console.info("websocket disconnected, reconnecting in 1000ms", ev) @@ -11,6 +13,8 @@ conn.addEventListener("open", ev => { console.info("websocket connected") }) + + // This is where we handle messages received. conn.addEventListener("message", ev => { if (typeof ev.data !== "string") { console.error("unexpected message type", typeof ev.data) @@ -29,15 +33,18 @@ const publishForm = document.getElementById("publish-form") const messageInput = document.getElementById("message-input") + // appendLog appends the passed text to messageLog. function appendLog(text) { const p = document.createElement("p") + // Adding a timestamp to each message makes the log easier to read. p.innerText = `${new Date().toLocaleTimeString()}: ${text}` messageLog.append(p) return p } appendLog("Submit a message to get started!") - publishForm.onsubmit = async ev => { + // onsubmit publishes the message from the user when the form is submitted. + publishForm.onsubmit = ev => { ev.preventDefault() const msg = messageInput.value diff --git a/example/main.go b/example/main.go index af643f26..2a520924 100644 --- a/example/main.go +++ b/example/main.go @@ -18,6 +18,8 @@ func main() { } } +// run initializes the chatServer and routes and then +// starts a http.Server for the passed in address. func run() error { if len(os.Args) < 2 { return errors.New("please provide an address to listen on as the first argument") From ce5a002f0ff92d5dd70a8884d65f60831c94ce1b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 22:57:28 -0500 Subject: [PATCH 7/8] Rename example directory to chat-example --- README.md | 2 +- {example => chat-example}/README.md | 2 +- {example => chat-example}/chat.go | 2 +- {example => chat-example}/go.mod | 0 {example => chat-example}/go.sum | 0 {example => chat-example}/index.css | 0 {example => chat-example}/index.html | 2 +- {example => chat-example}/index.js | 0 {example => chat-example}/main.go | 0 conn_test.go | 2 +- 10 files changed, 5 insertions(+), 5 deletions(-) rename {example => chat-example}/README.md (98%) rename {example => chat-example}/chat.go (98%) rename {example => chat-example}/go.mod (100%) rename {example => chat-example}/go.sum (100%) rename {example => chat-example}/index.css (100%) rename {example => chat-example}/index.html (94%) rename {example => chat-example}/index.js (100%) rename {example => chat-example}/main.go (100%) diff --git a/README.md b/README.md index 109d0b04..e967cd8a 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ go get nhooyr.io/websocket For a production quality example that demonstrates the complete API, see the [echo example](https://godoc.org/nhooyr.io/websocket#example-package--Echo). -For a full stack example, see the [./example](./example) subdirectory which contains a chat example with a browser client. +For a full stack example, see [./chat-example](./chat-example). ### Server diff --git a/example/README.md b/chat-example/README.md similarity index 98% rename from example/README.md rename to chat-example/README.md index a0ae0cca..ef06275d 100644 --- a/example/README.md +++ b/chat-example/README.md @@ -3,7 +3,7 @@ This directory contains a full stack example of a simple chat webapp using nhooyr.io/websocket. ```bash -$ cd example +$ cd chat-example $ go run . localhost:0 listening on http://127.0.0.1:51055 ``` diff --git a/example/chat.go b/chat-example/chat.go similarity index 98% rename from example/chat.go rename to chat-example/chat.go index b6910053..6b23a8a1 100644 --- a/example/chat.go +++ b/chat-example/chat.go @@ -33,7 +33,7 @@ func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { return } if websocket.CloseStatus(err) == websocket.StatusNormalClosure || - websocket.CloseStatus(err) == websocket.StatusGoingAway { + websocket.CloseStatus(err) == websocket.StatusGoingAway { return } if err != nil { diff --git a/example/go.mod b/chat-example/go.mod similarity index 100% rename from example/go.mod rename to chat-example/go.mod diff --git a/example/go.sum b/chat-example/go.sum similarity index 100% rename from example/go.sum rename to chat-example/go.sum diff --git a/example/index.css b/chat-example/index.css similarity index 100% rename from example/index.css rename to chat-example/index.css diff --git a/example/index.html b/chat-example/index.html similarity index 94% rename from example/index.html rename to chat-example/index.html index 3b6e75c5..e2383a42 100644 --- a/example/index.html +++ b/chat-example/index.html @@ -16,7 +16,7 @@
- +
diff --git a/example/index.js b/chat-example/index.js similarity index 100% rename from example/index.js rename to chat-example/index.js diff --git a/example/main.go b/chat-example/main.go similarity index 100% rename from example/main.go rename to chat-example/main.go diff --git a/conn_test.go b/conn_test.go index 14b7efc4..64e6736f 100644 --- a/conn_test.go +++ b/conn_test.go @@ -329,7 +329,7 @@ func newConnTest(t testing.TB, dialOpts *websocket.DialOptions, acceptOpts *webs } t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), time.Second * 30) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) tt = &connTest{t: t, ctx: ctx} tt.appendDone(cancel) From d44dcb91f1ce2da8356beb9621843df53fbc6ed8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 17 Feb 2020 01:01:33 -0500 Subject: [PATCH 8/8] chat-example: Add missing defer c.Close --- chat-example/chat.go | 1 + chat-example/index.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/chat-example/chat.go b/chat-example/chat.go index 6b23a8a1..e6e355d0 100644 --- a/chat-example/chat.go +++ b/chat-example/chat.go @@ -27,6 +27,7 @@ func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { log.Print(err) return } + defer c.Close(websocket.StatusInternalError, "") err = cs.subscribe(r.Context(), c) if errors.Is(err, context.Canceled) { diff --git a/chat-example/index.html b/chat-example/index.html index e2383a42..76ae8370 100644 --- a/chat-example/index.html +++ b/chat-example/index.html @@ -16,7 +16,7 @@
- +