Here we used the gorilla module to build a websocket application, and this tutorial will focus on building up a websoccket server.

About client part will implement with the chrome extension: Websocket King.

Get gorilla module

go get github.com/gorilla/websocket

Build a websocket server

Following are examples of simple websocket servers built with gin and gorilla,

Gin creates a get request and upgrades this HTTP connection to websocket connection by upgrader.Upgrade.

websocket.Upgrader with the following parameters can be used:

  • HandshakeTimeout: specifies the duration for the handshake to complete.
  • ReadBufferSize and WriteBufferSize: specify I/O buffer sizes in bytes.
  • WriteBufferPool: a pool of buffers for write operations.
  • Subprotocols specifies the server’s supported protocols in order of preference.
  • Error: specifies the function for generating HTTP error responses.
  • CheckOrigin: should carefully validate the request origin to prevent cross-site request forgery.
  • EnableCompression specifies if the server should attempt to negotiate per-message compression (RFC 7692).

Here, we set the Upgrade parameters CheckOrigin allowed the Websocket King Client origins. ((note: do not return true for all origins))

Next, using ws.ReadMessage() to read message from client, and ws.WriteMessage(mt, message) to response message to client.

Here when we get the “ping” message from the client, we will send a pong message to the client, and the message is a byte that should be transferred to string for checking and sent back as byte format.

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	"net/http"
)

var upgrader = websocket.Upgrader{
    //check origin will check the cross region source (note : please not using in production)
	CheckOrigin: func(r *http.Request) bool {
        //Here we just allow the chrome extension client accessable (you should check this verify accourding your client source)
		return origin == "chrome-extension://cbcbkhdmedgianpaifchdaddpnmgnknn"
	},
}

func main() {
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		//upgrade get request to websocket protocol
		ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
		if err != nil {
			fmt.Println(err)
			return
		}
		defer ws.Close()
		for {
			//Read Message from client
			mt, message, err := ws.ReadMessage()
			if err != nil {
				fmt.Println(err)
				break
			}
			//If client message is ping will return pong
			if string(message) == "ping" {
				message = []byte("pong")
			}
			//Response message to client
			err = ws.WriteMessage(mt, message)
			if err != nil {
				fmt.Println(err)
				break
			}
		}
	})
	r.Run() // listen and serve on 0.0.0.0:8080
}

About the basic websocket can declare into three parts: 1. upgrade to websocket protocol, 2. read client message, 3. write a message to the client.

Now, we can run the server and start listening the HTTP

Because we don’t declare environment port variable, here will use the :8080 by default

go run server.go

Run the client websocket

Here we using the chrome extension to run the client websocket and connect to serve side

You can see we send a ping message to the server, and will get the response pong message:

Host: ws://localhost:8080

Golang: Gin + Gorilla to build a websocket application

Refactor to allow Broadcast function

In the previous example, each client cannot communicate with each other, this will not work properly in the actuall case.

Here we will refactor to a broadcastable process.

At first, reference the gorilla Chat example

More info can reference the documentation, and here we modify the broadcast process will not send the message by self.

server.go

package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	hub := newHub()
	go hub.run()
	r.GET("/", func(c *gin.Context) {
		serveWs(hub, c.Writer, c.Request)
	})
	r.Run() // listen and serve on 0.0.0.0:8080
}

client.go

package main

import (
	"bytes"
	"encoding/json"
	"github.com/google/uuid"
	"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,
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}

type User struct {
	ID      string
	Addr    string
	EnterAt time.Time
}

// 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

	User
}

// 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))
		data := map[string][]byte{
			"message": message,
			"id":      []byte(c.ID),
		}
		userMessage, _ := json.Marshal(data)
		c.hub.broadcast <- userMessage
	}
}

// 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
	client.ID = GenUserId()
	client.Addr = conn.RemoteAddr().String()
	client.EnterAt = time.Now()

	// Allow collection of memory referenced by the caller by doing all work in
	// new goroutines.
	go client.writePump()
	go client.readPump()

	client.send <- []byte("Welcome")
}

func GenUserId() string {
	uid := uuid.NewString()
	return uid
}

hub.go

package main

import (
	"encoding/json"
)

// 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:
			clientId := client.ID
			for client := range h.clients {
				msg := []byte("some one join room (ID: " + clientId + ")")
				client.send <- msg
			}

			h.clients[client] = true

		case client := <-h.unregister:
			clientId := client.ID
			if _, ok := h.clients[client]; ok {
				delete(h.clients, client)
				close(client.send)
			}
			for client := range h.clients {
				msg := []byte("some one leave room (ID:" + clientId + ")")
				client.send <- msg
			}
		case userMessage := <-h.broadcast:
			var data map[string][]byte
			json.Unmarshal(userMessage, &data)

			for client := range h.clients {
				//prevent self receive the message
				if client.ID == string(data["id"]) {
					continue
				}
				select {
				case client.send <- data["message"]:
				default:
					close(client.send)
					delete(h.clients, client)
				}
			}
		}
	}
}

And finally, run the following commands

go run *.go