Skip to content

Instantly share code, notes, and snippets.

@paulo-ferraz-oliveira
Last active August 27, 2023 21:10
Show Gist options
  • Save paulo-ferraz-oliveira/a14bf478c0939362b29add6b76be8d51 to your computer and use it in GitHub Desktop.
Save paulo-ferraz-oliveira/a14bf478c0939362b29add6b76be8d51 to your computer and use it in GitHub Desktop.
TCP Echo Server using Livebook (with inputs)

TCP Echo Server using Livebook

Here's an example of a TCP Echo Server using Livebook.

Install the dependencies

Mix.install([{:ranch, "2.0.0"}])

Define the modules

defmodule TcpEchoServer.Listener do
  @moduledoc false

  require Logger

  @ranch_listener_ref __MODULE__
  @listening_port 7766

  def child_spec([]) do
    socket_opts = [port: @listening_port, keepalive: true, nodelay: true]
    transport = :ranch_tcp
    transport_opts = %{num_acceptors: 10, socket_opts: socket_opts}
    protocol = TcpEchoServer.Connection
    protocol_opts = []
    Logger.notice("Listening on port #{@listening_port}")
    :ranch.child_spec(@ranch_listener_ref, transport, transport_opts, protocol, protocol_opts)
  end
end
defmodule TcpEchoServer.Connection do
  @moduledoc false
  @behaviour :ranch_protocol

  require Logger
  require Record
  use GenServer

  #
  # Records and types
  #

  Record.defrecord(:state,
    conn_id: nil,
    socket: nil
  )

  #
  # :ranch_protocol functions
  #

  def start_link(acceptance_ref, :ranch_tcp = _transport, [] = _transport_opts) do
    :proc_lib.start_link(__MODULE__, :sys_init, [acceptance_ref])
  end

  #
  # :sys functions
  #

  def sys_init(acceptance_ref) do
    :proc_lib.init_ack({:ok, self()})
    {:ok, socket} = :ranch.handshake(acceptance_ref)
    # always call `:terminate/2' unless killed
    _ = Process.flag(:trap_exit, true)
    conn_id = System.unique_integer([:positive])
    Logger.info("Connection #{conn_id} accepted")
    state = state(conn_id: conn_id, socket: socket)

    socket_opts = [
      packet: :line,
      nodelay: true,
      keepalive: true,
      active: true
    ]

    _ = :ranch_tcp.setopts(socket, socket_opts)
    :gen_server.enter_loop(__MODULE__, _opts = [], state)
  end

  #
  # GenServer functions
  #

  @spec init(term) :: no_return

  def init(_) do
    raise "Not supposed to be called"
  end

  def handle_info({:tcp, socket, packet}, state(socket: socket, conn_id: conn_id) = state) do
    Logger.info("[#{conn_id}] Echoing: #{inspect(packet)}")
    reply = ["you said: ", packet, ?\n]
    # echo
    _ = :gen_tcp.send(socket, reply)
    {:noreply, state}
  end

  def handle_info({:tcp_closed, socket}, state(socket: socket) = state) do
    {:stop, :normal, state}
  end

  def handle_info({:tcp_error, socket}, state(socket: socket) = state) do
    {:stop, :normal, state}
  end

  def terminate(_reason, state) do
    state(conn_id: conn_id) = state
    Logger.info("Connection #{conn_id} closed")
  end
end
defmodule TcpEchoServer.Application do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Starts a worker by calling: TcpEchoServer.Worker.start_link(arg)
      # {TcpEchoServer.Worker, arg}
      TcpEchoServer.Listener
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: TcpEchoServer.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Run the server

TcpEchoServer.Application.start(:normal, [])

Run the demo

input = String.to_charlist(IO.gets("input: "))

:os.cmd('/bin/bash -c "cat <(echo ' ++ input ++ ') | nc localhost 7766"', %{})
:ok
@paulo-ferraz-oliveira
Copy link
Author

Here's visuals.

run_demo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment