# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE # Version 2, December 2004 # # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION # # 0. You just DO WHAT THE FUCK YOU WANT TO. defmodule Socket.TCP do @moduledoc """ This module wraps a passive TCP socket using `gen_tcp`. ## 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 - `:port` the local port to use - `:fd` an already opened file descriptor to use * `:backlog` sets the listen backlog * `:watermark` must be a keyword list - `:low` defines the `:low_watermark`, see `inet:setopts` - `:high` defines the `:high_watermark`, see `inet:setopts` * `:version` sets the IP version to use * `:options` must be a list of atoms - `:keepalive` sets `SO_KEEPALIVE` - `:nodelay` sets `TCP_NODELAY` * `:packet` see `inet:setopts` * `:size` sets the max length of the packet body, see `inet:setopts` ## Examples server = Socket.TCP.listen!(1337, packet: :line) client = server |> Socket.accept! client |> Socket.Stream.send!(client |> Socket.Stream.recv!) client |> Socket.Stream.close """ use Socket.Helpers require Record @opaque t :: port @doc """ Return a proper error string for the given code or nil if it can't be converted. """ @spec error(term) :: String.t() def error(code) do case :inet.format_error(code) do ~c"unknown POSIX error" -> nil message -> message |> to_string end end @doc """ Create a TCP socket connecting to the given host and port tuple. """ @spec connect({Socket.Address.t(), :inet.port_number()}) :: {:ok, t} | {:error, Socket.Error.t()} def connect({address, port}) do connect(address, port) end @doc """ Create a TCP socket connecting to the given host and port tuple, raising if an error occurs. """ @spec connect!({Socket.Address.t(), :inet.port_number()}) :: t | no_return defbang(connect(descriptor)) @doc """ Create a TCP socket connecting to the given host and port tuple and options, or to the given host and port. """ @spec connect( {Socket.Address.t(), :inet.port_number()} | Socket.Address.t(), Keyword.t() | :inet.port_number() ) :: {:ok, t} | {:error, Socket.Error.t()} def connect({address, port}, options) when options |> is_list do connect(address, port, options) end def connect(address, port) when port |> is_integer do connect(address, port, []) end @doc """ Create a TCP socket connecting to the given host and port tuple and options, or to the given host and port, raising if an error occurs. """ @spec connect!( {Socket.Address.t(), :inet.port_number()} | Socket.Address.t(), Keyword.t() | :inet.port_number() ) :: t | no_return defbang(connect(address, port)) @doc """ Create a TCP socket connecting to the given host and port. """ @spec connect(String.t() | :inet.ip_address(), :inet.port_number(), Keyword.t()) :: {:ok, t} | {:error, Socket.Error.t()} def connect(address, port, options) when address |> is_binary do timeout = options[:timeout] || :infinity options = Keyword.delete(options, :timeout) :gen_tcp.connect(String.to_charlist(address), port, arguments(options), timeout) end @doc """ Create a TCP socket connecting to the given host and port, raising in case of error. """ @spec connect!(String.t() | :inet.ip_address(), :inet.port_number(), Keyword.t()) :: t | no_return defbang(connect(address, port, options)) @doc """ Create a TCP socket listening on an OS chosen port, use `local` to know the port it was bound on. """ @spec listen :: {:ok, t} | {:error, Socket.Error.t()} def listen do listen(0, []) end @doc """ Create a TCP socket listening on an OS chosen port, use `local` to know the port it was bound on, raising in case of error. """ @spec listen! :: t | no_return defbang(listen) @doc """ Create a TCP socket listening on an OS chosen port using the given options or listening on the given port. """ @spec listen(:inet.port_number() | Keyword.t()) :: {:ok, t} | {:error, Socket.Error.t()} def listen(port) when port |> is_integer do listen(port, []) end def listen(options) when options |> is_list do listen(0, options) end @doc """ Create a TCP socket listening on an OS chosen port using the given options or listening on the given port, raising in case of error. """ @spec listen!(:inet.port_number() | Keyword.t()) :: t | no_return defbang(listen(port_or_options)) @doc """ Create a TCP socket listening on the given port and using the given options. """ @spec listen(:inet.port_number(), Keyword.t()) :: {:ok, t} | {:error, Socket.Error.t()} def listen(port, options) when options |> is_list do options = options |> Keyword.put(:mode, :passive) |> Keyword.put_new(:reuse, true) :gen_tcp.listen(port, arguments(options)) end @doc """ Create a TCP socket listening on the given port and using the given options, raising in case of error. """ @spec listen!(:inet.port_number(), Keyword.t()) :: t | no_return defbang(listen(port, options)) @doc """ Accept a new client from a listening socket, optionally passing options. """ @spec accept(t | port) :: {:ok, t} | {:error, Error.t()} @spec accept(t | port, Keyword.t()) :: {:ok, t} | {:error, Error.t()} def accept(socket, options \\ []) do timeout = options[:timeout] || :infinity case :gen_tcp.accept(socket, timeout) do {:ok, socket} -> # XXX: the error code here is not checked case options[:mode] do :active -> :inet.setopts(socket, active: true) :once -> :inet.setopts(socket, active: :once) :passive -> :inet.setopts(socket, active: false) nil -> :ok end {:ok, socket} {:error, reason} -> {:error, reason} end end @doc """ Accept a new client from a listening socket, optionally passing options, raising if an error occurs. """ @spec accept!(t) :: t | no_return @spec accept!(t, Keyword.t()) :: t | no_return defbang(accept(self)) defbang(accept(self, options)) @doc """ Set the process which will receive the messages. """ @spec process(t, pid) :: :ok | {:error, :closed | :not_owner | Error.t()} def process(socket, pid) do :gen_tcp.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 Socket.Error, reason: code end end @doc """ Set options of the socket. """ @spec options(t | Socket.SSL.t() | port, Keyword.t()) :: :ok | {:error, Socket.Error.t()} def options(socket, options) when socket |> Record.is_record(:sslsocket) do Socket.SSL.options(socket, options) end def options(socket, options) when socket |> is_port do :inet.setopts(socket, arguments(options)) end @doc """ Set options of the socket, raising if an error occurs. """ @spec options!(t | Socket.SSL.t() | port, Keyword.t()) :: :ok | no_return defbang(options(socket, options)) @doc """ Convert TCP 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 {:size, _} -> true {:packet, _} -> true {:backlog, _} -> true {:watermark, _} -> true {:local, _} -> true {:version, _} -> true {:options, _} -> true _ -> false end) {local, global} = { Map.get(options, true, []), Map.get(options, false, []) } Socket.arguments(global) ++ Enum.flat_map(local, fn {:as, :binary} -> [:binary] {:as, :list} -> [:list] {:size, size} -> [{:packet_size, size}] {:packet, packet} -> [{:packet, packet}] {:backlog, backlog} -> [{:backlog, backlog}] {:watermark, options} -> Enum.flat_map(options, fn {:low, low} -> [{:low_watermark, low}] {:high, high} -> [{:high_watermark, high}] end) {:local, options} -> Enum.flat_map(options, fn {:address, address} -> [{:ip, Socket.Address.parse(address)}] {:port, port} -> [{:port, port}] {:fd, fd} -> [{:fd, fd}] end) {:version, 4} -> [:inet] {:version, 6} -> [:inet6] {:options, options} -> Enum.flat_map(options, fn :keepalive -> [{:keepalive, true}] :nodelay -> [{:nodelay, true}] end) end) end end