# Using `:gen_tcp`

SmolNet can back Erlang's `:gen_tcp` and `:inet` APIs. This is the best
interface for code that already expects OTP socket conventions such as active
mode, packet framing, controlling-process ownership, and OTP-style error atoms.

## Select the SmolNet backend

Every socket needs two SmolNet-specific options:

- `{:tcp_module, module}` selects the callback for its address family.
- `{:smolnet_stack, stack}` selects the independent stack that owns it.

Use matching callback and family options:

| Family | Callback | Family option |
| --- | --- | --- |
| IPv4 | `SmolNet.Inet.Tcp` | `:inet` |
| IPv6 | `SmolNet.Inet6.Tcp` | `:inet6` |

For example, these are passive binary IPv4 options:

```elixir
options = [
  {:tcp_module, SmolNet.Inet.Tcp},
  {:smolnet_stack, stack},
  :inet,
  :binary,
  {:active, false},
  {:packet, :raw}
]
```

The callback option affects only sockets created with that option list. It does
not replace the node-wide inet backend.

## A complete loopback example

`SmolNet.Loopback` is useful for examples and tests because it sends the
stack's packets straight back to the same stack:

```elixir
{:ok, _link, stack} =
  SmolNet.Loopback.start_link(addresses: [{{127, 0, 0, 1}, 8}])

options = [
  {:tcp_module, SmolNet.Inet.Tcp},
  {:smolnet_stack, stack},
  :inet,
  :binary,
  {:active, false},
  {:ip, {127, 0, 0, 1}}
]

{:ok, listener} = :gen_tcp.listen(8080, options)

server =
  Task.async(fn ->
    {:ok, socket} = :gen_tcp.accept(listener, 5_000)
    {:ok, request} = :gen_tcp.recv(socket, 0, 5_000)
    :ok = :gen_tcp.send(socket, ["echo: ", request])
    :ok = :gen_tcp.close(socket)
  end)

{:ok, client} =
  :gen_tcp.connect({127, 0, 0, 1}, 8080, options, 5_000)

:ok = :gen_tcp.send(client, "hello")
{:ok, "echo: hello"} = :gen_tcp.recv(client, 0, 5_000)

:ok = Task.await(server, 5_000)
:ok = :gen_tcp.close(client)
:ok = :gen_tcp.close(listener)
:ok = SmolNet.stop_stack(stack)
```

The same example is available as `examples/loopback.exs` in a source checkout.

## Clients

Connect with the normal `:gen_tcp` API. The target address must match the
selected family:

```elixir
ipv6_options = [
  {:tcp_module, SmolNet.Inet6.Tcp},
  {:smolnet_stack, stack},
  :inet6,
  :binary,
  {:active, false}
]

peer = {0xFD00, 0, 0, 0, 0, 0, 0, 2}

{:ok, socket} = :gen_tcp.connect(peer, 443, ipv6_options, 5_000)
:ok = :gen_tcp.send(socket, "request")
{:ok, response} = :gen_tcp.recv(socket, 0, 5_000)
:ok = :gen_tcp.shutdown(socket, :write)
:ok = :gen_tcp.close(socket)
```

The stack must have a route to the peer, and the application's link must carry
the emitted packets to that route. A finite connect or receive timeout is
measured in the calling process; the stack owner and native scheduler never
block waiting for traffic.

## Servers

Listening and accepting use the standard calls:

```elixir
server_options = [{:backlog, 16} | ipv6_options]

{:ok, listener} = :gen_tcp.listen(8080, server_options)
{:ok, socket} = :gen_tcp.accept(listener, 5_000)
{:ok, request} = :gen_tcp.recv(socket, 0, 5_000)
:ok = :gen_tcp.send(socket, request)
```

The backlog defaults to 5 and may be set from 1 through 128. Internally, a
listener maintains a bounded pool of native listening sockets. Accepted
sockets inherit the listener's mode, active setting, packet framing, buffer
sizes, and send-timeout policy, but have independent adapter state.

Closing a listener aborts a pending accept and releases children still waiting
in its accepted queue. Children already returned by `:gen_tcp.accept/2` remain
usable.

## Passive and active delivery

With `{:active, false}`, call `:gen_tcp.recv/3`. In raw mode, a length of zero
returns one currently available chunk. A positive length accumulates until
that many bytes arrive, the peer closes, or the operation fails:

```elixir
{:ok, chunk} = :gen_tcp.recv(socket, 0, 5_000)
{:ok, exactly_32_bytes} = :gen_tcp.recv(socket, 32, 5_000)
```

Active mode supports `true`, `:once`, and counts from 1 through 32,767:

```elixir
:ok = :inet.setopts(socket, active: :once)

receive do
  {:tcp, ^socket, data} -> handle_data(data)
  {:tcp_closed, ^socket} -> handle_close()
  {:tcp_error, ^socket, reason} -> handle_error(reason)
end
```

Counted mode sends `{:tcp_passive, socket}` when its count is exhausted. Active
delivery is bounded to 16 native reads or complete logical packets per adapter
mailbox turn, so a busy socket yields to other processes.

Only the controlling process receives active messages. Transfer ownership with
`:gen_tcp.controlling_process/2`; queued matching messages and future delivery
move to the new owner in order. If the owner exits, the adapter and its
low-level socket close.

## Packet framing and buffers

Supported packet modes are `:raw`, `:line`, `1`, `2`, and `4`. The integer
modes add or consume an unsigned big-endian length header of that width.
`packet_size` bounds a logical framed packet and defaults to 65,536 bytes.
Oversized framed input returns `:emsgsize` and closes the socket because the
stream can no longer be resynchronized.

Raw streams are not message-oriented: `packet_size` does not cap a raw send or
an exact-length raw receive. Large operations are advanced through bounded
native reads and writes while the adapter retains their progress.

There are two layers of buffer configuration:

- `buffer` is the adapter's Elixir-side receive buffer.
- `recbuf` and `sndbuf` are the native TCP receive and transmit capacities.

`buffer` defaults to 65,536 bytes, and `recbuf` and `sndbuf` to 262,144
bytes (256 KiB), enough for a stream to keep about 20 Mbit/s in flight over a
100 ms round trip. All three accept values up to 1 MiB. Native `recbuf` and
`sndbuf` have a 1 KiB minimum and are fixed when the socket is created;
attempts to change them with `:inet.setopts/2` return `:einval`. They do not
tune themselves as Linux's do: size them for the path, larger for a long fast
one, or smaller to fit more sockets under a stack's buffer cap (see
`SmolNet.start_stack/1`).
Setting `recbuf` at creation also raises `buffer` to at least that size unless
a later option explicitly lowers `buffer`.

`send_timeout` and `send_timeout_close` control a blocked adapter send. One
read and one write may be in progress concurrently; a second operation in the
same direction returns `:busy`.

`nodelay` is as for `:gen_tcp`. It defaults to `false`, which leaves Nagle's
algorithm on: a write shorter than a segment waits while an earlier short one
is unacknowledged, in Minshall's variant, as on Linux. `nodelay: true` turns
it off, at connect or listen or later with `:inet.setopts/2`; turning it off
sends a write it was holding at once. A socket accepted from a listener takes
the listener's `nodelay` as `:gen_tcp.accept/2` returns it.

`keepalive` is as for `:gen_tcp`, with Linux's default timing: a connection
that has received nothing for 2 hours sends a probe, then one every 75 s
while none is answered, and fails with `:etimedout` once 9 have gone
unanswered. Anything the peer sends answers them. It defaults to `false`,
which sends none, so an idle connection whose peer has gone is never
noticed, as on Linux without `SO_KEEPALIVE`. Set it at connect or listen or
later with `:inet.setopts/2`; set later, the 2 hours count from the last
packet received. A socket accepted from a listener takes the listener's
`keepalive` as `nodelay`. The timing is fixed, with no options for
`TCP_KEEPIDLE`, `TCP_KEEPINTVL` or `TCP_KEEPCNT`.

A connection that has data unacknowledged, a FIN unacknowledged, or data
that its peer's zero window holds back, and hears nothing from its peer for
924.6 s, fails with `:etimedout`: RFC 5482's user timeout, fixed at how long
Linux takes to give up by default (`tcp_retries2`). Probing a zero window is
bounded the same way, since a peer that answers the probes keeps the
connection open. A pending `:gen_tcp.recv/3` or `:gen_tcp.send/2` returns
`{:error, :etimedout}`, an active socket's owner receives
`{:tcp_error, socket, :etimedout}` and then `{:tcp_closed, socket}`, and the
socket closes, as for `:econnreset`. An idle connection, with nothing
outstanding, is never timed out. There is no `TCP_USER_TIMEOUT` option.

## TLS with `:ssl`

`:ssl` runs over SmolNet, with either callback module as its transport. Name
it in `:ssl`'s `cb_info` option, and pass the stack, and the family and
address options, alongside the TLS options, as for `:gen_tcp`:

```elixir
cb_info = {SmolNet.Inet.Tcp, :tcp, :tcp_closed, :tcp_error}
```

Use `SmolNet.Inet6.Tcp` and `:inet6` for IPv6. [Using `:ssl`](ssl.md) covers
clients, servers, upgrading a connected socket, certificate verification,
and how `:ssl` reports the errors this guide describes.

`:ssl` uses three calls a plain `:gen_tcp` user rarely needs, and both modules
provide them:

- `port/1`, the socket's local port, as `:inet.port/1` returns it.
- `monitor/1` and `cancel_monitor/1`, which `:inet.monitor/1` and
  `:inet.cancel_monitor/1` call for a SmolNet socket. `:ssl.listen/2` watches
  its listener this way. As for an OTP socket-backed `:gen_tcp` socket, the
  monitor's message is `{:DOWN, ref, :socket, socket, :closed}` when the
  socket closes, or `{:DOWN, ref, :socket, socket, :nosock}` at once if it
  was already closed. Any process may monitor a socket, but only the process
  that set a monitor can cancel it.

A SmolNet socket closes when it is closed, when its owner exits, when a
fatal error or `send_timeout_close` ends it, or when its stack stops. A peer's
FIN alone does not close it, since the socket can still send, so a monitor
does not trigger until one of those.

## Supported options

The inet option surface is deliberately finite:

| Option | At connect/listen | At runtime |
| --- | --- | --- |
| `:smolnet_stack` | required | fixed |
| `:inet` / `:inet6` | supported | fixed |
| `:binary` / `:list` / `:mode` | supported | supported |
| `:active` | `false`, `true`, `:once`, or `1..32767` | supported |
| `:packet` | `:raw`, `:line`, `1`, `2`, or `4` | supported |
| `:packet_size` | `0..1048576` | supported |
| `:header` | `0` only | `0` only |
| `:buffer` | `1..1048576` | supported |
| `:recbuf` / `:sndbuf` | `1024..1048576` | fixed |
| `:nodelay` | `true` or `false` (default) | supported |
| `:keepalive` | `true` or `false` (default) | supported |
| `:send_timeout` / `:send_timeout_close` | supported | supported |
| `:ip` / `:ifaddr` / `:port` | supported | fixed |
| `:backlog` | listen only, `1..128` | fixed |
| `:ipv6_v6only` | IPv6 `true` only | fixed |

Invalid values, options used in the wrong lifecycle phase, and unlisted
options return `:einval`. Selecting the wrong family returns `:eafnosupport`.
Ancillary data, OS file descriptors, raw socket options, and other packet modes
are not supported.

Network and lifecycle errors use the usual OTP-style atoms, including
`:econnrefused`, `:econnreset`, `:etimedout`, `:enetunreach`, `:eaddrinuse`,
`:closed`, and `:enetdown`.

If a socket's stack fails, a call pending on the socket returns
`{:error, :enetdown}`, and an active socket's owner receives
`{:tcp_error, socket, :enetdown}` and then `{:tcp_closed, socket}`. A send
that had already queued part of its data returns `{:error, {:enetdown, rest}}`
with the part it did not queue. A stack fails when it crashes, and when its
link dies under `link_down: :stop`. Calls pending when `SmolNet.stop_stack/1`
stops the stack, or when its supervisor shuts it down, fail with `:closed`.
Under `link_down: :mark_down` or `{:notify, pid}` the stack keeps running
without its link, so pending calls keep waiting until they time out or the
stack is stopped.
