minato_socket (minato v1.0.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.

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(), [term()], 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. binary, {mode, binary}, {active, false}, {packet, raw} and {buffer, _} are what this module always does or has no equivalent of, and are accepted and ignored. {nodelay, _}, {keepalive, _}, {recbuf, _} and {sndbuf, _} are set on the socket. 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.

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.

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.