Redis, Go, & How to Build a Chat Application

In this article we'll show you an example of a chat application we put together which uses just Redis and Go. Redis is full of neat things that make writing an application like this so much easier.

We'll look at how things like a publish and subscribe system makes sending and getting messages simple and using expiring keys allows you to ensure that a user is keeping their session live. In combination with Go's goroutines and channels, we'll show you how to use those features and help you start exploring the power of Redis.

Connecting

But we'll start our look at the code with the connection to Redis. We're going to use
Redigo, the popular Redis client library for Go. Now, Redigo is a fine library but it does make you work to log in if you are using a Redis URI. You have to parse the URL and present various parsed parts to the API to complete the connection – connecting to a local Redis is remarkably easy though. Anyway, someone else thought it was overly complex and did something about it. The Redis-url package means we can connect with just:

conn, err := redisurl.Connect()  

And the redisurl package will get the REDISURL environment variable and use that to connect. If you don't like using environment variables, there is an alternate version
ConnectToURL() which takes a string URL directly. Remember to set your
REDIS
URL environment variable, complete with password, by getting it from your Compose Redis dashboard.

Making a Name

Now, once we're connected, we want to ensure there's no user with our username already on line. What we want is to acquire a lock on the name in the server, so what we do is create a key/value pairing to represent that lock. We get the username as the first command line argument earlier on. Using that we'll make a key for our lock:

userkey := "online." + username  

Now, if that key already exists, we want the client to exit because the user is, apparently already logged in. In Redis additional options to setting a key to ensure that the pre-condition "the key for this user must not exist" with the
NX option.

Now we do want the key to go away when the client exits. With Go we could clean up of the key when the user exits with the defer command. When the client exits normally, that could delete the user key. The only problem with using defer is that it isn't called when the program exits abnormally, through a crash or control-C, unless we do something else. So, we turn to Redis's ability to set expiry times on keys.

We can set an expiry time, the TTL (time to live), on the key so that unless the client comes back within that time and resets the key, it'll disappear on its own. To do this, we'll use the EX option to set the TTL on the key. We'll set this to 120 seconds. If we put that all together we get:

val, err := conn.Do("SET", userkey, username, "NX", "EX", "120")

  if val == nil {
    fmt.Println("User already online")
    os.Exit(1)
  }

If the key already exists, the command returns a nil value rather than "ok" so we can use that to determine if we are allowed on.

Setting the user list

We're also going to add the user's name to a Redis set of users online like so...

val, err = conn.Do("SADD", "users", username)  

You may be wondering when there's a key in the Redis database for each user online why we aren't using the Redis SCAN command to just select which users have an online key. The SCAN command with a MATCH, which is what we'd need to match "online.username" in the keys isn't a cheap operation so to save on that, we use the set.

Ah, you say, why not then just use a set and expire the members from that set. The reason is simple, you can only TTL expire keys and their values in Redis. There is another solution to the expiry issue, creating a Redis sorted set with timestamps as the score for sorting. That makes it easy to work out which members should be expired because the oldest members will be first in the set. The drawback is that it does require a process to check and expire the set members.

So, for our example, we're keeping it simple - one key/value for the locking and expiry and one set member for an easy way of seeing who's online. Oh, and before we move on, let's just set up a Go ticker to remind us when to update that key/value:

tickerChan := time.NewTicker(time.Second * 60).C  

Subscribing for messages

We are going to use Redis's publish and subscribe system to move messages around. This involves there being a channel – we'll call it messages – where new messages are written and to which every client is listening. We'll get to the writing messages in a bit, but first the listening. It's only sensible here to use Go's channels and goroutines to have something always listening for new messages. We're going to make a channel, called subChan, and then start up a new connection to Redis in a goroutine:

subChan := make(chan string)  
  go func() {
    subconn, err := redisurl.Connect()
    if err != nil {
      fmt.Println(err)
      os.Exit(1)
    }
    defer subconn.Close()

Why a new connection? Well, Redis subscribe traffic is easier to handle if it's not mixed in with other traffic, so giving it its own connection makes things a lot simpler for everyone. But what makes that connection a publish and subscribe connection? That happens when we do this:

psc := redis.PubSubConn{Conn: subconn}  

That wraps the connection with the bits needed to make it easy to subscribe to a channel with:

psc.Subscribe("messages")  

And at this point we can drop into a forever loop, where we can recieve Redis pubsub messages and use switch to act on the different types of message. That's one with data and one with subscription information. We're only interested in the first one, the one with data as that'll be a string message sent by another client. When we get that, we send that to our subChan channel.

for {  
  switch v := psc.Receive().(type) {
  case redis.Message:
    subChan <- string(v.Data)

The message, by the way, also comes with the name of the channel it was sent to; you can be subscribed to multiple channels and wild-card selections of channels with Redis, so that's useful to know. In this example, we're just using one channel, but you could have multiple chat rooms on different channels.

Back to our switch. We throw away subscription information and exit on errors (letting the
defer earlier tidy up the connection).

  case redis.Subscription:
    break // We don't need to listen to subscription messages,
    case error:
      return
    }
  }
}()

And that routine is set running... We'll pick up the messages in the channel later.

Reading the terminal

We have another channel and goroutine for reading input from the user. The channel
sayChan is populated by a goroutine that reads lines from Stdin. If there's an error, it'll put "/exit" into the channel which also happens to be the command a user would enter to exit.

sayChan := make(chan string)  
  go func() {
    prompt := username + ">"
    bio := bufio.NewReader(os.Stdin)
    for {
      fmt.Print(prompt)
      line, _, err := bio.ReadLine()
      if err != nil {
        fmt.Println(err)
        sayChan <- "/exit"
        return
      }
      sayChan <- string(line)
    }
  }()

Ready to chat

We are now ready, almost, to enter into the main loop of chatting. Just some final things to do, like our first message publication:

conn.Do("PUBLISH", "messages", username+" has joined")  

We send our publish commands from the connection we initially established. It's only subscribing which needs its own channel. This line just announced to everyone listening that the user has logged on. Now we can dive into the loop, which we'll only exit when a chatExit flag is set true:

for !chatExit {  
  select {

As we have three active channels, the subscribed channel subChan, the user entry channel sayChan and the ticker channel tickerChan, we can use the Go select command to listen on all of them. The simplest one to handle is the subscribed channel:

case msg := <-subChan:  
  fmt.Println(msg)

If there's a message to be read, read it and print it.

Remember that ticker we set up? When that goes off we want to set the user key again. This time we want to be sure the key already exists and fail if it doesn't exist. For that, we use the "XX" option; the key must exist otherwise we can assume we've been suspended and the key expiry has kicked in. If that's the case, we'll set the chatExit flag and leave:

case <-tickerChan:  
  val, err = conn.Do("SET", userkey, username, "XX", "EX", "120")
  if err != nil || val == nil {
    fmt.Println("Heartbeat set failed")
    chatExit = true
  }

That leaves the user's input to handle. We read the user's entry from the sayChan channel and if it's an exit command ("/exit"), we set the chatExit to true:

case line := <-sayChan:  
  if line == "/exit" {
    chatExit = true
  }

But if it's a "/who" command, we retrieve that set we added to earlier and print that out. Note the use of the redis.Strings helper to easily coerce the results from the
SMEMBERS Redis command into an array of strings.

else if line == "/who" {  
  names, _ := redis.Strings(conn.Do("SMEMBERS", "users"))
  for _, name := range names {
    fmt.Println(name)
  }
}

If the entry was neither of those commands, we publish whatever was entered to the messages channel in the same way we did with the "joined" announcement:

else {  
  conn.Do("PUBLISH", "messages", username+":"+line)
}

And thats it for the three channel handlers. If nothing has happened, we just sleep for 100 milliseconds rather then chewing up a CPU:

  default:
    time.Sleep(100 * time.Millisecond)
  }
}

Checking out

There's still some tidying up to be done when the client does exit cleanly. We need to delete that user key ourselves, remove our user's name from the set and, it's only polite, publish a message to everyone that we're leaving.

conn.Do("DEL", userkey)  
  conn.Do("SREM", "users", username)
  conn.Do("PUBLISH", "messages", username+" has left")

We deferred closing the Redis connections so they should shut down as we leave, and that's our chat client.

Concluding

There's plenty that could be enhanced in this chat client, but it is primarily an example of how you can use Redis, and Go, to produce reliable publish and subscribe based application. It's a powerful mechanism you can embed into your application to enable interapp communications. Combined with Redis' key/value storage capabilities, it helps show why Redis is considered by many to be a good, flexible yet solid cement for binding application components together.