Skip to Content
Course content

111: Building a Simple Actor-Based Chat Server

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Do I need a separate actor for every single user, or can one actor handle the whole room?

You definitely want an actor for every user. If you try to cram all the connection logic, buffer management, and state for 100 different users into one "God Actor," you're just recreating the same concurrency nightmares we use actors to avoid. Plus, you'll hit a bottleneck because an actor processes messages sequentially.

I usually structure this with a hierarchy: a ChatRoom actor that acts as the coordinator, and multiple UserActor instances. The UserActor handles the messy details of the specific socket connection, while the ChatRoom focuses on who is currently in the room and where messages need to go. It keeps the concerns separated and makes the code much easier to reason about.

case class Join(username: String, ref: ActorRef)
case class Message(from: String, text: String)

class ChatRoom extends Actor {
  var users = Map.empty[String, ActorRef]

  def receive = {
    case Join(name, ref) =>
      users += (name -> ref)
      println(s"$name joined the party!")
    case Message(from, text) =>
      // We'll talk about how to broadcast this in a second
  }
}

How do I actually get the room actor to send a message to everyone except the person who sent it?

This is where the ActorRef really shines. Since the ChatRoom stores a map of usernames to their corresponding actor references, broadcasting is essentially just iterating over that map. I've seen people try to use a shared list or a database for this in real-time apps, but that's overkill and slow. Just keep the references in memory.

To avoid the "echo" effect—where the sender receives their own message back—you just filter the map during the broadcast. Here is how I'd implement that Message handler:

case Message(from, text) =>
  val broadcastMsg = s"$from: $text"
  users.foreach { case (name, ref) =>
    if (name != from) {
      ref ! broadcastMsg
    }
  }

It's simple, direct, and doesn't block the ChatRoom actor from receiving the next message while the others are processing the broadcast.

How do I stop the server from trying to send messages to someone who has already closed their connection?

This is the part most people forget. If a user closes their browser or loses signal, the UserActor might die, but the ChatRoom actor still has that ActorRef in its map. Sending messages to a dead actor doesn't crash your system (the messages just go to deadLetters), but it's a waste of resources and a potential memory leak.

The professional way to handle this is using context.watch(ref). This tells the actor system: "Let me know if this other actor dies." When the UserActor terminates, the ChatRoom will automatically receive a Terminated message. I always use this pattern for any long-lived session management.

import scala.akka.actor.Terminated

class ChatRoom extends Actor {
  var users = Map.empty[String, ActorRef]

  def receive = {
    case Join(name, ref) =>
      users += (name -> ref)
      context.watch(ref) // Start monitoring this user

    case Terminated(ref) =>
      // Find who this ref belonged to and scrub them from the map
      users = users.filterNot(_._2 == ref)
      println("A user disconnected. Cleaned up the map.")
  }
}

By doing this, your users map stays lean and reflects the actual state of the world.




📋 Practical Task

Implement a /whisper Command for the Chat Server

Currently, your chat server only supports global broadcasting. Your task is to modify the ChatRoom actor to handle private messages between two specific users.

  • Define a new message type PrivateMessage(from: String, to: String, text: String).
  • Update the ChatRoom's receive block to handle PrivateMessage.
  • The logic should look up the to username in the users map.
  • If the user exists, send the message only to them. If the user doesn't exist, send a message back to the sender (the from user) stating "User not found."
  • Ensure that the sender does not receive their own private message.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.