From 0eca9f6e5e301f57baea7dd8e135436d9be9e5ba Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Tue, 16 Nov 2021 10:54:49 +0200 Subject: [PATCH] add example for websocket handler relative to: https://github.com/kataras/muxie/issues/12 --- _examples/14_websocket/README.md | 107 +++++++++++++++++++++++++ _examples/14_websocket/client.go | 133 +++++++++++++++++++++++++++++++ _examples/14_websocket/go.mod | 8 ++ _examples/14_websocket/go.sum | 4 + _examples/14_websocket/home.html | 98 +++++++++++++++++++++++ _examples/14_websocket/hub.go | 49 ++++++++++++ _examples/14_websocket/main.go | 42 ++++++++++ 7 files changed, 441 insertions(+) create mode 100644 _examples/14_websocket/README.md create mode 100644 _examples/14_websocket/client.go create mode 100644 _examples/14_websocket/go.mod create mode 100644 _examples/14_websocket/go.sum create mode 100644 _examples/14_websocket/home.html create mode 100644 _examples/14_websocket/hub.go create mode 100644 _examples/14_websocket/main.go diff --git a/_examples/14_websocket/README.md b/_examples/14_websocket/README.md new file mode 100644 index 0000000..11ef0af --- /dev/null +++ b/_examples/14_websocket/README.md @@ -0,0 +1,107 @@ +# Chat Example + +A clone of https://github.com/gorilla/websocket/blob/master/examples/chat written for Muxie. + +------- + +This application shows how to use the +[websocket](https://github.com/gorilla/websocket) package to implement a simple +web chat application. + +## Running the example + +The example requires a working Go development environment. The [Getting +Started](http://golang.org/doc/install) page describes how to install the +development environment. + +Once you have Go up and running, you can download, build and run the example +using the following commands. + + $ go mod init my_project + $ go get github.com/kataras/muxie@latest + $ go get github.com/gorilla/websocket@latest + $ go run . # or go build && ./my_project + +To use the chat example, open http://localhost:8080/ in your browser. + +## Server + +The server application defines two types, `Client` and `Hub`. The server +creates an instance of the `Client` type for each websocket connection. A +`Client` acts as an intermediary between the websocket connection and a single +instance of the `Hub` type. The `Hub` maintains a set of registered clients and +broadcasts messages to the clients. + +The application runs one goroutine for the `Hub` and two goroutines for each +`Client`. The goroutines communicate with each other using channels. The `Hub` +has channels for registering clients, unregistering clients and broadcasting +messages. A `Client` has a buffered channel of outbound messages. One of the +client's goroutines reads messages from this channel and writes the messages to +the websocket. The other client goroutine reads messages from the websocket and +sends them to the hub. + +### Hub + +The code for the `Hub` type is in +[hub.go](https://github.com/gorilla/websocket/blob/master/examples/chat/hub.go). +The application's `main` function starts the hub's `run` method as a goroutine. +Clients send requests to the hub using the `register`, `unregister` and +`broadcast` channels. + +The hub registers clients by adding the client pointer as a key in the +`clients` map. The map value is always true. + +The unregister code is a little more complicated. In addition to deleting the +client pointer from the `clients` map, the hub closes the clients's `send` +channel to signal the client that no more messages will be sent to the client. + +The hub handles messages by looping over the registered clients and sending the +message to the client's `send` channel. If the client's `send` buffer is full, +then the hub assumes that the client is dead or stuck. In this case, the hub +unregisters the client and closes the websocket. + +### Client + +The code for the `Client` type is in [client.go](https://github.com/gorilla/websocket/blob/master/examples/chat/client.go). + +The `serveWs` function is registered by the application's `main` function as +an HTTP handler. The handler upgrades the HTTP connection to the WebSocket +protocol, creates a client, registers the client with the hub and schedules the +client to be unregistered using a defer statement. + +Next, the HTTP handler starts the client's `writePump` method as a goroutine. +This method transfers messages from the client's send channel to the websocket +connection. The writer method exits when the channel is closed by the hub or +there's an error writing to the websocket connection. + +Finally, the HTTP handler calls the client's `readPump` method. This method +transfers inbound messages from the websocket to the hub. + +WebSocket connections [support one concurrent reader and one concurrent +writer](https://godoc.org/github.com/gorilla/websocket#hdr-Concurrency). The +application ensures that these concurrency requirements are met by executing +all reads from the `readPump` goroutine and all writes from the `writePump` +goroutine. + +To improve efficiency under high load, the `writePump` function coalesces +pending chat messages in the `send` channel to a single WebSocket message. This +reduces the number of system calls and the amount of data sent over the +network. + +## Frontend + +The frontend code is in [home.html](https://github.com/gorilla/websocket/blob/master/examples/chat/home.html). + +On document load, the script checks for websocket functionality in the browser. +If websocket functionality is available, then the script opens a connection to +the server and registers a callback to handle messages from the server. The +callback appends the message to the chat log using the appendLog function. + +To allow the user to manually scroll through the chat log without interruption +from new messages, the `appendLog` function checks the scroll position before +adding new content. If the chat log is scrolled to the bottom, then the +function scrolls new content into view after adding the content. Otherwise, the +scroll position is not changed. + +The form handler writes the user input to the websocket and clears the input +field. \ No newline at end of file diff --git a/_examples/14_websocket/client.go b/_examples/14_websocket/client.go new file mode 100644 index 0000000..d64fe18 --- /dev/null +++ b/_examples/14_websocket/client.go @@ -0,0 +1,133 @@ +package main + +import ( + "bytes" + "log" + "net/http" + "time" + + "github.com/gorilla/websocket" +) + +const ( + // Time allowed to write a message to the peer. + writeWait = 10 * time.Second + + // Time allowed to read the next pong message from the peer. + pongWait = 60 * time.Second + + // Send pings to peer with this period. Must be less than pongWait. + pingPeriod = (pongWait * 9) / 10 + + // Maximum message size allowed from peer. + maxMessageSize = 512 +) + +var ( + newline = []byte{'\n'} + space = []byte{' '} +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +// Client is a middleman between the websocket connection and the hub. +type Client struct { + hub *Hub + + // The websocket connection. + conn *websocket.Conn + + // Buffered channel of outbound messages. + send chan []byte +} + +// readPump pumps messages from the websocket connection to the hub. +// +// The application runs readPump in a per-connection goroutine. The application +// ensures that there is at most one reader on a connection by executing all +// reads from this goroutine. +func (c *Client) readPump() { + defer func() { + c.hub.unregister <- c + c.conn.Close() + }() + c.conn.SetReadLimit(maxMessageSize) + c.conn.SetReadDeadline(time.Now().Add(pongWait)) + c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) + for { + _, message, err := c.conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + log.Printf("error: %v", err) + } + break + } + message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1)) + c.hub.broadcast <- message + } +} + +// writePump pumps messages from the hub to the websocket connection. +// +// A goroutine running writePump is started for each connection. The +// application ensures that there is at most one writer to a connection by +// executing all writes from this goroutine. +func (c *Client) writePump() { + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + c.conn.Close() + }() + for { + select { + case message, ok := <-c.send: + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) + if !ok { + // The hub closed the channel. + c.conn.WriteMessage(websocket.CloseMessage, []byte{}) + return + } + + w, err := c.conn.NextWriter(websocket.TextMessage) + if err != nil { + return + } + w.Write(message) + + // Add queued chat messages to the current websocket message. + n := len(c.send) + for i := 0; i < n; i++ { + w.Write(newline) + w.Write(<-c.send) + } + + if err := w.Close(); err != nil { + return + } + case <-ticker.C: + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) + if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { + return + } + } + } +} + +// serveWs handles websocket requests from the peer. +func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return + } + client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)} + client.hub.register <- client + + // Allow collection of memory referenced by the caller by doing all work in + // new goroutines. + go client.writePump() + go client.readPump() +} diff --git a/_examples/14_websocket/go.mod b/_examples/14_websocket/go.mod new file mode 100644 index 0000000..82841f2 --- /dev/null +++ b/_examples/14_websocket/go.mod @@ -0,0 +1,8 @@ +module github.com/kataras/muxie/_examples/14_websocket + +go 1.17 + +require ( + github.com/gorilla/websocket v1.4.2 // indirect + github.com/kataras/muxie v1.1.2 // indirect +) diff --git a/_examples/14_websocket/go.sum b/_examples/14_websocket/go.sum new file mode 100644 index 0000000..06824a6 --- /dev/null +++ b/_examples/14_websocket/go.sum @@ -0,0 +1,4 @@ +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/kataras/muxie v1.1.2 h1:adKtuNVFwT7TlGG2eIfhNYyRMK5CyjXw0F31HAv6POE= +github.com/kataras/muxie v1.1.2/go.mod h1:xvAGGV93oksm/i9OBHyHqbiwUk1OenPd5CllnuO5lNU= diff --git a/_examples/14_websocket/home.html b/_examples/14_websocket/home.html new file mode 100644 index 0000000..67199f9 --- /dev/null +++ b/_examples/14_websocket/home.html @@ -0,0 +1,98 @@ + + + +Chat Example + + + + +
+
+ + +
+ + \ No newline at end of file diff --git a/_examples/14_websocket/hub.go b/_examples/14_websocket/hub.go new file mode 100644 index 0000000..3efd65a --- /dev/null +++ b/_examples/14_websocket/hub.go @@ -0,0 +1,49 @@ +package main + +// Hub maintains the set of active clients and broadcasts messages to the +// clients. +type Hub struct { + // Registered clients. + clients map[*Client]bool + + // Inbound messages from the clients. + broadcast chan []byte + + // Register requests from the clients. + register chan *Client + + // Unregister requests from clients. + unregister chan *Client +} + +func newHub() *Hub { + return &Hub{ + broadcast: make(chan []byte), + register: make(chan *Client), + unregister: make(chan *Client), + clients: make(map[*Client]bool), + } +} + +func (h *Hub) run() { + for { + select { + case client := <-h.register: + h.clients[client] = true + case client := <-h.unregister: + if _, ok := h.clients[client]; ok { + delete(h.clients, client) + close(client.send) + } + case message := <-h.broadcast: + for client := range h.clients { + select { + case client.send <- message: + default: + close(client.send) + delete(h.clients, client) + } + } + } + } +} diff --git a/_examples/14_websocket/main.go b/_examples/14_websocket/main.go new file mode 100644 index 0000000..7348ed4 --- /dev/null +++ b/_examples/14_websocket/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "flag" + "log" + "net/http" + + "github.com/kataras/muxie" +) + +var addr = flag.String("addr", ":8080", "http service address") + +func serveHome(w http.ResponseWriter, r *http.Request) { + log.Println(r.URL) + if r.URL.Path != "/" { + http.Error(w, "Not found", http.StatusNotFound) + return + } + if r.Method != "GET" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + http.ServeFile(w, r, "home.html") +} + +func main() { + flag.Parse() + hub := newHub() + go hub.run() + + mux := muxie.NewMux() // <- + mux.HandleFunc("/", serveHome) + mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { + serveWs(hub /* - > */, w.(*muxie.Writer).ResponseWriter /* <- */, r) + }) + + log.Printf("Open http://localhost%s/ in your browser.\n", *addr) + err := http.ListenAndServe(*addr, mux) + if err != nil { + log.Fatal("ListenAndServe: ", err) + } +}