TCP GenServer in Elixir

As part of a toy chat server I've been working on, I needed to implement a TCP server in Elixir. This is something that's so commonly done, there are tons of resources online showing the usage of :gen_tcp. However, I wanted to showcase a neat way of integrating this into an Elixir project, without the use of a library like Ranch.

Firstly, we define our application supervisor's children as follows:

children = [
  {MyApp.Server, get_port},
  {DynamicSupervisor, strategy: :one_for_one, name: MyApp.ClientSupervisor},
]
Supervisor.start_link(children, strategy: :one_for_one)

The Server component will be responsible for accepting connections from new clients, spawning a new process to handle that connection, and then handing over control before listening for new connections again. ClientSupervisor is a DynamicSupervisor which monitors the client processes.

Here's what the Server code looks like:

defmodule MyApp.Server do
  alias MyApp.Client

  def start_link(port) do
    Task.start_link(__MODULE__, :accept, [port])
  end

  def accept(port) do
    {:ok, listen_socket} = :gen_tcp.listen(
      port,
      [:binary, packet: :line, active: :once, reuseaddr: true]
    )
    loop_acceptor(listen_socket)
  end

  defp loop_acceptor(listen_socket) do
    {:ok, socket} = :gen_tcp.accept(listen_socket)
    {:ok, pid} = DynamicSupervisor.start_child(
      MyApp.ClientSupervisor,
      {MyApp.Client, socket}
    )
    :gen_tcp.controlling_process(socket, pid)
    loop_acceptor(listen_socket)
  end
end

Note that this is using gen_tcp's active: true mode, which isn't documented particularly well. It causes data to be delivered as messages to the controlling process, rather than via the return value of a call to :gen_tcp.recv.

Now, we can define the Client, which is a standard GenServer, as follows:

defmodule MyApp.Client do
  use GenServer

  def start_link(socket, opts \\ []) do
    GenServer.start_link(__MODULE__, socket, opts)
  end

  def init(socket) do
    %{ socket: socket }
  end

  def send(pid, data) do
    GenServer.cast(pid, {:send, data})
  end

  # TCP callbacks

  def handle_info({:tcp, socket, data}, state) do
    state = process_data(data, state)
    {:noreply, state}
  end

  def handle_info({:tcp_closed, _socket}, state) do
    Process.exit(self(), :normal)
  end

  # GenServer callbacks

  def handle_cast({:send, data}, %{socket: socket} = state) do
    :gen_tcp.send(socket, data)
    {:noreply, state}
  end
end

The client is initialized with a socket, which has been passed from the server. It's stored in the state of the process, alongside any other state we need to manage for this connection.

I have shown an example server callback above, which can be used to send data to the client from any other process via Client.send(pid, data).

You can see a fully working example in the chat server these examples were taken from here.