nupp.io.net

nupp.io.net is the network as bytes: a listener, the connections it accepts, and connections this process opens itself.

local listener = assert(nupp.io.net.listen({host = "127.0.0.1", port = 0}))
local stream = assert(listener:accept())
print(assert(stream:read(5)))
stream:close()
listener:close()

A connection reads and writes through the Reader and Writer contracts on nupp.io, so a parser written against byte I/O reads a socket without knowing it has one. A read answers bytes, suspends while none have arrived, or answers empty because the peer closed its sending half; empty is only ever the end.

A listener and a connection are owners. See ownership.md for the contract they are handed out under.

Module contents

Types

TypeKindDescription
AddresstypeWhere a datagram came from, and where one may be sent.
ConnectOptionstypeHow a connection is opened.
DatagramrecordA datagram socket.
DatagramOptionstypeHow a datagram socket is opened.
DirectionViewrecordA borrowed view of one direction of a connection.
ListenerrecordA listening socket.
ListenOptionstypeHow a listener is opened.
MessagerecordWhat one received datagram was.
StreamrecordOne connection.

Functions

FunctionKindDescription
asReaderfunctionBorrows a connection's reading half through the shared contract.
asWriterfunctionBorrows a connection's writing half through the shared contract.
bindfunctionOpens a datagram socket.
connectfunctionOpens a connection, resolving the peer's name off this frame.
listenfunctionOpens a listener.
pumpfunctionAdvances the selected network reactor.

Types#

Addresstype#

type Address = {
    --- The peer's address, as a literal.
    host: string,

    --- The peer's port.
    port: integer
}

Where a datagram came from, and where one may be sent.

ConnectOptionstype#

type net.ConnectOptions = {
    --- The peer to reach, as a name or an address literal. A name is resolved
    --- off this frame. Given with `port`, and not with `path`.
    host: string?,

    --- The peer's port.
    port: integer?,

    --- A filesystem name to reach instead, for a Unix domain socket. Nothing is
    --- resolved for one, so a connect to a path starts at the handshake.
    path: string?,

    --- The most bytes this connection may hold unsent before `write` waits.
    --- Defaults to one mebibyte.
    sendHighWater: integer?,

    --- How long the connect may take, resolution included, before it gives up.
    --- Defaults to thirty seconds.
    ---
    --- There is no way to ask for no deadline at all. A connect that waits
    --- forever is a hang somebody eventually reports as a bug, and a peer that
    --- accepts a packet and then says nothing is a normal thing on a network
    --- rather than an unusual one.
    timeoutMs: integer?
}

How a connection is opened.

Datagramrecord#

record net.Datagram is nupp.Closeable
    function port(self): integer end

    function isReleased(self): boolean end

    function receiveFrom(self, exclusive destination: Buffer, maximum: integer): (net.Message?, string?) end

    function sendTo(self, address: net.Address, bytes: string): (boolean, string?) end

    function setBroadcast(self, enable: boolean): (boolean, string?) end

    function setMulticastTTL(self, ttl: integer): (boolean, string?) end

    function setMulticastLoop(self, enable: boolean): (boolean, string?) end

    function joinMulticast(self, group: string, interfaceAddress: string?): (boolean, string?) end

    function leaveMulticast(self, group: string, interfaceAddress: string?): (boolean, string?) end

    function close(takes self): nil end
end

A datagram socket.

A message transport rather than a stream one: nothing here buffers arrivals into a run, because the boundaries between them are the protocol.

Methods

port#
port: function port(self): integer

The port this socket actually bound.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
integer
isReleased#
isReleased: function isReleased(self): boolean

Whether this socket has been released.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean
receiveFrom#
receiveFrom: function receiveFrom(self, exclusive destination: Buffer, maximum: integer): net.Message?, string?

Takes the next datagram into destination, suspending until one arrives.

A zero-length answer is an empty datagram and not an absence: several protocols use them as keepalives, and a receive that could not tell them from a quiet socket would make a live peer look like silence.

Arguments
NameTypeDescription
selfany

this socket

exclusive destinationBuffer

where the bytes go

maximuminteger

the most bytes to take from one datagram

Returns
TypeDescription
net.Message?

what the message was, or nil when the socket failed

string?

why there is none, when unsuccessful

Raises
  • when maximum is not a positive integer

sendTo#
sendTo: function sendTo(self, address: net.Address, bytes: string): boolean, string?

Sends one datagram.

Answers whether the platform took it, which is not a delivery receipt and never could be: a datagram that goes is a datagram that may still not arrive, and a protocol that needs to know says so in the protocol.

Arguments
NameTypeDescription
selfany

this socket

addressnet.Address

where to send it

bytesstring

what to send

Returns
TypeDescription
boolean

whether the platform took it

string?

why it did not, when unsuccessful

setBroadcast#
setBroadcast: function setBroadcast(self, enable: boolean): boolean, string?

Allows sending to a broadcast address.

Refused by default by the platform, so a program that means to broadcast has to say so rather than discover it works.

Arguments
NameTypeDescription
selfany
enableboolean
Returns
TypeDescription
boolean
string?
setMulticastTTL#
setMulticastTTL: function setMulticastTTL(self, ttl: integer): boolean, string?

How many hops a multicast datagram may take.

One keeps it on the local segment, which is what discovery on a LAN wants and what the platform starts at.

Arguments
NameTypeDescription
selfany
ttlinteger
Returns
TypeDescription
boolean
string?
Raises
  • when the hop limit is not 1 through 255

setMulticastLoop#
setMulticastLoop: function setMulticastLoop(self, enable: boolean): boolean, string?

Whether this socket also receives what it sends to a group it joined.

Off is usually what is wanted: a discovery protocol that answers its own announcements is the first bug somebody writes with one.

Arguments
NameTypeDescription
selfany
enableboolean
Returns
TypeDescription
boolean
string?
joinMulticast#
joinMulticast: function joinMulticast(self, group: string, interfaceAddress: string?): boolean, string?

Joins a multicast group.

interfaceAddress names which of this machine's addresses to join on. On a machine with more than one, the platform's own choice is rarely the one wanted, which is why it is asked for rather than assumed.

Arguments
NameTypeDescription
selfany
groupstring
interfaceAddressstring?
Returns
TypeDescription
boolean
string?
leaveMulticast#
leaveMulticast: function leaveMulticast(self, group: string, interfaceAddress: string?): boolean, string?

Leaves a multicast group.

Arguments
NameTypeDescription
selfany
groupstring
interfaceAddressstring?
Returns
TypeDescription
boolean
string?
close#
close: function close(takes self): nil

Releases the socket. Idempotent, and terminal.

Arguments
NameTypeDescription
takes selfany
Returns
TypeDescription
nil

DatagramOptionstype#

type net.DatagramOptions = {
    --- The address to bind, as an IPv4 or IPv6 literal.
    host: string,

    --- The port to bind, or zero to let the platform choose one.
    port: integer,

    --- Whether to ask for load-balancing port reuse, so that a socket per lane
    --- shares one port. Refused where the platform's semantics are not load
    --- balancing rather than quietly degraded.
    reusePort: boolean?
}

How a datagram socket is opened.

DirectionViewrecord#

record net.DirectionView is Reader, Writer
    function read(self, count: integer): (string?, string?) end

    function readSpan(self, exclusive destination: span.Writable<uint8>): (integer?, string?) end

    function readInto(self, exclusive destination: Buffer, offset: integer?, count: integer?): (integer?, string?) end

    function transferTo(self, exclusive destination: Writer): (integer?, string?) end

    function write(exclusive self, bytes: string): (boolean, string?) end

    function writeSpan(exclusive self, borrows source: span.ByteSpan): (integer?, string?) end

    function flush(self): (boolean, string?) end

    function close(takes self): nil end
end

A borrowed view of one direction of a connection.

Closing one ends that direction rather than the connection: the writing view half-closes, and the reading view stops this side reading. Neither is the cleanup obligation, which stays with the connection.

Methods

read#
read: function read(self, count: integer): string?, string?
Arguments
NameTypeDescription
selfany
countinteger
Returns
TypeDescription
string?
string?
Raises
  • when count is not a positive integer

readSpan#
readSpan: function readSpan(self, exclusive destination: span.Writable<uint8>): integer?, string?
Arguments
NameTypeDescription
selfany
exclusive destinationspan.Writable<uint8>
Returns
TypeDescription
integer?
string?
Raises
  • when the destination is empty

readInto#
readInto: function readInto(self, exclusive destination: Buffer, offset: integer?, count: integer?): integer?, string?
Arguments
NameTypeDescription
selfany
exclusive destinationBuffer
offsetinteger?
countinteger?
Returns
TypeDescription
integer?
string?
Raises
  • when count is not a positive integer

transferTo#
transferTo: function transferTo(self, exclusive destination: Writer): integer?, string?
Arguments
NameTypeDescription
selfany
exclusive destinationWriter
Returns
TypeDescription
integer?
string?
write#
write: function write(exclusive self, bytes: string): boolean, string?
Arguments
NameTypeDescription
exclusive selfany
bytesstring
Returns
TypeDescription
boolean
string?
writeSpan#
writeSpan: function writeSpan(exclusive self, borrows source: span.ByteSpan): integer?, string?
Arguments
NameTypeDescription
exclusive selfany
borrows sourcespan.ByteSpan
Returns
TypeDescription
integer?
string?
flush#
flush: function flush(self): boolean, string?

Waits until nothing this view wrote is still held by the platform.

The same queue the connection has, because it is the same connection.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean
string?
close#
close: function close(takes self): nil

Ends this direction and leaves the connection alone.

The writing view half-closes, so the peer sees end of stream. The reading view stops this side reading, which is deliberately not the platform's receive shutdown: its behaviour varies enough between stacks to be worth not depending on.

Arguments
NameTypeDescription
takes selfany
Returns
TypeDescription
nil

Listenerrecord#

record net.Listener is nupp.Closeable
    function port(self): integer end

    function isReleased(self): boolean end

    function accept(self): (affine(net.Stream)?, string?) end

    function close(takes self): nil end
end

A listening socket.

Methods

port#
port: function port(self): integer

The port this listener actually bound, which is what a caller who asked for zero needs.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
integer
isReleased#
isReleased: function isReleased(self): boolean

Whether this listener has been released.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean
accept#
accept: function accept(self): affine(net.Stream)?, string?

Takes the next connection, suspending until one arrives.

Arguments
NameTypeDescription
selfany

this listener

Returns
TypeDescription
affine(net.Stream)?

the connection, which the caller owns and must close

string?

why there is none, when the listener failed

close#
close: function close(takes self): nil

Releases the listener and every connection it accepted that nobody took.

Idempotent, and terminal.

Arguments
NameTypeDescription
takes selfany
Returns
TypeDescription
nil

ListenOptionstype#

type net.ListenOptions = {
    --- The address to bind, as an IPv4 or IPv6 literal. A name is not resolved
    --- here, because a listener binds an interface rather than reaching a peer.
    ---
    --- Given with `port`, and not with `path`: a listener binds an address or a
    --- filesystem name, and naming both says two different things at once.
    host: string?,

    --- The port to bind, or zero to let the platform choose one. `Listener.port`
    --- answers which was chosen.
    port: integer?,

    --- A filesystem name to bind instead, for a Unix domain socket.
    ---
    --- The name is not removed when the listener closes. A caller that binds a
    --- path owns that path, and unlinking a file this process may not have
    --- created is not a transport's decision to make.
    path: string?,

    --- How many connections the platform may hold before this process accepts
    --- them, or nil for the platform's usual depth.
    backlog: integer?,

    --- Whether to ask for load-balancing port reuse, so that a listener per lane
    --- shares one port. Refused where the platform's semantics are not load
    --- balancing rather than quietly degraded, so a program depending on it
    --- learns at bind.
    reusePort: boolean?
}

How a listener is opened.

Messagerecord#

record net.Message
    length: integer
    address: net.Address
    truncated: boolean
end

What one received datagram was.

The bytes are not here: they went into the storage the receive named. What is here is everything a caller cannot get from those bytes -- how many of them landed, who sent them, and whether the rest is gone.

Fields

length#
length: integer

How many bytes landed in the storage offered.

address#
address: net.Address

The peer that sent them.

truncated#
truncated: boolean

Whether the datagram was larger than the storage offered, so that what landed is the beginning of a message rather than a message.

Never silently false: a protocol that parses the first 1500 bytes of a larger datagram without being told is parsing something nobody sent, and that is a security bug rather than a lost packet.

Streamrecord#

record net.Stream is Reader, Writer
    function isReleased(self): boolean end

    function isEnded(self): boolean end

    function pending(self): integer end

    function peerAddress(self): net.Address? end

    function localAddress(self): net.Address? end

    function setNoDelay(self, enable: boolean): (boolean, string?) end

    function setKeepAlive(self, enable: boolean, delaySeconds: integer): (boolean, string?) end

    function read(self, count: integer): (string?, string?) end

    function readSpan(self, exclusive destination: span.Writable<uint8>): (integer?, string?) end

    function readInto(self, exclusive destination: Buffer, offset: integer?, count: integer?): (integer?, string?) end

    function transferTo(self, exclusive destination: Writer): (integer?, string?) end

    function write(exclusive self, bytes: string): (boolean, string?) end

    function writeSpan(exclusive self, borrows source: span.ByteSpan): (integer?, string?) end

    function flush(self): (boolean, string?) end

    function shutdownWrite(self): (boolean, string?) end

    function close(takes self): nil end
end

One connection.

The owner of the socket, and the only cleanup obligation: a direction view borrows this and ends a direction, while close here ends the connection.

Methods

isReleased#
isReleased: function isReleased(self): boolean

Whether this side has released the connection.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean
isEnded#
isEnded: function isEnded(self): boolean

Whether the peer closed its sending half and nothing is left buffered.

A read answers empty exactly when this is true, which is what keeps a quiet connection and a finished one apart.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean
pending#
pending: function pending(self): integer

How many bytes this process still holds unsent.

A local fact and not a delivery receipt: bytes leave this count when the platform takes them, which says nothing about the peer having read them.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
integer
peerAddress#
peerAddress: function peerAddress(self): net.Address?

Who is at the other end, or nil for a connection that has no peer address.

A Unix socket has a filesystem name and no peer address, and answers nil rather than something that looks like one.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
net.Address?
localAddress#
localAddress: function localAddress(self): net.Address?

Which of this machine's addresses the connection is on, or nil where there is none.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
net.Address?
setNoDelay#
setNoDelay: function setNoDelay(self, enable: boolean): boolean, string?

Turns Nagle's algorithm off, so a small write goes out now.

The kernel coalesces small writes by default, which can hold a reply for tens of milliseconds waiting for more to send. That is the right trade for bulk transfer and the wrong one for a protocol that answers requests, so a server usually wants this on every connection it accepts.

Arguments
NameTypeDescription
selfany

this connection

enableboolean

whether to send small writes immediately

Returns
TypeDescription
boolean

whether the option was set

string?

why it was not, when unsuccessful

setKeepAlive#
setKeepAlive: function setKeepAlive(self, enable: boolean, delaySeconds: integer): boolean, string?

Asks the kernel to probe an idle connection.

A peer that vanished without closing leaves a connection that looks open forever. Probing is what eventually notices, and the delay is how long the connection may be idle before the first probe.

Arguments
NameTypeDescription
selfany

this connection

enableboolean

whether to probe

delaySecondsinteger

idle seconds before the first probe

Returns
TypeDescription
boolean

whether the option was set

string?

why it was not, when unsuccessful

Raises
  • when the delay is not a positive integer

read#
read: function read(self, count: integer): string?, string?

Reads up to count bytes, suspending while none have arrived.

An empty answer is the end, as the contract says, and it is only ever the end: a quiet connection parks rather than answering zero.

Arguments
NameTypeDescription
selfany

this connection

countinteger

the most bytes to read

Returns
TypeDescription
string?

the bytes, or nil when the connection is closed

string?

why it could not read, when unsuccessful

Raises
  • when count is not a positive integer

readSpan#
readSpan: function readSpan(self, exclusive destination: span.Writable<uint8>): integer?, string?

Reads directly into a checked writable span. A zero answer is the end.

Arguments
NameTypeDescription
selfany

this connection

exclusive destinationspan.Writable<uint8>

the positive-sized range to fill

Returns
TypeDescription
integer?

how many bytes were read, or nil when the connection is closed

string?

why it could not read, when unsuccessful

Raises
  • when the destination is empty

readInto#
readInto: function readInto(self, exclusive destination: Buffer, offset: integer?, count: integer?): integer?, string?

Reads into a buffer. A zero answer is the end.

Arguments
NameTypeDescription
selfany

this connection

exclusive destinationBuffer

the buffer to write into

offsetinteger?

where in the destination to start, or the beginning

countinteger?

the most bytes to read

Returns
TypeDescription
integer?

how many bytes were read, or nil when the connection is closed

string?

why it could not read, when unsuccessful

Raises
  • when count is not a positive integer

transferTo#
transferTo: function transferTo(self, exclusive destination: Writer): integer?, string?

Writes everything left to a writer.

Arguments
NameTypeDescription
selfany

this connection

exclusive destinationWriter

the writer to fill

Returns
TypeDescription
integer?

how many bytes moved, or nil on failure

string?

why it could not, when unsuccessful

write#
write: function write(exclusive self, bytes: string): boolean, string?

Appends bytes, waiting where the send queue is already at its maximum.

The whole value is written, as the contract promises. An input larger than sendHighWater is submitted in pieces that fit beneath it, so the bound governs how much is queued at one time rather than how much may be sent.

Arguments
NameTypeDescription
exclusive selfany

this connection

bytesstring

the bytes to append

Returns
TypeDescription
boolean

whether they were written

string?

why they were not, when unsuccessful

writeSpan#
writeSpan: function writeSpan(exclusive self, borrows source: span.ByteSpan): integer?, string?

Appends bytes from a checked shared span.

Arguments
NameTypeDescription
exclusive selfany

this connection

borrows sourcespan.ByteSpan

bytes valid for the duration of the call

Returns
TypeDescription
integer?

how many bytes moved, or nil on failure

string?

why it could not, when unsuccessful

flush#
flush: function flush(self): boolean, string?

Waits until nothing this connection accepted is still held here.

Unlike a buffer's writer there is something to flush: write completes when the platform takes the bytes, and the platform may still be holding them. A caller that writes, flushes and closes would otherwise lose whatever was queued, because closing cancels queued writes.

Still not a delivery receipt. It says the bytes left this process, not that anybody read them.

Arguments
NameTypeDescription
selfany

this connection

Returns
TypeDescription
boolean

whether everything left

string?

why it did not, when unsuccessful

shutdownWrite#
shutdownWrite: function shutdownWrite(self): boolean, string?

Ends the sending half and leaves the receiving half open, so the peer sees end of stream while this side goes on reading.

This is what a graceful drain is built from. close ends the connection in both directions and cannot wait for a peer, so a handler with a response still to deliver finishes it, half-closes, and then releases.

Arguments
NameTypeDescription
selfany

this connection

Returns
TypeDescription
boolean

whether the sending half was ended

string?

why it was not, when unsuccessful

close#
close: function close(takes self): nil

Releases the connection in both directions.

Idempotent, and terminal: this is the cleanup obligation the owner carries.

Arguments
NameTypeDescription
takes selfany
Returns
TypeDescription
nil

Functions#

net.asReaderfunction#

function net.asReader(borrows source: net.Stream): affine(net.DirectionView)

Borrows a connection's reading half through the shared contract.

Arguments

NameTypeDescription
borrows sourcenet.Stream

the connection to view

Returns

TypeDescription
affine(net.DirectionView)

the view, which may not outlive it

net.asWriterfunction#

function net.asWriter(borrows source: net.Stream): affine(net.DirectionView)

Borrows a connection's writing half through the shared contract.

Arguments

NameTypeDescription
borrows sourcenet.Stream

the connection to view

Returns

TypeDescription
affine(net.DirectionView)

the view, which may not outlive it

net.bindfunction#

function net.bind(options: net.DatagramOptions): affine(net.Datagram)?, string?

Opens a datagram socket.

Arguments

NameTypeDescription
optionsnet.DatagramOptions

where to bind

Returns

TypeDescription
affine(net.Datagram)?

the socket, which the caller owns and must close

string?

why it could not bind, when unsuccessful

Raises

  • when the options do not describe a bindable address

net.connectfunction#

function net.connect(options: net.ConnectOptions): affine(net.Stream)?, string?

Opens a connection, resolving the peer's name off this frame.

Arguments

NameTypeDescription
optionsnet.ConnectOptions

who to reach and how much may be queued unsent

Returns

TypeDescription
affine(net.Stream)?

the connection, which the caller owns and must close

string?

why it could not connect, when unsuccessful

Raises

  • when the options do not describe a reachable peer

net.listenfunction#

function net.listen(options: net.ListenOptions): affine(net.Listener)?, string?

Opens a listener.

Arguments

NameTypeDescription
optionsnet.ListenOptions

where to bind and how deep a backlog to ask for

Returns

TypeDescription
affine(net.Listener)?

the listener, which the caller owns and must close

string?

why it could not bind, when unsuccessful

Raises

  • when the options do not describe a bindable address

net.pumpfunction#

function net.pump(timeoutMs: integer): nil

Advances the selected network reactor.

Arguments

NameTypeDescription
timeoutMsinteger

Returns

TypeDescription
nil