Building a simple Chat application with WebSockets in Go and Vue.js (Part 1)

In this tutorial series, we will build a fully working chat application, the server part will be built with WebSockets in Go, on the front-end we will leverage Vue.js to create a simple interface.

We start out simple and have some fun building a fully working chat application. After there will be other posts helping you to build more advanced features.

Posts overview

Preconditions

Make sure your Go environment is set-up. If not follow the official documentation here. Some basic knowledge of the Go Syntax and Javascript/Vue.js is assumed.

Step 1: Setting up the WebSocket server

In the first step, we will step up the WebSocket server. For the WebSocket implementation, we will be using the gorilla/WebSocket package. To install this package paste the following in your console while in you are in your project folder:

go get github.com/gorilla/websocket

Client

Alright, now let’s add client.go, this file will represent the WebSocket client on the server-side.

We start with the bare minimum, define a Client type to hold the connection. Then expose a ServeWs() function to allow the creation of Websocket connections and use newClient() to create Client structs.

//client.go
package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  4096,
	WriteBufferSize: 4096,
}

// Client represents the websocket client at the server
type Client struct {
	// The actual websocket connection.
	conn *websocket.Conn
}

func newClient(conn *websocket.Conn) *Client {
	return &Client{
		conn: conn,
	}
}

// ServeWs handles websocket requests from clients requests.
func ServeWs(w http.ResponseWriter, r *http.Request) {

	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println(err)
		return
	}

	client := newClient(conn)

	fmt.Println("New Client joined the hub!")
	fmt.Println(client)
}

The upgrader is used to upgrade the HTTP server connection to the WebSocket protocol. the return value of this function is a WebSocket connection.

Main

Alright, so now we need a simple HTTP server to handle requests from clients and pass them to the ServeWs function.

At first create an HTTP server that listens on the port specified as a flag or on :8080 as default. Requests to the endpoint “/ws” will be handled by our ServeWs function.

//main.go
package main

import (
	"flag"
	"log"
	"net/http"
)

var addr = flag.String("addr", ":8080", "http server address")

func main() {
	flag.Parse()

	http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
		ServeWs(w, r)
	})

	log.Fatal(http.ListenAndServe(*addr, nil))
}

That’s it for the server part, for now at least… next, we will try to connect to the WebSocket server with a client.

Step 2: Creating the front-end

The front-end will be kept simple, first, we will add an index.html with some external dependencies.

<!-- public/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Chat</title>
    <!-- Load required Bootstrap and BootstrapVue CSS -->
    <link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
    <link type="text/css" rel="stylesheet" href="//unpkg.com/[email protected]/dist/bootstrap-vue.min.css" />

    <!-- Load polyfills to support older browsers -->
    <script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>

    <!-- Load Vue followed by BootstrapVue -->
    <script src="https://unpkg.com/vue"></script>
    <script src="//unpkg.com/[email protected]/dist/bootstrap-vue.min.js"></script>

    <!-- Load the following for BootstrapVueIcons support -->
    <script src="//unpkg.com/[email protected]/dist/bootstrap-vue-icons.min.js"></script> 
  </head>

  <body>
    <div id="app">
    </div>
  </body>

  <script src="assets/app.js"></script>
</html>

Then create an app.js in the /public/assets folder. In this file we will do three things for now:

  1. Create a Vue.js app with new Vue()
  2. Create a WebSocket connection to our server
  3. Listen to the WebSocket open event which indicates that we have a connection.
// public/assets/app.js
var app = new Vue({
    el: '#app',
    data: {
      ws: null,
      serverUrl: "ws://localhost:8080/ws"
    },
    mounted: function() {
      this.connectToWebsocket()
    },
    methods: {
      connectToWebsocket() {
        this.ws = new WebSocket( this.serverUrl );
        this.ws.addEventListener('open', (event) => { this.onWebsocketOpen(event) });
      },
      onWebsocketOpen() {
        console.log("connected to WS!");
      }
    }
  })

Alright now let’s make sure Go serves our static files, open the main.go file and add the following lines before the listenAndServe call:

...

fs := http.FileServer(http.Dir("./public"))
http.Handle("/", fs)

log.Fatal(http.ListenAndServe(*addr, nil))

Testing the connection

Now you should be able to establish a WebSocket connection from your browser with your Go server. Startup your server from your terminal with:

go run ./	

Open up your browser and go to http://localhost:8080. If everything’s working you should see a console.log message that tells you you are connected to the WebSocket! You can also check the network tab of your console and see the pending WebSocket connection:

Webscoket connection info chrome for go websockets

Meanwhile in the terminal where you started the Go program, you should see a log with the message “New Client joined the hub!”.

Step 3: Sending and receiving messages

OK, connection established… let’s make sure we can send and receive messages with our connected client. In order to keep track of the connected clients at the server, we add a new file called chatServer.go.

This file contains a WsServer type that has one map for the Clients registered in the server. It also has two channels, one for register requests and one for unregister requests.

package main

type WsServer struct {
	clients    map[*Client]bool
	register   chan *Client
	unregister chan *Client
}

// NewWebsocketServer creates a new WsServer type
func NewWebsocketServer() *WsServer {
	return &WsServer{
		clients:    make(map[*Client]bool),
		register:   make(chan *Client),
		unregister: make(chan *Client),
	}
}

// Run our websocket server, accepting various requests
func (server *WsServer) Run() {
	for {
		select {

		case client := <-server.register:
			server.registerClient(client)

		case client := <-server.unregister:
			server.unregisterClient(client)
		}
	}
}

func (server *WsServer) registerClient(client *Client) {
	server.clients[client] = true
}

func (server *WsServer) unregisterClient(client *Client) {
	if _, ok := server.clients[client]; ok {
		delete(server.clients, client)
	}
}

The Run() function will run infinitely and listens to the channels. One’s new requests present themself, they will be handled through dedicated functions. For now, this is simply adding clients to the map or removing them.

Main

After this we have to update the main.go file and:

  1. Create a new WsServer
  2. Run in in a Go routine
  3. Pass the server to the ServeWs function
wsServer := NewWebsocketServer()
go wsServer.Run()

http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
  ServeWs(wsServer, w, r)
})

Client.go

The next step involves the client file. First, we modify the type struct, we want to keep a reference to the WsServer for each Client. We may as well register the client within the server by pushing the newly created client in the register channel. Just before registering in the server we are starting two goroutines that we will define below.

// Client represents the websocket client at the server
type Client struct {
	// The actual websocket connection.
	conn     *websocket.Conn
	wsServer *WsServer
}

func newClient(conn *websocket.Conn, wsServer *WsServer) *Client {
	return &Client{
		conn:     conn,
		wsServer: wsServer,
	}
}

// ServeWs handles websocket requests from clients requests.
func ServeWs(wsServer *WsServer, w http.ResponseWriter, r *http.Request) {

	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println(err)
		return
	}

	client := newClient(conn, wsServer)
	
    go client.writePump()
	go client.readPump()
  
	wsServer.register <- client
}

revised type declaration, newClient() and ServeWs() functions.

readPump

In the readPump Goroutine, the client will read new messages send over the WebSocket connection. It will do so in an endless loop until the client is disconnected. When the connection is closed, the client will call its own disconnect method to clean up.

//import statements

const (
	// Max wait time when writing message to peer
	writeWait = 10 * time.Second

	// Max time till next pong from peer
	pongWait = 60 * time.Second

	// Send ping interval, must be less then pong wait time
	pingPeriod = (pongWait * 9) / 10

	// Maximum message size allowed from peer.
	maxMessageSize = 10000
)

....

func (client *Client) readPump() {
	defer func() {
		client.disconnect()
	}()

	client.conn.SetReadLimit(maxMessageSize)
	client.conn.SetReadDeadline(time.Now().Add(pongWait))
	client.conn.SetPongHandler(func(string) error { client.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })

	// Start endless read loop, waiting for messages from client
	for {
		_, jsonMessage, err := client.conn.ReadMessage()
		if err != nil {
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
				log.Printf("unexpected close error: %v", err)
			}
			break
		}

		client.wsServer.broadcast <- jsonMessage
	}
}

Upon receiving new messages the client will push them in the WsServer broadcast channel. We will create this channel below, first, we finish our client by adding the WritePump method

WritePump

The writePump goroutine handles sending the messages to the connected client. It runs in an endless loop waiting for new messages in the client.send channel. When receiving new messages it writes them to the client, if there are multiple messages available they will be combined in one write.

...
var (
	newline = []byte{'\n'}
	space   = []byte{' '}
)

...
func (client *Client) writePump() {
	ticker := time.NewTicker(pingPeriod)
	defer func() {
		ticker.Stop()
		client.conn.Close()
	}()
	for {
		select {
		case message, ok := <-client.send:
			client.conn.SetWriteDeadline(time.Now().Add(writeWait))
			if !ok {
				// The WsServer closed the channel.
				client.conn.WriteMessage(websocket.CloseMessage, []byte{})
				return
			}

			w, err := client.conn.NextWriter(websocket.TextMessage)
			if err != nil {
				return
			}
			w.Write(message)

			// Attach queued chat messages to the current websocket message.
			n := len(client.send)
			for i := 0; i < n; i++ {
				w.Write(newline)
				w.Write(<-client.send)
			}

			if err := w.Close(); err != nil {
				return
			}
		case <-ticker.C:
			client.conn.SetWriteDeadline(time.Now().Add(writeWait))
			if err := client.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
				return
			}
		}
	}
}

The writePump is also responsible for keeping the connection alive by sending ping messages to the client with the interval given in pingPeriod. If the client does not respond with a pong, the connection is closed.

Wiring up the WsServer

The last thing we need to do in our Go application is the creation of the broadcast channel in the WsServer.

type WsServer struct {
    ...
	broadcast  chan []byte
}

func NewWebsocketServer() *WsServer {
	return &WsServer{
		...
		broadcast:  make(chan []byte),
	}
}

func (server *WsServer) Run() {
	for {
		select {		
        ...
		case message := <-server.broadcast:
			server.broadcastToClients(message)
		}

	}
}

func (server *WsServer) broadcastToClients(message []byte) {
	for client := range server.clients {
		client.send <- message
	}
}

The broadcast channel listens for messages, sent by the client readPump. It in turn pushes this messages in the send channel of all the clients registered.

Step 4: Creating the chat window

In this last step we will create the chat window to send & display the messages!

First update your index.html, add the following between <div id=”app”></div>. This displays each message and provides a textarea to submit new messages.

 <div class="container-fluid h-100">
   <div class="row justify-content-center h-100">
     <div class="col-md-8 col-xl-6 chat">
       <div class="card">
         <div class="card-header msg_head">
           <div class="d-flex bd-highlight justify-content-center">
             Chat
           </div>
         </div>
         <div class="card-body msg_card_body">
           <div
                v-for="(message, key) in messages"
                :key="key"
                class="d-flex justify-content-start mb-4"
                >
             <div class="msg_cotainer">
               {{message.message}}
               <span class="msg_time"></span>
             </div>
           </div>
         </div>
         <div class="card-footer">
           <div class="input-group">
             <textarea
                       v-model="newMessage"
                       name=""
                       class="form-control type_msg"
                       placeholder="Type your message..."
                       @keyup.enter.exact="sendMessage"
                       ></textarea>
             <div class="input-group-append">
               <span class="input-group-text send_btn" @click="sendMessage"
                     >></span
                 >
             </div>
           </div>
         </div>
       </div>
     </div>
   </div>
</div>

To get some basic styling you can add a style.css in public/assets/. For this example the following stylesheet is used: https://github.com/jeroendk/go-vuejs-chat/blob/master/public/assets/style.css.

App.js

Now we make sure the chat window works by finishing the VueJs component. First, add two new data properties (messages & newMessage).

Then add an event listener to the message event on the WebSocket connection. At last add two functions, one for handling the new messages received through the connect (remember there can be multiple messages at once) and the other for sending a message from the textarea.

data: {
      ...
      messages: [],
      newMessage: ""
},
  
connectToWebsocket() {
  ...
  this.ws.addEventListener('message', (event) => { this.handleNewMessage(event) });
},  

handleNewMessage(event) {
  let data = event.data;
  data = data.split(/\r?\n/);
  for (let i = 0; i < data.length; i++) {
    let msg = JSON.parse(data[i]);
    this.messages.push(msg);
  }   
}

sendMessage() {
  if(this.newMessage !== "") {
    this.ws.send(JSON.stringify({message: this.newMessage}));
    this.newMessage = "";
  }
}

That’s it! You should now be able to send and receive chat messages when you visit your browser a http://localhost:8080.

Go chat window
Working chatbox!

Whats next?

You could ask the user his name before posting then you can display his or her name in the message. You can add any info you like to the message object, passed to the WebSocket.

Also stay tuned for the next parts in this series:

The final source code for this part can be found here:
https://github.com/jeroendk/go-vuejs-chat/tree/v1.0

Leave a Reply

Your email address will not be published. Required fields are marked *