TCP Server in Elixir
Public
08 May 09:06

The simplest example could be written in a single module, but the one below makes use of OTP supervision tree to restart the server when something goes wrong.

defmodule Socket.Application do
  @moduledoc false

  use Application

  @doc """
  This is the main function that starts our application's supervision tree.
  """
  def start(_type, _args) do
    # List all child processes to be supervised
    children = [
      {Task.Supervisor, name: Socket.ConnectionSupervisor},
      {Socket.Listener, 3001}
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_all, name: Socket.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
defmodule Socket.Listener do
  @moduledoc """
  This module implements a long-running Task that will listen on a port for TCP connections.
  We're using the Task abstraction to create the listener process, but we want it to restart when
  it fails (Tasks don't restart by default).
  """

  use Task, restart: :permanent
  require Logger

  @doc """
  This is the function that will be called by the Supervisor to start the Listener process.
  We're using `Task.start_link/3` to make the configuration easier.
  """
  def start_link(port) do
    Task.start_link(__MODULE__, :run, [port])
  end

  @doc """
  The main function of our Listener. It will try to listen on a given port and start accepting
  connections if it succeeds. Otherwise it will fail (and be restarted by the Supervisor).
  """
  def run(port) do
    opts = [:binary, packet: 0, active: false, reuseaddr: true, ip: {127, 0, 0, 1}]

    case :gen_tcp.listen(port, opts) do
      {:ok, listen_socket} ->
        Logger.info("Listening for connections on #{port}...")
        accept_connections(listen_socket)

      {:error, reason} ->
        raise "Could not listen on #{port}: #{reason}"
    end
  end

  @doc """
  A recursive function that aceepts new connections and delegates them to new processes.
  """
  defp accept_connections(listen_socket) do
    # This process will hang until a new connection is opened...
    {:ok, socket} = :gen_tcp.accept(listen_socket)

    # ...then spawn a new process to handle the connection...
    Task.Supervisor.start_child(Socket.ConnectionSupervisor, Socket.Connection, :run, [socket])

    # ...and continue listening for new connections
    accept_connections(listen_socket)
  end
end
defmodule Socket.Connection do
  @moduledoc """
  This module creates an abstraction around a TCP connection.
  """

  require Logger

  @doc """
  A recursive function (or a loop) that waits for data from the client, reverses it (as a string)
  and sends it back. The loop ends when trying to receive data fails.
  """
  def run(socket) do
    case :gen_tcp.recv(socket, 0) do
      {:ok, msg} ->
        :gen_tcp.send(socket, transform_message(msg))
        run(socket)

      {:error, reason} ->
        Logger.info("Connection closed: #{reason}")
    end
  end

  defp transform_message(msg) do
    msg
    |> String.trim()
    |> String.reverse()
  end
end

Comments

Empty! You must sign in to add comments.