Let's learn how to to build a chat application with Redis, WebSocket and Go [part 1]
Tags: redis, database, showdev, tutorial
Published: 2020-04-13 15:53:00

The [`WebSocket`](https://tools.ietf.org/html/rfc6455) protocol offers a bi-directional (both server and client can exchange messages) and full-duplex (server or client can send messages at the same time) communication channel that makes it suitable for real-time scenarios such as chat applications etc. The connected chat users (clients) can send messages to the application (`WebSocket` server) and can exchange messages with each other - similar to a peer-to-peer setting.

In this blog, we will explore how to build a simple chat application using `WebSocket` and [`Go`](https://golang.org/). The solution will make use of [`Redis`](http://redis.io/) as well (more on this soon). 

> A follow-up blog post (part 2) will demonstrate how to deploy this application to [Azure App Service](https://docs.microsoft.com/azure/app-service/?WT.mc_id=devto-blog-abhishgu) which will communicate with Azure Redis Cache using [Virtual Network](https://docs.microsoft.com/azure/virtual-network/virtual-networks-overview?WT.mc_id=devto-blog-abhishgu) integration

You will learn:

- Redis data structures - this app uses [`SET`](https://redis.io/commands#set) and [`PUBSUB`](https://redis.io/commands#pubsub)
- Interacting with Redis using the [`go-redis`](https://github.com/go-redis/redis) client
- The [`gorilla WebSocket`](https://github.com/gorilla/websocket) library which provides a complete and tested implementation of the WebSocket protocol
- [Azure Cache for Redis](https://docs.microsoft.com/azure/azure-cache-for-redis/cache-overview?WT.mc_id=devto-blog-abhishgu) which is a managed Redis offering in the cloud

## Why Redis?

![](https://dev-to-uploads.s3.amazonaws.com/i/x3368c7fvxm4xqqbuxsd.jpeg)

Let's consider a chat application. When a user first connects, a corresponding `WebSocket` connection is created within the application (`WebSocket` server) and it is associated with the specific application instance. This `WebSocket` connection is what enables us to broadcast chat messages between users. We can scale (out) our application (for e.g. to account for a large user base) by running multiple instances. Now, if a new user comes in, they may be connected to a new instance. So we have a scenario where different users (hence their respective `WebSocket` connections) are associated with different instances. As a result, they will not be able to exchange messages with each other - this is unacceptable, even for our toy chat application 😉

[Redis](https://redis.io/) is a versatile key value that supports a variety of [rich data structures](https://redis.io/commands/) such as `List`, `Set`, `Sorted Set`, `Hash` etc. One of the features also includes a `PubSub` capability using which publishers can send messages to Redis channel(s) and subscribers can listen for messages on these channel(s) - both are completely independent and decoupled from each other. This can be used to solve the problem we have. Now, instead of depending on `WebSocket` connections only, we can use a Redis `channel` which each chat application can subscribe to. Thus, the messages sent to the `WebSocket` connection can be now piped via the Redis channel to ensure that all the application instances (and associated chat users) receive them. 

More on this when we dive into the code in the next section. It is [available on Github](https://github.com/abhirockzz/chat-redis-websocket-go)

> Please note that instead of plain `WebSocket`, you can also use technologies such as [Azure SignalR](https://docs.microsoft.com/azure/azure-signalr/signalr-overview?WT.mc_id=devto-blog-abhishgu) that allows apps to push content updates to connected clients, such as a single page web or mobile application. As a result, clients are updated without the need to poll the server or submit new HTTP requests for updates

To follow along and deploy this solution to Azure, you are going to need a [Microsoft Azure](https://docs.microsoft.com/azure/cosmos-db/create-cosmosdb-resources-portal?WT.mc_id=devto-blog-abhishgu) account. [You can grab one for free](https://azure.microsoft.com/free/?WT.mc_id=devto-blog-abhishgu) if you don't have it already!

## Chat application overview

Time for a quick code walkthrough. Here is the application structure:

```
.
├── Dockerfile
├── chat
│   ├── chat-session.go
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
```

In `main.go`, we [register our `WebSocket` handler](https://golang.org/pkg/net/http/#Handle) and [start the web server](https://golang.org/pkg/net/http/#ListenAndServe) - all that's used is plain `net/http` package

```
	http.Handle("/chat/", http.HandlerFunc(websocketHandler))
	server := http.Server{Addr: ":" + port, Handler: nil}
	go func() {
		err := server.ListenAndServe()
		if err != nil && err != http.ErrServerClosed {
			log.Fatal("failed to start server", err)
		}
	}()
```

The `WebSocket` handler processes chat users (which are nothing but `WebSocket` clients) and starts a new chat session.

```
func websocketHandler(rw http.ResponseWriter, req *http.Request) {
	user := strings.TrimPrefix(req.URL.Path, "/chat/")

	peer, err := upgrader.Upgrade(rw, req, nil)
	if err != nil {
		log.Fatal("websocket conn failed", err)
	}
	chatSession := chat.NewChatSession(user, peer)
	chatSession.Start()
}
```

A `ChatSession` (part of `chat/chat-session.go`) represents a user and it's corresponding `WebSocket` connection (on the server side)

```
type ChatSession struct {
	user string
	peer *websocket.Conn
}
```

When a session is started, it starts a `goroutine` to accept messages from the user who just joined the chat. It does so by calling [`ReadMessage()` (from `websocket.Conn`)](https://godoc.org/github.com/gorilla/websocket#Conn.ReadMessage) in a `for` loop. This goroutine `exits` if the user disconnects (`WebSocket` connection gets closed) or the application is shut down (e.g. using `ctrl+c`). To summarize, there is a separate goroutine spawned for each user in order to handle its chat messages.

```
func (s *ChatSession) Start() {
...
	go func() {
		for {
			_, msg, err := s.peer.ReadMessage()
			if err != nil {
				_, ok := err.(*websocket.CloseError)
				if ok {
					s.disconnect()
				}
				return
			}
			SendToChannel(fmt.Sprintf(chat, s.user, string(msg)))
		}
	}()
```

As soon as a message is received from the user (over the `WebSocket` connection), its broadcasted to other users using the `SendToChannel` function which is a part of `chat/redis.go`. All it does it publish the message to a Redis `pubsub` channel

```
func SendToChannel(msg string) {
	err := client.Publish(channel, msg).Err()
	if err != nil {
		log.Println("could not publish to channel", err)
	}
}
```

The important part is the `sub` (subscriber) part of the equation. As opposed to the case where there was a dedicated goroutine for each connected chat user, we use a `single` goroutine (at an application scope) to subscribe to the Redis channel, receive messages and broadcast it to *all* the users using their respective server-side `WebSocket` connection. 

```
func startSubscriber() {
	go func() {
		sub = client.Subscribe(channel)
		messages := sub.Channel()
		for message := range messages {
			from := strings.Split(message.Payload, ":")[0]
			for user, peer := range Peers {
				if from != user {
					peer.WriteMessage(websocket.TextMessage, []byte(message.Payload))
				}
			}
		}
	}()
}
```

> The subscription is ended when the application instance is shut down - this is turn terminates the channel `for-range` loop and the goroutine exits 

The `startSubscriber` function is called from the `init()` function in `redis.go`. The `init() `function starts off by connecting to Redis and the application exits if connectivity fails.

Alright! It's time to set up a Redis instance to which we can hook up our chat backend to. Let's create a Redis server in the cloud!

## Azure Redis Cache setup

Azure Cache for Redis provides access to a secure, dedicated Redis cache which is hosted within Azure, and accessible to any application within or outside of Azure. 

For the purposes of this blog, we will setup an Azure Redis Cache with a `Basic` tier which is a single node cache ideal for development/test and non-critical workloads. Please note that you also choose from `Standard` and `Premium` tiers which [provide different features ranging from persistence, clustering, geo-replication, etc](https://docs.microsoft.com/azure/azure-cache-for-redis/cache-overview?WT.mc_id=devto-blog-abhishgu#feature-comparison).

I will be using [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest&WT.mc_id=devto-blog-abhishgu) for the installation. You can also use [Azure Cloud Shell](https://docs.microsoft.com/azure/cloud-shell/overview?WT.mc_id=devto-blog-abhishgu) if you are a browser person! 

To quickly setup an Azure Redis Cache instance, we can use the [`az redis create`](https://docs.microsoft.com/cli/azure/redis?view=azure-cli-latest&WT.mc_id=devto-blog-abhishgu#az-redis-create) command, e.g.

```
az redis create --location westus2 --name chat-redis --resource-group chat-app-group --sku Basic --vm-size c0
```

> Checkout ["Create an Azure Cache for Redis"](https://docs.microsoft.com/azure/azure-cache-for-redis/scripts/create-cache?WT.mc_id=devto-blog-abhishgu) for a step-by-step guide

Once that's done, you need the get the information required to connect to Azure Redis Cache instance i.e. host, port and access keys. This can be done using CLI as well, e.g.

```
//host and (SSL) port
az redis show --name chat-redis --resource-group chat-app-group --query [hostName,sslPort] --output tsv

//primary access key
az redis list-keys --name chat-redis --resource-group chat-app-group --query [primaryKey] --output tsv
```

> Checkout ["Get the hostname, ports, and keys for Azure Cache for Redis"](https://docs.microsoft.com/azure/azure-cache-for-redis/scripts/cache-keys-ports?WT.mc_id=devto-blog-abhishgu) for a step-by-step guide

That's it...

## .... lets chat!

To keep things simple, the application is available in the form of a [Docker image](https://hub.docker.com/r/abhirockzz/redis-chat-go)

First, set a few environment variables:

```
//use port 6380 for SSL
export REDIS_HOST=[redis cache hostname as obtained from CLI]:6380
export REDIS_PASSWORD=[redis cache primary access key as obtained from CLI]
export EXT_PORT=9090
export NAME=chat1
```

> The application uses a static port `8080` internally (for the web server). We use an external port specified by `EXT_PORT` and map it to the port `8080` inside our container (using `-p $EXT_PORT:8080`) 

Start the Docker container

```
docker run --name $NAME -e REDIS_HOST=$REDIS_HOST -e REDIS_PASSWORD=$REDIS_PASSWORD -p $EXT_PORT:8080 abhirockzz/redis-chat-go
```

Its time to join the chat! You can use any WebSocket client. I prefer using [`wscat`](https://www.npmjs.com/package/wscat) in the terminal and or the [Chrome WebSocket extension in the browser](https://chrome.google.com/webstore/detail/simple-websocket-client/pfdhoblngboilpfeibdedpjgfnlcodoo?hl=en)

I will demonstrate this using `wscat` from my terminal. Open two separate terminals to simulate different users:

```
//terminal 1 (user "foo")
wscat -c ws://localhost:9090/chat/foo

//terminal 2 (user "bar")
wscat -c ws://localhost:9090/chat/bar
```

Here is an example of a chat between `foo` and `bar`

![](chat.PNG)

`foo` joined first and got a `Welcome foo!` message and so did `bar` who joined after `foo`. Notice that `foo` was notified that `bar` had joined. `foo` and `bar` exchanged a few messages before `bar` left (`foo` was notified of that too).

As an exercise, you can start another instance of the chat application. Spin up another Docker container with a different value for `EXT_PORT` and NAME e.g.

```
//use port 6380 for SSL
export REDIS_HOST=[redis cache host name as obtained from CLI]:6380
export REDIS_PASSWORD=[redis cache primary access key as obtained from CLI]
export EXT_PORT=9091
export NAME=chat2

docker run --name $NAME -e REDIS_HOST=$REDIS_HOST -e REDIS_PASSWORD=$REDIS_PASSWORD -p $EXT_PORT:8080 abhirockzz/redis-chat-go
```

Now connect on port `9091` (or your chosen port) to simulate another user

```
//user "pi"
wscat -c ws://localhost:9091/chat/pi
```

Since `foo` is still active, it will get notified: `pi` and `foo` can exchange pleasantries

### Check Redis

Let's confirm by peeking into the Redis data structures. You can use the [`redis-cli`](https://redis.io/topics/rediscli) for this. If you're using the Azure Redis Cache, I would recommend using a really handy [web based Redis console](https://docs.microsoft.com/azure/azure-cache-for-redis/cache-configure?WT.mc_id=devto-blog-abhishgu#redis-console) for this. 

We have a `SET` (name `chat-users`) which stores the active users

```
SMEMBERS chat-users
```

You should see the result - this means that the users `foo` and `bar` are currently connected to the chat application and have an associated active `WebSocket` connection

```
1) "foo"
2) "bar"
```

What about the PubSub channel? 

```
PUBUSB CHANNELS
```

As a single channel is used for all the users, you should get this result from the Redis server:

```
1) "chat"
```

That's it for this blog post. Stay tuned for part 2!

If you found this useful, please don't forget to like and follow 🙌 I would love to get your feedback: just drop a comment here or reach out on [Twitter](https://twitter.com/abhi_tweeter) 🙏🏻