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