Here's an example of a TCP Echo Server using Livebook.
Mix.install([{:ranch, "2.0.0"}])
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
TcpEchoServer.Application.start(:normal, [])
input = String.to_charlist(IO.gets("input: "))
:os.cmd('/bin/bash -c "cat <(echo ' ++ input ++ ') | nc localhost 7766"', %{})
:ok
Here's visuals.