Skip to content

A guide on how to use Ranch with Elixir

License

Notifications You must be signed in to change notification settings

asciibeats/elixir_ranch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

A guide on how to use Ranch with Elixir

Since the top search results where outdated and generally a bit unsatisfactory to me, here is my example based on the official Ranch guide for Erlang.

Configuration

Add Ranch to the list of dependencies in your mix.exs:

defp deps do
  [
    {:ranch, "~> 2.1"}
  ]
end

In accordance with the official guide, I created a simple echo server, which just responds with the message one sends to it:

defmodule ElixirRanch.Protocols.Echo do
  @behaviour :ranch_protocol
  @timeout 5000

  def start_link(ref, transport, opts) do
    {:ok, spawn_link(__MODULE__, :init, [ref, transport, opts])}
  end

  def init(ref, transport, _opts) do
    {:ok, socket} = :ranch.handshake(ref)
    loop(socket, transport)
  end

  defp loop(socket, transport) do
    case transport.recv(socket, 0, @timeout) do
      {:ok, data} ->
        :ok = transport.send(socket, data)
        loop(socket, transport)
      _ ->
        transport.close(socket)
    end
  end
end

Or, if you like to use GenServer:

defmodule ElixirRanch.Protocols.EchoServer do
  use GenServer

  @behaviour :ranch_protocol
  @timeout 5000

  @impl true
  def start_link(ref, transport, opts) do
    {:ok, :proc_lib.spawn_link(__MODULE__, :init, [{ref, transport, opts}])}
  end

  @impl true
  def init({ref, transport, _opts}) do
    {:ok, socket} = :ranch.handshake(ref)
    :ok = transport.setopts(socket, active: :once)
    :gen_server.enter_loop(__MODULE__, [], {socket, transport}, @timeout)
  end

  @impl true
  def handle_info({:tcp, socket, data}, {socket, transport} = state) do
    :ok = transport.send(socket, data)
    :ok = transport.setopts(socket, active: :once)
    {:noreply, state, @timeout}
  end

  @impl true
  def handle_info(_, {socket, transport} = state) do
    transport.close(socket)
    {:stop, :shutdown, state}
  end
end

For clarity I encapsulated both listeners in a module:

defmodule ElixirRanch.Listeners.Echo do
  def child_spec(opts) do
    :ranch.child_spec(__MODULE__, :ranch_tcp, opts, ElixirRanch.Protocols.Echo, [])
  end
end

Or for the GenServer:

defmodule ElixirRanch.Listeners.EchoServer do
  def child_spec(opts) do
    :ranch.child_spec(__MODULE__, :ranch_tcp, opts, ElixirRanch.Protocols.EchoServer, [])
  end
end

Then I embedded both listeners into a supervision tree:

defmodule ElixirRanch.ListenerSup do
  use Supervisor

  def start_link(args) do
    Supervisor.start_link(__MODULE__, args)
  end

  @impl true
  def init({}) do
    children = [
      {ElixirRanch.Listeners.Echo, [{:port, 5555}]},
      {ElixirRanch.Listeners.EchoServer, [{:port, 5556}]}
    ]
    
    Supervisor.init(children, strategy: :one_for_one)
  end
end

Application

To put it all together, I added the listener supervisor to an application file...

defmodule ElixirRanch.Application do
  use Application

  def start(_type, _args) do
    children = [
      {ElixirRanch.ListenerSup, {}}
    ]

    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

...and added the application to my mix.exs, so it would start automatically:

def application do
  [
    mod: {ElixirRanch.Application, []}
  ]
end

Try it!

Clone this repo and get Ranch:

git clone https://github.com/asciibeats/elixir_ranch.git
cd elixir_ranch
mix deps.get

Start the server:

mix run --no-halt

Connect to it:

telnet localhost 5555

Type something, press ENTER and the server should respond with the same message. Wait five seconds for the connection to time out. Change the port to 5556 to try the protocol using GenServer. It should do the same.

I hope you found this little guide helpful!