iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🏣

Building a TCP Server in Elixir to Broadcast Client Input to All Other Clients

に公開

What I am trying to do is exactly what the title says. To achieve this, I implemented it using mtrudel/thousand_island: Thousand Island is a pure Elixir socket server, which is a library for handling socket communication that forms the base layer for mtrudel/bandit: Bandit is a pure Elixir HTTP server for Plug applications, a project that has recently become a hot topic in the Elixir community.

What I want to achieve

  1. The actors are as follows:
    • Server (one)
    • Clients (multiple)
  2. The server waits for TCP connections on port 1234, so each client connects via nc localhost 1234.
  3. When a client sends a message, that message is sent to all clients except the sender.

I've recorded the behavior in an animated GIF. It might be hard to see, so please click the image to enlarge it while watching.

Implementation

Starting the ThousandIsland process

First, we set up the process startup configuration in the Application module. This is done in the usual way.

We will use Registry in addition to ThousandIsland. Note that Registry is started with the keys: :duplicate option, so when you register with the same key, the values passed will be added as a list (refer to the implementation details below).

defmodule Broadcast.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      {ThousandIsland, port: 1234, handler_module: Broadcast.Handler},
      {Registry, keys: :duplicate, name: Broadcast.Registry}
    ]

    opts = [strategy: :one_for_one, name: Broadcast.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Handler Implementation

A key feature of thousand_island is that it allows you to build a nice TCP server just by implementing the ThousandIsland.Handler behavior.

First, we implement handle_connection/2, which is required by the ThousandIsland.Handler behavior. This function is called whenever a client connects. Within this function, we use Registry to record the client's socket information.

Next, we implement handle_data/3, also required by the ThousandIsland.Handler behavior. This function is called when a message is sent from a client. Since it contains the message content from the client, we send that message to all clients except the sender.

For sending messages, we can simply use the handle_cast/2 callback from the GenServer behavior as usual. We retrieve the sockets of connected clients recorded in the Registry and send the message to all clients except the source.

defmodule Broadcast.Handler do
  use ThousandIsland.Handler

  def send_message(pid, from, msg) do
    GenServer.cast(pid, {:send, from, msg})
  end

  @impl ThousandIsland.Handler
  def handle_connection(socket, state) do
    Registry.register(Broadcast.Registry, "clients", socket)
    {:continue, state}
  end

  @impl ThousandIsland.Handler
  def handle_data(msg, socket, state) do
    send_message(self(), socket, msg)
    {:continue, [socket | state]}
  end

  @impl GenServer
  def handle_cast({:send, from, msg}, {socket, state}) do
    Registry.lookup(Broadcast.Registry, "clients")
    |> Enum.filter(fn {_pid, socket} ->
      socket != from
    end)
    |> Enum.each(fn {_pid, socket} ->
      ThousandIsland.Socket.send(socket, msg)
    end)

    {:noreply, {socket, state}}
  end
end

Source Code

The source code is available in the following repository.

kentaro/broadcast: An Example Broadcast Server for TCP Connection

Conclusion

It's a nice library that is easy to use. I'd like to eventually look into the implementation of Bandit, the HTTP server built using this.

Discussion