p2pchat/lib/p2p_chat/transport.ex

114 lines
2.8 KiB
Elixir

defmodule P2pChat.Transport do
require Logger
import P2pChat.TransportProto
alias P2pChat.TransportProto.Messages, as: Messages
use GenServer
@uuid_namespace "8a6a64ee-1628-4fdb-b3fc-574ae5eb5797"
defstruct [:socket, :netid, :neighbors]
def start_link(args \\ []) do
GenServer.start_link(__MODULE__, args)
end
@impl true
def init(args) do
{:ok, socket} = open_listening_socket(args)
{:ok, hostname} = :net.gethostname()
netid = UUID.uuid5(@uuid_namespace, to_string(hostname))
initial_neighbors = Access.get(args, :initial_neighbors, %{})
{:ok,
%__MODULE__{
socket: socket,
netid: netid,
neighbors: initial_neighbors
}}
end
def open_listening_socket(args) do
port = Access.get(args, :port, 12345)
listen_options = [:binary, :inet]
Logger.info("Starting transport on port #{port}")
{:ok, socket} = :gen_udp.open(port, listen_options)
{:ok, socket}
end
@impl true
def handle_info({:udp, _socket, peer_ip, peer_port, payload}, state) do
msg = decode_message(payload)
Logger.debug("Received UDP message from #{:inet.ntoa(peer_ip)}@#{peer_port}: #{inspect(msg)}")
{_, state} = case msg do
%Messages.Hello{} -> __MODULE__.handle_cast({:add_neighbor, msg.netid, {peer_ip, peer_port}}, state)
_ -> throw(:unhandled_message)
end
{:noreply, state}
end
@impl true
def handle_call(:get_netid, _from, state) do
{:reply, state.netid, state}
end
@impl true
def handle_call(:get_neighbors, _from, state) do
{:reply, state.neighbors, state}
end
@impl true
def handle_cast({:add_neighbor, netid, connector}, state) do
new_neighbors =
Map.update(state.neighbors, netid, MapSet.new([connector]), fn current_value ->
MapSet.put(current_value, connector)
end)
new_state = %__MODULE__{
socket: state.socket,
netid: state.netid,
neighbors: new_neighbors
}
{:noreply, new_state}
end
@impl true
def handle_cast(:inform_about_self, state) do
inform_msg =
encode_message(%Messages.Hello{
netid: state.netid
})
Enum.each(Map.keys(state.neighbors), fn i_netid ->
Enum.each(Map.get(state.neighbors, i_netid), fn i_connector ->
{i_addr, i_port} = i_connector
Logger.debug("Informing neighbor #{i_netid} at #{:inet.ntoa(i_addr)}@#{i_port} about self")
:gen_udp.send(state.socket, i_addr, i_port, inform_msg)
end)
end)
{:noreply, state}
end
#
# CLIENT API
#
def get_netid(pid) do
GenServer.call(pid, :get_netid)
end
def get_neighbors(pid) do
GenServer.call(pid, :get_neighbors)
end
def add_neighbor(pid, netid, connector) do
GenServer.cast(pid, {:add_neighbor, netid, connector})
end
def inform_about_self(pid) do
GenServer.cast(pid, :inform_about_self)
end
end