How to Build a Concurrent Chat App With Go and WebSockets
Just as Go makes programming such an application simple, Heroku makes it easy to supplement it with additional infrastructure.
Join the DZone community and get the full member experience.Join For Free
Go emerged from Google out of a need to build highly performant applications using an easy-to-understand syntax. It's a statically typed, compiled language developed by some of C's innovators, without the programming burden of manual memory management. Primarily, it was designed to take advantage of modern multicore CPUs and networked machines.
In this post, I'll demonstrate the capabilities of Go. We'll take advantage of Go's ability to create concurrent apps to build a chat app easily. On the backend, we'll use Redis as the intermediary to accept messages from the browser and send them to the subscribed clients. On the frontend, we'll use WebSockets via socket.io to facilitate client-side communication. We'll deploy it all on Heroku, a PaaS provider that makes it easy to deploy and host your apps. Just as Go makes programming such an application simple, Heroku makes it easy to supplement it with additional infrastructure.
Channels in Go
What developers find appealing about Go is its ability to communicate concurrently, which it does through a system called channels. It's important to draw upon an oft-cited distinction between concurrency and parallelism. Parallelism is the process by which a CPU executes multiple tasks simultaneously. Simultaneously, concurrency is the CPU's ability to switch between multiple tasks, which start, run, and complete while overlapping one another. In other words, parallel programs handle many operations at once, while concurrent programs can switch between many operations over the same period of time.
A channel in Go is the conduit through which concurrency flows. Channels can be unidirectional—with data either sent to or received by them — or bidirectional, which can do both. Here's an example that demonstrates the basic principles of concurrency and channels:
You can run this example online at the Go Playground to see the results. Channels are created by first specifying the data type they will communicate with — in this case, string. Two goroutines, one and two, accept each of these channels as an argument. Both then loop five times, passing a message to the channel, indicated by the <- glyph. Meanwhile, in the main function, an infinite loop waits for messages to come in from the channels. The select statement picks the channel which has a pending message, prints it, and moves on. If the channel was closed (which is important not just for memory management but also indicates that no more data will be sent), the channel is set to nil; when both channels are nil, the loop breaks.
In essence, a receiver is waiting endlessly to receive packets of data. When it receives the data, it acts upon it, then continues to wait for more messages. These receivers operate concurrently, without interrupting the rest of the program's flow. For this chat application, we will wait for a user to send a message to a receiver over a channel. When the message is received, the app will broadcast it to the front end. Everyone sitting in chat can read the text.
You should have a relatively recent version of Golang installed; anything past 1.12 will do. Create a directory in your GOPATH called heroku_chat_sample. If you'd like to run the code locally, you can also install and run a Redis server—but this is definitely not required, as a Heroku add-on will provide this for us in production.
Building a Simple Server
Let's start with a quick and easy "Hello World" server to verify that we can run Go programs. We'll start by fetching Gorilla, a web toolkit that simplifies the process of writing HTTP servers:
go get -u github.com/gorilla/mux
Next, create a file called main. Go, and paste these lines into it:
Finally, enter run main. Go to your terminal. You should be able to visit localhost:4444 in the browser and see the greeting. With just these few lines, we can better understand how to create routes using Gorilla.
But the static text is boring, right? Let's have this server show an HTML file. Create a directory called public, and within that, create a file called index.html that looks like this:
Then, let's change our server code to look like this:
If you restart the server and head back to localhost:4444, you should see a page inviting you to chat. It won't do much yet, but it's a start!
Let's make one more minor change to see this app on the way to becoming a twelve-factor app: store our port number in an environment variable. This won't be hugely important right now in development, but it will make a difference when deploying the app to production.
Create a file called .env and paste this line into it:
Then, fetch the godotenv modules:
And lastly, let's change the server code one more time to accept this environment variable:
In short, so long as GO_ENV is empty, we will load our environment variables from whatever is defined locally in .env. Otherwise, the app expects the system's environment variables, which we will do when the time comes.
Establishing Communication Using WebSockets and Redis
Websockets are a useful technique to pass messages from the client/browser to the server. It will be the fundamental technology used to send and receive chat messages from all the users in our chat room. On the backend, we will use Redis to store the chat history so that any new user can instantly get all of the room's previous messages. Redis is an in-memory database, which is often used for caching. We don't need the heft of a relational database for this project, but we do want some kind of storage system to keep track of users and their messages.
Setting Up Redis
To start with, let's prepare to introduce Redis as a dependency. If you have Redis running locally, you'll need to add a new line to specify the host and port of your Redis instance in your .env file:
Grab the Redis module as a dependency from GitHub:
go get -u github.com/gomodule/redigo/redis
We'll set up our Redis client as a global variable to make life easier:
Then, in our main() function, we will create an instance of this client via the environment variable:
We're using environment variables here because the server address is likely to be different than the one we use in development, but we don't want to hardcode those values. If you don't have a Redis server running locally, don't worry — you can still follow along in the tutorial and see the result in your browser live after we publish the app to Heroku.
When the server starts up, it'll connect to Redis first before listening for any incoming connections.
Setting Up WebSockets
Since we're going to be communicating with the frontend, it's useful to prepare to think about this structure in terms of how it will be represented in JSON.
Next, let's add two more lines of functionality to our web server in main(). The first line will indicate which function we want to run whenever a new WebSocket connection is opened—in other words, whenever a new user joins. The second line will set up a long-running goroutine which decides what to do whenever a user sends a message:
Last, let's jump back to the top of the file and add some global variables. We'll explain what they're for after the code:
In these lines:
clientsis a list of all the currently active clients (or open WebSockets)
broadcasteris a single channel which is responsible for sending and receiving our ChatMessage data structure
upgraderis a bit of a clunker; it's necessary to "upgrade" Gorilla's incoming requests into a WebSocket connection
Sending a Message
Let's start building out handleConnections first. When a new user joins the chat, three things should happen:
- They should be set up to receive messages from other clients.
- They should be able to send their own messages.
- They should receive a full history of the previous chat (backed by Redis).
Addressing number one is simple with Gorilla. We'll create a new client and append it to our global client's list in just a few lines:
Let's look at sending messages next instead:
After a client WebSocket is opened and added to the client's pool, an infinite loop will run endlessly.
Unlike other languages, infinite loops are practically encouraged in Go. The trick is to remember to break out of them and to clean up after yourself when you do. Here, the WebSocket is just endlessly looking for messages that the client has sent: ws.ReadJSON(&msg) is checking to see if msg is populated. If msg is ever not nil, it'll send the message over to the broadcaster channel. That's pretty much it as far as sending messages goes. If this WebSocket has an issue afterward, it'll remove itself from the client pool--delete(clients, ws), and then break out of this loop, severing its connection.
What happens when a msg is sent to the broadcaster channel? That's where handleMessages comes in.
It's the responsibility of handleMessages to send any new messages to every connected client. Just like the sending of messages, it all starts with an infinite for loop:
This line does nothing until something is sent to the channel. This is the core of goroutines, concurrency, and channels. Concurrency depends on channels to communicate with one another. If there's no data being sent, there's nothing to reason about or workaround. When a msg is received, we can send it to all the open clients:
Saving and Restoring History
But what about our final feature, which requires that every new client has access to the full chat history? We'll need to use Redis for that, and in particular, two operations:
- Any new message should be added to a list of running messages.
- Any new user should receive that full list.
When sending new messages, we can store them as a list in Redis using RPUSH:
When a new user joins, we can send the entire list at once using LRANGE:
This application is a bit tricky because we need to send all the messages to a single client. However, we can assume that only new connections call handleConnections, and at any point before the infinite for loop, we can communicate to this client and send them our messages. Our code would look something like this:
Full Backend Code
He's what our complete Go code would look like:
Our previous index.html can stay the same. Let's replace the contents of app.js with the following:
Let's break this down into chunks. The first lines (let websocket and let room) set up some global variables we can use later on.
Deploying to Heroku
You're now ready to deploy this app to Heroku! If you don't have one already, be sure to create a free account on Heroku, then install the Heroku CLI, which makes creating apps and attaching add-ons much easier.
First, log into your account:
Next, let's create a new app using create:
You'll be assigned a random name; I've got evening-wave-98825, so I'll be referring to that here.
Next, create a Procfile. A Procfile specifies which commands to run when your app boots up and set up any workers.
Ours will be a single line:
Since we need Redis, we can attach the free instance for our demo app:
heroku addons:create heroku-redis:hobby-dev -a evening-wave-98825
Let's build the app, and commit everything we have:
And let's send it all to Heroku:
heroku git:remote -a evening-wave-98825
git push heroku main
This process is all you need to deploy everything into production. If you visit the URL Heroku generated for you; you should see your chat app. It may look basic, but there's a lot going on behind the scenes!
You can download all of the code used in this article here.
If you enjoyed how easy it was to deploy a Go app with Redis onto Heroku, this is just the beginning! Here's another tutorial on building something exciting with Go. If you'd like to know more about how Go works with Heroku, here's another article with all the details.
Opinions expressed by DZone contributors are their own.