Skip to content
Switch branches/tags
Go to file
Cannot retrieve contributors at this time
82 lines (67 sloc) 3.22 KB
defmodule Peerage.Via.Udp do
@moduledoc """
Use UDP multicast to find other nodes.
config :peerage, via: Peerage.Via.Udp, serves: true
ip: {0,0,0,0},
port: 45_900,
multicast_addr: {230,1,1,1}
Will both broadcast and receive node names via UDP on port 45900,
and keep track of ones it's seen in process state. It's a GenServer,
so we let Peerage know it needs to be run and
supervised with `serves: true`.
`Peerage.Server` periodically calls `poll()`, which is a client
function for the GenServer's `handle_call(:poll, _, state)`, which
returns the seen node names from state.
Only one node can bind the socket, but you can test multiple
nodes using docker, like
For more info on UDP in Elixir, see
[this scaleSmall post on multicast UDP in Elixir from
especially the explation of gen_udp's `active: N` mode.
use GenServer
require Logger
@behaviour Peerage.Provider
def start_link, do: GenServer.start_link __MODULE__, :ok, name: __MODULE__
def init(:ok) do
{:ok, socket} = port = get_port(), [
:binary, reuseaddr: true, broadcast: true, multicast_loop: true,
active: 10, multicast_ttl: get_ttl(),
ip: get_ip(), add_membership: {maddr = get_maddr(), {0,0,0,0}}
{:ok, %{seen:, conn: {maddr, port, socket}}, 0}
@doc "Client function: `Peerage.Provider` callback. Calls this GenServer."
def poll do
__MODULE__ |> GenServer.whereis |> do_poll
defp do_poll(pid) when is_pid(pid), do: __MODULE__ |>
defp do_poll(_), do: Logger.debug "(no server)"; []
@doc "Server function: returns list of node names we've seen."
def handle_call(:poll, _, state = %{seen: ms}), do: {:reply, MapSet.to_list(ms), state}
def handle_call(:poll, _, state), do: {:reply, [], state}
@doc "Broadcast our node name via UDP every 3-7 seconds"
def handle_info(:broadcast, state = %{conn: {addr, port, sock}}) do
:ok = :gen_udp.send(sock, addr, port, ["Peer:#{ node() }"])
Process.send_after(self(), :broadcast, :rand.uniform(4_000) + 3_000)
{:noreply, state}
def handle_info(:timeout, state), do: handle_info(:broadcast, state)
@doc "Handle UDP packet. If it's a node name broadcast, adds to `state.seen`."
def handle_info({:udp,sock,_,_, "Peer:" <> name}, state = %{seen: ms}) do
Logger.debug " -> Peerage.Via.Udp sees: #{ name }"
:inet.setopts(sock, active: 1)
{:noreply, %{state | seen: ms |> MapSet.put(name |> String.to_atom)}}
def handle_info({:udp,sock,_,_,_}, state) do # bad packet,
:inet.setopts(sock, active: 1) # but don't die.
{:noreply, state} # ^ TODO configurable.
def terminate(_,_, %{conn: {_,_,sock}}), do: :gen_udp.close(sock)
# helpers
defp get_port, do: Application.get_env :peerage, :port, 45_900
defp get_ip, do: Application.get_env :peerage, :ip, {0,0,0,0}
defp get_maddr, do: Application.get_env :peerage, :multicast_addr, {230,1,1,1}
defp get_ttl, do: Application.get_env :peerage, :ttl, 1