Build a Realtime Chat Server With Go and WebSockets

Build a Realtime Chat Server With Go and WebSockets

Realtime communication can be hard to implement in your applications, but it doesn't have to be. Websockets provides an easy and compact approach and can be used in almost any programming language.

In this article, you will build a realtime chat application in Golang using Websockets. You will also containerize the application using Docker.

Setting Up Our Go Project

Let's start by creating the files needed for the project. This can be done using the following commands:

mkdir public
touch main.go Dockerfile public/index.html public/styles.css public/main.js

Note: The mkdir command creates a new directory, and the touch command is used to create new files.

Basic HTTP server

Not that you have completed the setup of the folder structure, you will continue by implementing a basic HTTP server using the gin package.

For that, you first need to install the needed dependencies:

go get github.com/gin-gonic/gin
go get gopkg.in/olahol/melody.v1

The server will have a single GET Endpoint to handle all the Websocket requests and will also serve a static directory which will be used as a frontend.

package main

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

func main() {
	r := gin.Default()

	r.Use(static.Serve("/", static.LocalFile("./public", true)))

	r.GET("/ws", func(c *gin.Context) {
		m.HandleRequest(c.Writer, c.Request)
	})

	r.Run(":5000")
}

As you can see, the server is statically serving the public directory you created earlier and exposing a single GET Endpoint of /ws.

The server is then started using the run command, which takes the port as an argument.

Implementing the Websockets server

Now that you have a basic HTTP server to build on let's continue by implementing the Websockets functionality. For that, you will use the Melody library, a minimalistic Websockets framework for the Golang programming language.

Since you only have one Websocket event which is triggered whenever you send a chat message, you just need to broadcast the message to the other clients.

package main

import (
	static "github.com/gin-contrib/static"
	"github.com/gin-gonic/gin"
	"gopkg.in/olahol/melody.v1"
)

func main() {
	r := gin.Default()
	m := melody.New()

	r.Use(static.Serve("/", static.LocalFile("./public", true)))

	r.GET("/ws", func(c *gin.Context) {
		m.HandleRequest(c.Writer, c.Request)
	})

	m.HandleMessage(func(s *melody.Session, msg []byte) {
		m.Broadcast(msg)
	})

	r.Run(":5000")
}

The HandleMessage() function is used to receive all the messages sent to the Websockets GET endpoint. After that, you broadcast the messages to all connected clients.

Adding the frontend

The next step is to create a basic layout in your index.html file. The layout contains an input field for the username and a textarea for the messages.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
    <title>Websockets Chat</title>
    <link rel="stylesheet" href="styles.css">
    <script type="text/javascript" src="https://cdn.socket.io/socket.io-1.4.5.js"></script>
</head>
<body>
    <div class="container">
            <div class="row">
                <div class="col-md-6 offset-md-3 col-sm-12">
                    <h1 class="text-center">Socket IO Chat</h1>
                    <br>
                    <div id="status"></div>
                    <div id="chat">
                        <input type="text" name="username" id="username" class="form-control" placeholder="Enter name...">
                        <br>
                        <div class="card">
                            <div id="messages" class="card-block"></div>
                        </div>
                        <br>
                        <textarea id="textarea" name="inputMessage" class="form-control" placeholder="Enter message..."></textarea>
                        <br>
                        <button id="send" class="btn">Send</button>
                    </div>
                </div>
            </div>
    </div>
    <script type="text/javascript" src="main.js"></script>
</body>
</html>

You also include the main.js file at the bottom so you can implement the websockets client functionality for the frontend.

First, you need to get the elements of your frontend layout from the Javascript DOM using the document.querySelector() function.

If you don't know how the Javascript DOM works you can check out this article.

const input = document.querySelector('#textarea')
const messages = document.querySelector('#messages')
const username = document.querySelector('#username')
const send = document.querySelector('#send')

Next, you connect to the Websockets server and create a listener for the onmessage event. The event will trigger a function to insert the new message you received into the chat.

const url = "ws://" + window.location.host + "/ws";
const ws = new WebSocket(url);

ws.onmessage = function (msg) {
    insertMessage(JSON.parse(msg.data))
};

Now that you can receive the incomming messages it is time to send messages by placing an onclick event on the send button.

send.onclick = () => {
    const message = {
		username: username.value,
		content: input.value,
    }

    ws.send(JSON.stringify(message));
    input.value = "";
};

The next step is implementing the insertMessage() function that will insert the received message into the text area using the Javascript DOM.

/**
 * Insert a message into the UI
 * @param {Message that will be displayed in the UI} messageObj
 */
function insertMessage(messageObj) {
	// Create a div object which will hold the message
	const message = document.createElement('div')

	// Set the attribute of the message div
	message.setAttribute('class', 'chat-message')
	console.log("name: " +messageObj.username + " content: " + messageObj.content)
	message.textContent = `${messageObj.username}: ${messageObj.content}`

	// Append the message to our chat div
	messages.appendChild(message)

	// Insert the message as the first message of our chat
	messages.insertBefore(message, messages.firstChild)
}

All of this results in the following main.js file:

const input = document.querySelector('#textarea')
const messages = document.querySelector('#messages')
const username = document.querySelector('#username')
const send = document.querySelector('#send')

const url = "ws://" + window.location.host + "/ws";
const ws = new WebSocket(url);

ws.onmessage = function (msg) {
    insertMessage(JSON.parse(msg.data))
};

send.onclick = () => {
    const message = {
		username: username.value,
		content: input.value,
	}

    ws.send(JSON.stringify(message));
    input.value = "";
};

/**
 * Insert a message into the UI
 * @param {Message that will be displayed in the UI} messageObj
 */
function insertMessage(messageObj) {
	// Create a div object which will hold the message
	const message = document.createElement('div')

	// Set the attribute of the message div
	message.setAttribute('class', 'chat-message')
	console.log("name: " +messageObj.username + " content: " + messageObj.content)
	message.textContent = `${messageObj.username}: ${messageObj.content}`

	// Append the message to our chat div
	messages.appendChild(message)

	// Insert the message as the first message of our chat
	messages.insertBefore(message, messages.firstChild)
}

Lastly, you will add some css to the styles.css file.

#messages{
    height:300px;
    overflow-y: scroll;
}

Testing the application

Awesome, now that we have finished the application, you can run it using the following command.

go run main.go

You can now visit localhost:5000 in two different browser tabs and start chatting with each other.

Containerizing the application

The last step now is to containerize the application using Docker. For that, we can use a simple Dockerfile that builds and runs the application.

FROM golang:latest

# Set the Current Working Directory inside the container
WORKDIR /app

RUN GO111MODULE=on

# Copy go mod and sum files
COPY go.mod go.sum ./

# Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed
RUN go mod download

COPY . . 

# Build the application
RUN go build -o main .

# Expose port 5000 to the outside world
EXPOSE 5000

# Command to run the executable
CMD ["./main"]

Build the image can be done using the build command. The -t flag is used to give the image a custom tag.

docker build -t websocketschat .

You can now run the image using the run command. The -p flag is used to set the port that should be exposed to the host machine.

docker run -p 5000:5000 websocketschat

The whole project and many other Golang project can be found on my Github.

Conclusion

You made it all the way until the end! I hope that this article helped you understand Websockets and how you can use it in your Golang applications.

If you have found this useful, please consider recommending and sharing it with other fellow developers and subscribing to my newsletter. If you have any questions or feedback, let me know using my contact form or contact me on twitter.