minato_socket (minato v1.1.1)

View Source

The socket module behind the same calls gen_tcp answers.

The default transport. minato_conn speaks to its transport through connect/4, send/2, recv/3, close/1, controlling_process/2 and setopts/2, and this module answers all six on top of the socket NIFs so that the transport can be chosen per connection with nothing else changing. transport => inet goes back to gen_tcp.

Why

A send and a read through the inet driver cost about 323 reductions a round trip: the driver's own layer monitors the port, serialises a reference, hands the bytes over and waits for a reply message. The same round trip through socket costs about 97, because a NIF call is a function call.

The other half is the pool. A driver port has a connected process and only that process may use it, so lending a connection out and taking it back is two controlling_process/2 calls, which cost about 389 reductions a query - more than the query itself. A socket handle has an owner too, but only for how long it lives; any process holding the handle can send and receive on it. Moving that ownership is a NIF call rather than a port reassignment, and a pool that decided a borrower's death should cost a reset rather than a connection could stop moving it at all.

Waiting for bytes without reading

socket has no active mode, so setopts/2 refuses {active, once} rather than pretending. What it has instead is activate/1: a read that either hands back what is already here or arranges one message for when there is something. That is the same arrangement {active, once} is - exactly one message, and the owner asks again - and it costs no process, because the message goes to whoever made the call rather than to a reader spawned to forward it.

Windows completes reads rather than selecting on them: the wait answers with the bytes in the message rather than with permission to go and read them. Both shapes are here and waiting/0 is either, because the difference belongs to whoever reads the message, not to whoever waits.

TLS

ssl:connect/3 takes one of these handles, so a connection that asks for TLS sends its SSLRequest through this module and hands the upgraded socket to ssl exactly as the driver transport does. What comes back is an ordinary ssl:sslsocket/0, so nothing below TLS is this module's business afterwards - including the certificate hash SCRAM binds to.

Summary

Types

A socket handle.

What activate/1 is waiting on, which is what cancel/2 drops.

Functions

Take the bytes that are here, or arrange for a message when there are some.

Drop a waiting arranged by activate/1.

Close the socket.

Hand the socket's lifetime to another process.

Read Length bytes, or whatever has arrived when Length is 0.

Send every byte, or say why not.

Send every byte, or say why not, within Timeout.

Set what can be set.

Types

socket()

-type socket() :: socket:socket().

A socket handle.

Unlike a port it is not owned for the purpose of using it: any process holding one can read and write. It is owned for the purpose of living, and is closed when that process terminates.

waiting()

-type waiting() :: socket:select_info() | socket:completion_info().

What activate/1 is waiting on, which is what cancel/2 drops.

A select token where the machine says the socket is readable, a completion token where it hands over the bytes.

Functions

activate(Socket)

-spec activate(socket()) -> {ok, binary()} | {waiting, waiting()} | {error, term()}.

Take the bytes that are here, or arrange for a message when there are some.

{waiting, Waiting} means one message will be sent to the caller, and which message depends on how the machine waits:

  • {'$socket', Socket, select, Handle} says the socket is readable and the bytes are still in it, so reading is this call again. A wakeup with nothing behind it therefore re-arms rather than being mistaken for data, which is why there is one function here and not an arm and a take.
  • {'$socket', Socket, completion, {Handle, {ok, Data}}} carries the bytes, and calling again would ask for the next ones.

The Handle is the one inside Waiting either way. The waiting is registered for the calling process, so whoever wants the message has to make the call.

cancel(Socket, Waiting)

-spec cancel(socket(), waiting()) -> ok | pending.

Drop a waiting arranged by activate/1.

pending means it was too late and the message is on its way, which matters where the message carries the bytes: dropping it there would drop them. ok means nothing is coming.

Bytes still in the socket are not lost either way. The next recv/3 reads them.

close(Socket)

-spec close(socket()) -> ok.

Close the socket.

connect(Host, Port, Opts, Timeout)

-spec connect(inet:socket_address() | inet:hostname(),
              inet:port_number(),
              [gen_tcp:connect_option()],
              timeout()) ->
                 {ok, socket()} | {error, term()}.

Open a connection.

Takes the option list gen_tcp would take, so a caller does not have to know which transport it got.

Three kinds of option, and the difference is what the option decides.

Read before connecting, because that is the only time they can be honoured: inet and inet6 choose which address a hostname resolves to; {ip, _} and {port, _} choose the local end to bind, which is how a multi homed host picks the interface the server sees.

Set on the socket: {nodelay, _}, {keepalive, _}, {recbuf, _}, {sndbuf, _}, {priority, _}, {reuseaddr, _}, {tos, _}, {bind_to_device, _} and {linger, _}. {send_timeout, _} and {send_timeout_close, _} belong here in spirit but are read by minato_conn and applied to send/3, because there is no driver underneath to hold them.

Accepted and ignored, because this module already does them or the thing they configure does not exist here: binary, {mode, binary}, {active, false}, {packet, raw}, {buffer, _}, {delay_send, _}, {high_watermark, _}, {low_watermark, _}, {exit_on_close, _} and {show_econnreset, _}. The last five configure a port's send queue and a port's reporting, and there is no port here.

Anything else is refused rather than silently dropped: this is the default transport, and an option that did something through the driver must not quietly stop doing it here.

The lists above are what callers pass, not what a socket happens to be settable to. Getting that backwards is how inet came to be refused, which broke every deployment that names its address family - so when a real caller passes something missing, the answer is to add it here rather than to tell them to stop.

controlling_process(Socket, Pid)

-spec controlling_process(socket(), pid()) -> ok | {error, term()}.

Hand the socket's lifetime to another process.

A socket handle has an owner, but only for how long it lives: it is closed when that process terminates. Reading and writing need no ownership at all, which a port does, so lending a connection out could stop moving ownership entirely and let the lender keep it - a borrower that dies would then cost a connection reset rather than a connection. That belongs to whoever lends, so this answers the call gen_tcp answers, and answers it with one NIF rather than a port reassignment.

recv(Socket, Length, Timeout)

-spec recv(socket(), non_neg_integer(), timeout()) -> {ok, binary()} | {error, term()}.

Read Length bytes, or whatever has arrived when Length is 0.

A timeout of 0 is the non-blocking probe minato_conn:usable/1 makes, and answers {error, timeout} when nothing is waiting.

send(Socket, Data)

-spec send(socket(), iodata()) -> ok | {error, term()}.

Send every byte, or say why not.

A send with no deadline blocks until the whole of it has gone, but the call can still answer with what is left rather than with ok, so what is left is sent.

send/3

-spec send(socket(), iodata(), timeout()) -> ok | {error, term()}.

Send every byte, or say why not, within Timeout.

The deadline is for the whole of it rather than for each attempt, because a peer that accepts one byte per attempt would otherwise never time out. What is left when the deadline passes is not sent and not retried: a Query the server has half of is a connection that has to be given up on, which is what send_timeout is for.

setopts/2

-spec setopts(socket(), [term()]) -> ok | {error, term()}.

Set what can be set.

{active, _} is refused; see the module note. So is an option this module has no answer for, because the alternative is a connection that was configured and silently was not.