defmodule GSMLG.Socket.UDP do
@moduledoc """
This module wraps a UDP socket using `gen_udp`.
## Options
When creating a socket you can pass a series of options to use for it.
* `:as` sets the kind of value returned by recv, either `:binary` or `:list`,
the default is `:binary`.
* `:mode` can be either `:passive` or `:active`, default is `:passive`
* `:local` must be a keyword list
- `:address` the local address to use
- `:fd` an already opened file descriptor to use
* `:version` sets the IP version to use
* `:broadcast` enables broadcast sending
## Examples
server = GSMLG.Socket.UDP.open!(1337)
{ data, client } = server |> GSMLG.Socket.Datagram.recv!
server |> GSMLG.Socket.Datagram.send! data, client
"""
@type t :: port
use GSMLG.Socket.Helpers
@doc """
Create a UDP socket listening on an OS chosen port, use `local` to know the
port it was bound on.
"""
@spec open :: {:ok, t} | {:error, Error.t()}
def open do
open(0, [])
end
@doc """
Create a UDP socket listening on an OS chosen port, use `local` to know the
port it was bound on, raising if an error occurs.
"""
@spec open! :: t | no_return
defbang(open)
@doc """
Create a UDP socket listening on the given port or using the given options.
"""
@spec open(:inet.port_number() | Keyword.t()) :: {:ok, t} | {:error, Error.t()}
def open(port) when port |> is_integer do
open(port, [])
end
def open(options) when options |> is_list do
open(0, options)
end
@doc """
Create a UDP socket listening on the given port or using the given options,
raising if an error occurs.
"""
@spec open!(:inet.port_number() | Keyword.t()) :: t | no_return
defbang(open(port_or_options))
@doc """
Create a UDP socket listening on the given port and using the given options.
"""
@spec open(:inet.port_number(), Keyword.t()) :: {:ok, t} | {:error, Error.t()}
def open(port, options) do
options = Keyword.put_new(options, :mode, :passive)
:gen_udp.open(port, arguments(options))
end
@doc """
Create a UDP socket listening on the given port and using the given options,
raising if an error occurs.
"""
@spec open!(:inet.port_number(), Keyword.t()) :: t | no_return
defbang(open(port, options))
@doc """
Set the process which will receive the messages.
"""
@spec process(t | port, pid) :: :ok | {:error, :closed | :not_owner | Error.t()}
def process(socket, pid) when socket |> is_port do
:gen_udp.controlling_process(socket, pid)
end
@doc """
Set the process which will receive the messages, raising if an error occurs.
"""
@spec process!(t | port, pid) :: :ok | no_return
def process!(socket, pid) do
case process(socket, pid) do
:ok ->
:ok
:closed ->
raise RuntimeError, message: "the socket is closed"
:not_owner ->
raise RuntimeError, message: "the current process isn't the owner"
code ->
raise GSMLG.Socket.Error, reason: code
end
end
@doc """
Set options of the socket.
"""
@spec options(t, Keyword.t()) :: :ok | {:error, Error.t()}
def options(socket, opts) when socket |> is_port do
:inet.setopts(socket, arguments(opts))
end
@doc """
Convert UDP options to `:inet.setopts` compatible arguments.
"""
@spec arguments(Keyword.t()) :: list
def arguments(options) do
options =
options
|> Keyword.put_new(:as, :binary)
options =
Enum.group_by(options, fn
{:as, _} -> true
{:local, _} -> true
{:version, _} -> true
{:broadcast, _} -> true
{:multicast, _} -> true
{:membership, _} -> true
_ -> false
end)
{local, global} = {
Map.get(options, true, []),
Map.get(options, false, [])
}
GSMLG.Socket.arguments(global) ++
Enum.flat_map(local, fn
{:as, :binary} ->
[:binary]
{:as, :list} ->
[:list]
{:local, options} ->
Enum.flat_map(options, fn
{:address, address} ->
[{:ip, GSMLG.Socket.Address.parse(address)}]
{:fd, fd} ->
[{:fd, fd}]
end)
{:version, 4} ->
[:inet]
{:version, 6} ->
[:inet6]
{:broadcast, broadcast} ->
[{:broadcast, broadcast}]
{:multicast, options} ->
Enum.flat_map(options, fn
{:address, address} ->
[{:multicast_if, GSMLG.Socket.Address.parse(address)}]
{:loop, loop} ->
[{:multicast_loop, loop}]
{:ttl, ttl} ->
[{:multicast_ttl, ttl}]
end)
{:membership, membership} ->
[{:add_membership, membership}]
end)
end
end