├── .gitignore ├── README.md ├── cmd ├── client │ └── main.go └── server │ └── main.go ├── go.mod ├── go.sum └── pkg ├── client └── client.go └── server ├── client.go └── hub.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | .DS_Store 3 | *.exe 4 | *.dll 5 | *.so 6 | *.dylib 7 | *-e 8 | *.swp 9 | *.swo 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 18 | .glide/ 19 | 20 | # builed file 21 | main 22 | linux-build 23 | darwin-build 24 | 25 | # Dev envirement 26 | .env.testing 27 | .settings 28 | .vscode 29 | temp 30 | 31 | logs/ 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Websocket client 2 | 3 | Its step by step guide for creating websocket client in Golang using mutex and channel. 4 | Please open [blog post](https://webdevelop.pro/blog/websocket-client-in-golang) for more information. -------------------------------------------------------------------------------- /cmd/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/webdeveloppro/golang-websocket-client/pkg/client" 12 | ) 13 | 14 | var addr = flag.String("addr", ":8000", "http service address") 15 | 16 | func main() { 17 | flag.Parse() 18 | 19 | client, err := client.NewWebSocketClient(*addr, "frontend") 20 | if err != nil { 21 | panic(err) 22 | } 23 | fmt.Println("Connecting") 24 | 25 | go func() { 26 | // write down data every 100 ms 27 | ticker := time.NewTicker(time.Millisecond * 1500) 28 | i := 0 29 | for range ticker.C { 30 | err := client.Write(i) 31 | if err != nil { 32 | fmt.Printf("error: %v, writing error\n", err) 33 | } 34 | i++ 35 | } 36 | }() 37 | 38 | // Close connection correctly on exit 39 | sigs := make(chan os.Signal, 1) 40 | 41 | // `signal.Notify` registers the given channel to 42 | // receive notifications of the specified signals. 43 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 44 | 45 | // The program will wait here until it gets the 46 | <-sigs 47 | client.Stop() 48 | fmt.Println("Goodbye") 49 | } 50 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "net/http" 11 | 12 | "github.com/webdeveloppro/golang-websocket-client/pkg/server" 13 | ) 14 | 15 | var addr = flag.String("addr", ":8000", "http service address") 16 | 17 | func serveHome(w http.ResponseWriter, r *http.Request) { 18 | if r.URL.Path != "/" { 19 | http.Error(w, "Not found", http.StatusNotFound) 20 | return 21 | } 22 | if r.Method != "GET" { 23 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 24 | return 25 | } 26 | http.ServeFile(w, r, "home.html") 27 | } 28 | 29 | func main() { 30 | flag.Parse() 31 | 32 | hub := server.NewHub() 33 | go hub.Run() 34 | http.HandleFunc("/frontend", func(w http.ResponseWriter, r *http.Request) { 35 | fmt.Println("got new connection") 36 | server.ServeWs(hub, w, r) 37 | }) 38 | 39 | fmt.Println("server started ... ") 40 | err := http.ListenAndServe(*addr, nil) 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/webdeveloppro/golang-websocket-client 2 | 3 | go 1.12 4 | 5 | require github.com/gorilla/websocket v1.4.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 2 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 3 | -------------------------------------------------------------------------------- /pkg/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | "sync" 9 | "time" 10 | 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | // Send pings to peer with this period 15 | const pingPeriod = 30 * time.Second 16 | 17 | // WebSocketClient return websocket client connection 18 | type WebSocketClient struct { 19 | configStr string 20 | sendBuf chan []byte 21 | ctx context.Context 22 | ctxCancel context.CancelFunc 23 | 24 | mu sync.RWMutex 25 | wsconn *websocket.Conn 26 | } 27 | 28 | // NewWebSocketClient create new websocket connection 29 | func NewWebSocketClient(host, channel string) (*WebSocketClient, error) { 30 | conn := WebSocketClient{ 31 | sendBuf: make(chan []byte, 1), 32 | } 33 | conn.ctx, conn.ctxCancel = context.WithCancel(context.Background()) 34 | 35 | u := url.URL{Scheme: "ws", Host: host, Path: channel} 36 | conn.configStr = u.String() 37 | 38 | go conn.listen() 39 | go conn.listenWrite() 40 | go conn.ping() 41 | return &conn, nil 42 | } 43 | 44 | func (conn *WebSocketClient) Connect() *websocket.Conn { 45 | conn.mu.Lock() 46 | defer conn.mu.Unlock() 47 | if conn.wsconn != nil { 48 | return conn.wsconn 49 | } 50 | 51 | ticker := time.NewTicker(time.Second) 52 | defer ticker.Stop() 53 | for ; ; <-ticker.C { 54 | select { 55 | case <-conn.ctx.Done(): 56 | return nil 57 | default: 58 | ws, _, err := websocket.DefaultDialer.Dial(conn.configStr, nil) 59 | if err != nil { 60 | conn.log("connect", err, fmt.Sprintf("Cannot connect to websocket: %s", conn.configStr)) 61 | continue 62 | } 63 | conn.log("connect", nil, fmt.Sprintf("connected to websocket to %s", conn.configStr)) 64 | conn.wsconn = ws 65 | return conn.wsconn 66 | } 67 | } 68 | } 69 | 70 | func (conn *WebSocketClient) listen() { 71 | conn.log("listen", nil, fmt.Sprintf("listen for the messages: %s", conn.configStr)) 72 | ticker := time.NewTicker(time.Second) 73 | defer ticker.Stop() 74 | for { 75 | select { 76 | case <-conn.ctx.Done(): 77 | return 78 | case <-ticker.C: 79 | for { 80 | ws := conn.Connect() 81 | if ws == nil { 82 | return 83 | } 84 | _, bytMsg, err := ws.ReadMessage() 85 | if err != nil { 86 | conn.log("listen", err, "Cannot read websocket message") 87 | conn.closeWs() 88 | break 89 | } 90 | conn.log("listen", nil, fmt.Sprintf("websocket msg: %x\n", bytMsg)) 91 | } 92 | } 93 | } 94 | } 95 | 96 | // Write data to the websocket server 97 | func (conn *WebSocketClient) Write(payload interface{}) error { 98 | data, err := json.Marshal(payload) 99 | if err != nil { 100 | return err 101 | } 102 | ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50) 103 | defer cancel() 104 | 105 | for { 106 | select { 107 | case conn.sendBuf <- data: 108 | return nil 109 | case <-ctx.Done(): 110 | return fmt.Errorf("context canceled") 111 | } 112 | } 113 | } 114 | 115 | func (conn *WebSocketClient) listenWrite() { 116 | for data := range conn.sendBuf { 117 | ws := conn.Connect() 118 | if ws == nil { 119 | err := fmt.Errorf("conn.ws is nil") 120 | conn.log("listenWrite", err, "No websocket connection") 121 | continue 122 | } 123 | 124 | if err := ws.WriteMessage( 125 | websocket.TextMessage, 126 | data, 127 | ); err != nil { 128 | conn.log("listenWrite", nil, "WebSocket Write Error") 129 | } 130 | conn.log("listenWrite", nil, fmt.Sprintf("send: %s", data)) 131 | } 132 | } 133 | 134 | // Close will send close message and shutdown websocket connection 135 | func (conn *WebSocketClient) Stop() { 136 | conn.ctxCancel() 137 | conn.closeWs() 138 | } 139 | 140 | // Close will send close message and shutdown websocket connection 141 | func (conn *WebSocketClient) closeWs() { 142 | conn.mu.Lock() 143 | if conn.wsconn != nil { 144 | conn.wsconn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 145 | conn.wsconn.Close() 146 | conn.wsconn = nil 147 | } 148 | conn.mu.Unlock() 149 | } 150 | 151 | func (conn *WebSocketClient) ping() { 152 | conn.log("ping", nil, "ping pong started") 153 | ticker := time.NewTicker(pingPeriod) 154 | defer ticker.Stop() 155 | for { 156 | select { 157 | case <-ticker.C: 158 | ws := conn.Connect() 159 | if ws == nil { 160 | continue 161 | } 162 | if err := conn.wsconn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(pingPeriod/2)); err != nil { 163 | conn.closeWs() 164 | } 165 | case <-conn.ctx.Done(): 166 | return 167 | } 168 | } 169 | } 170 | 171 | // Log print log statement 172 | // In real word I would recommend to use zerolog or any other solution 173 | func (conn *WebSocketClient) log(f string, err error, msg string) { 174 | if err != nil { 175 | fmt.Printf("Error in func: %s, err: %v, msg: %s\n", f, err, msg) 176 | } else { 177 | fmt.Printf("Log in func: %s, %s\n", f, msg) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /pkg/server/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package server 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "net/http" 11 | "time" 12 | 13 | "github.com/gorilla/websocket" 14 | ) 15 | 16 | const ( 17 | // Time allowed to write a message to the peer. 18 | writeWait = 10 * time.Second 19 | 20 | // Time allowed to read the next pong message from the peer. 21 | pongWait = 60 * time.Second 22 | 23 | // Send pings to peer with this period. Must be less than pongWait. 24 | pingPeriod = (pongWait * 9) / 10 25 | 26 | // Maximum message size allowed from peer. 27 | maxMessageSize = 512 28 | ) 29 | 30 | var ( 31 | newline = []byte{'\n'} 32 | space = []byte{' '} 33 | ) 34 | 35 | var upgrader = websocket.Upgrader{ 36 | ReadBufferSize: 1024, 37 | WriteBufferSize: 1024, 38 | } 39 | 40 | // Client is a middleman between the websocket connection and the hub. 41 | type Client struct { 42 | hub *Hub 43 | 44 | // The websocket connection. 45 | conn *websocket.Conn 46 | 47 | // Buffered channel of outbound messages. 48 | send chan []byte 49 | } 50 | 51 | // readPump pumps messages from the websocket connection to the hub. 52 | // 53 | // The application runs readPump in a per-connection goroutine. The application 54 | // ensures that there is at most one reader on a connection by executing all 55 | // reads from this goroutine. 56 | func (c *Client) readPump() { 57 | defer func() { 58 | c.hub.unregister <- c 59 | c.conn.Close() 60 | }() 61 | c.conn.SetReadLimit(maxMessageSize) 62 | c.conn.SetReadDeadline(time.Now().Add(pongWait)) 63 | c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) 64 | for { 65 | _, message, err := c.conn.ReadMessage() 66 | if err != nil { 67 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 68 | c.hub.log("readPump", err, "reading error") 69 | } 70 | break 71 | } 72 | message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1)) 73 | c.hub.log("readPump", nil, fmt.Sprintf("got: %s", message)) 74 | c.hub.broadcast <- message 75 | } 76 | } 77 | 78 | // writePump pumps messages from the hub to the websocket connection. 79 | // 80 | // A goroutine running writePump is started for each connection. The 81 | // application ensures that there is at most one writer to a connection by 82 | // executing all writes from this goroutine. 83 | func (c *Client) writePump() { 84 | ticker := time.NewTicker(pingPeriod) 85 | defer func() { 86 | ticker.Stop() 87 | c.conn.Close() 88 | }() 89 | for { 90 | select { 91 | case message, ok := <-c.send: 92 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 93 | if !ok { 94 | // The hub closed the channel. 95 | c.conn.WriteMessage(websocket.CloseMessage, []byte{}) 96 | return 97 | } 98 | 99 | w, err := c.conn.NextWriter(websocket.TextMessage) 100 | if err != nil { 101 | return 102 | } 103 | w.Write(message) 104 | 105 | // Add queued chat messages to the current websocket message. 106 | n := len(c.send) 107 | for i := 0; i < n; i++ { 108 | w.Write(newline) 109 | w.Write(<-c.send) 110 | } 111 | 112 | if err := w.Close(); err != nil { 113 | return 114 | } 115 | case <-ticker.C: 116 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 117 | if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { 118 | return 119 | } 120 | } 121 | } 122 | } 123 | 124 | // serveWs handles websocket requests from the peer. 125 | func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) { 126 | conn, err := upgrader.Upgrade(w, r, nil) 127 | if err != nil { 128 | hub.log("ServeWs", err, "cannot handle websocket request") 129 | return 130 | } 131 | client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)} 132 | client.hub.register <- client 133 | 134 | // Allow collection of memory referenced by the caller by doing all work in 135 | // new goroutines. 136 | go client.writePump() 137 | go client.readPump() 138 | } 139 | -------------------------------------------------------------------------------- /pkg/server/hub.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Hub maintains the set of active clients and broadcasts messages to the 8 | // clients. 9 | type Hub struct { 10 | // Registered clients. 11 | clients map[*Client]bool 12 | 13 | // Inbound messages from the clients. 14 | broadcast chan []byte 15 | 16 | // Register requests from the clients. 17 | register chan *Client 18 | 19 | // Unregister requests from clients. 20 | unregister chan *Client 21 | } 22 | 23 | func NewHub() *Hub { 24 | return &Hub{ 25 | broadcast: make(chan []byte), 26 | register: make(chan *Client), 27 | unregister: make(chan *Client), 28 | clients: make(map[*Client]bool), 29 | } 30 | } 31 | 32 | func (h *Hub) Run() { 33 | for { 34 | select { 35 | case client := <-h.register: 36 | h.clients[client] = true 37 | case client := <-h.unregister: 38 | if _, ok := h.clients[client]; ok { 39 | delete(h.clients, client) 40 | close(client.send) 41 | h.log("Run", nil, "connection closed") 42 | } 43 | case <-h.broadcast: 44 | for client := range h.clients { 45 | select { 46 | case client.send <- []byte("acknowledge"): 47 | default: 48 | close(client.send) 49 | delete(h.clients, client) 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | // Log print log statement 57 | // In real word I would recommend to use zerolog or any other solution 58 | func (h *Hub) log(f string, err error, msg string) { 59 | if err != nil { 60 | fmt.Printf("Error in func: %s, err: %v, msg: %s\n", f, err, msg) 61 | } else { 62 | fmt.Printf("Log in func: %s, %s\n", f, msg) 63 | } 64 | } 65 | --------------------------------------------------------------------------------