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 returns bytes, suspends while none have arrived, or returns 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.

Submodules

ModuleDescription
nupp.io.net.spiImplementation interface for network transports.

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 a direction of a connection.
ListenerrecordA listening socket.
ListenOptionstypeHow a listener is opened.
MessagerecordWhat a The bytes are not here: they went into the storage the receive named.
StreamrecordA connection.

Functions

FunctionKindDescription
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. A link-local IPv6 address carries the
    --- zone it arrived through, as in `fe80::1%4`, so a reply reaches it; a
    --- zone may also name an interface, as in `fe80::1%lo0`.
    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, as anything
    --- [](nupp.io.files) accepts. Nothing is resolved for one, so a connect to a
    --- path starts at the handshake.
    path: Path?,

    --- 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 ...

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.

Members

NameKindDescription
portmethodThe port this socket actually bound.
isReleasedmethodWhether this socket has been released.
receiveFrommethodTakes the next datagram into destination, suspending until one arrives.
sendTomethodSends a datagram.
setBroadcastmethodAllows sending to a broadcast address.
setMulticastTTLmethodHow many hops a multicast datagram may take.
setMulticastLoopmethodWhether this socket also receives what it sends to a group it joined.
joinMulticastmethodJoins a multicast group.
leaveMulticastmethodLeaves a multicast group.
closemethodReleases the socket.

portmethod#

port: function port(self): integer

The port this socket actually bound.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
integer

isReleasedmethod#

isReleased: function isReleased(self): boolean

Whether this socket has been released.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean

receiveFrommethod#

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

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

io.Error?

why there is none, when unsuccessful

Raises
TypeCondition
string

when maximum is not a positive integer

sendTomethod#

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

Sends a datagram.

Returns 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

io.Error?

why it did not, when unsuccessful

setBroadcastmethod#

setBroadcast: function setBroadcast(self, enable: boolean): boolean, io.Error?

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
io.Error?

setMulticastTTLmethod#

setMulticastTTL: function setMulticastTTL(self, ttl: integer): boolean, io.Error?

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
io.Error?
Raises
TypeCondition
string

when the hop limit is not 1 through 255

setMulticastLoopmethod#

setMulticastLoop: function setMulticastLoop(self, enable: boolean): boolean, io.Error?

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
io.Error?

joinMulticastmethod#

joinMulticast: function joinMulticast(self, group: string, interfaceAddress: string?): boolean, io.Error?

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
io.Error?

leaveMulticastmethod#

leaveMulticast: function leaveMulticast(self, group: string, interfaceAddress: string?): boolean, io.Error?

Leaves a multicast group.

Arguments
NameTypeDescription
selfany
groupstring
interfaceAddressstring?
Returns
TypeDescription
boolean
io.Error?

closemethod#

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 ...

A borrowed view of a 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.

Members

NameKindDescription
readmethod
readSpanmethod
readIntomethod
transferTomethod
writemethod
writeSpanmethod
flushmethodWaits until nothing this view wrote is still held by the platform.
closemethodEnds this direction and leaves the connection alone.

readmethod#

read: function read(exclusive self, count: integer): string?, string?
Arguments
NameTypeDescription
exclusive selfany
countinteger
Returns
TypeDescription
string?
string?
Raises
TypeCondition
string

when count is not a positive integer

readSpanmethod#

readSpan: function readSpan(exclusive self, exclusive destination: span.Writable<uint8>): integer?, string?
Arguments
NameTypeDescription
exclusive selfany
exclusive destinationspan.Writable<uint8>
Returns
TypeDescription
integer?
string?
Raises
TypeCondition
string

when the destination is empty

readIntomethod#

readInto: function readInto(exclusive self, exclusive destination: Buffer, offset: integer?, count: integer?): integer?, string?
Arguments
NameTypeDescription
exclusive selfany
exclusive destinationBuffer
offsetinteger?
countinteger?
Returns
TypeDescription
integer?
string?
Raises
TypeCondition
string

when count is not a positive integer

transferTomethod#

transferTo: function transferTo(exclusive self, exclusive destination: Writer): integer?, string?
Arguments
NameTypeDescription
exclusive selfany
exclusive destinationWriter
Returns
TypeDescription
integer?
string?

writemethod#

write: function write(exclusive self, bytes: string): boolean, string?
Arguments
NameTypeDescription
exclusive selfany
bytesstring
Returns
TypeDescription
boolean
string?

writeSpanmethod#

writeSpan: function writeSpan(exclusive self, borrows source: span.ByteSpan): integer?, string?
Arguments
NameTypeDescription
exclusive selfany
borrows sourcespan.ByteSpan
Returns
TypeDescription
integer?
string?

flushmethod#

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?

closemethod#

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 ...

A listening socket.

Members

NameKindDescription
portmethodThe port this listener actually bound, which is what a caller who asked for zero needs.
isReleasedmethodWhether this listener has been released.
acceptmethodTakes the next connection, suspending until one arrives.
closemethodReleases the listener and every connection it accepted that nobody took.

portmethod#

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

isReleasedmethod#

isReleased: function isReleased(self): boolean

Whether this listener has been released.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean

acceptmethod#

accept: function accept(self): affine(net.Stream)?, io.Error?

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

io.Error?

why there is none, when the listener failed

closemethod#

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` returns which was chosen.
    port: integer?,

    --- A filesystem name to bind instead, for a Unix domain socket, as anything
    --- [](nupp.io.files) accepts.
    ---
    --- 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: Path?,

    --- How many connections the platform may hold before this process accepts
    --- them, or nil for the platform's usual depth. When given, it must be
    --- positive and no greater than 2,147,483,647.
    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 ...

What a

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.

Members

NameKindDescription
lengthfieldHow many bytes landed in the storage offered.
addressfieldThe peer that sent them.
truncatedfieldWhether the datagram was larger than the storage offered, so that what landed is the beginning of a message rather...

lengthfield#

length: integer

How many bytes landed in the storage offered.

addressfield#

address: net.Address

The peer that sent them.

truncatedfield#

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 ...

A 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.

Members

NameKindDescription
isReleasedmethodWhether this side has released the connection.
isEndedmethodWhether the peer closed its sending half and nothing is left buffered.
pendingmethodHow many bytes this process still holds unsent.
peerAddressmethodWho is at the other end, or nil for a connection that has no peer address.
localAddressmethodWhich of this machine's addresses the connection is on, or nil where there is none.
setNoDelaymethodTurns Nagle's algorithm off, so a small write goes out now.
setKeepAlivemethodAsks the kernel to probe an idle connection.
readmethodReads up to count bytes, suspending while none have arrived.
readSpanmethodReads directly into a checked writable span.
readIntomethodReads into a buffer.
transferTomethodWrites everything left to a writer.
writemethodAppends bytes, waiting where the send queue is already at its maximum.
writeSpanmethodAppends bytes from a checked shared span.
flushmethodWaits until nothing this connection accepted is still held here.
shutdownWritemethodEnds the sending half and leaves the receiving half open, so the peer sees end of stream while this side goes on...
asReadermethodBorrows this connection's reading half through the shared contract.
asWritermethodBorrows this connection's writing half through the shared contract.
closemethodReleases the connection in both directions.

isReleasedmethod#

isReleased: function isReleased(self): boolean

Whether this side has released the connection.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean

isEndedmethod#

isEnded: function isEnded(self): boolean

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

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

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
boolean

pendingmethod#

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

peerAddressmethod#

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 returns nil rather than something that looks like one.

Arguments
NameTypeDescription
selfany
Returns
TypeDescription
net.Address?

localAddressmethod#

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?

setNoDelaymethod#

setNoDelay: function setNoDelay(self, enable: boolean): boolean, io.Error?

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

io.Error?

why it was not, when unsuccessful

setKeepAlivemethod#

setKeepAlive: function setKeepAlive(self, enable: boolean, delayMs: integer): boolean, io.Error?

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. The platform may round it to its own granularity, which is whole seconds on most.

Arguments
NameTypeDescription
selfany

this connection

enableboolean

whether to probe

delayMsinteger

idle milliseconds before the first probe

Returns
TypeDescription
boolean

whether the option was set

io.Error?

why it was not, when unsuccessful

Raises
TypeCondition
string

when the delay is not a positive integer no greater than 4,294,967,295

readmethod#

read: function read(exclusive 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
exclusive 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
TypeCondition
string

when count is not a positive integer

readSpanmethod#

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

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

Arguments
NameTypeDescription
exclusive 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
TypeCondition
string

when the destination is empty

readIntomethod#

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

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

Arguments
NameTypeDescription
exclusive 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
TypeCondition
string

when count is not a positive integer

transferTomethod#

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

Writes everything left to a writer.

Arguments
NameTypeDescription
exclusive 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

writemethod#

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

writeSpanmethod#

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

flushmethod#

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

shutdownWritemethod#

shutdownWrite: function shutdownWrite(self): boolean, io.Error?

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

io.Error?

why it was not, when unsuccessful

asReadermethod#

asReader: function asReader(borrows self): affine(Reader)

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

Closing the view stops this side reading and leaves the connection open.

Arguments
NameTypeDescription
borrows selfany

the connection to view

Returns
TypeDescription
affine(Reader)

the view, which may not outlive it

asWritermethod#

asWriter: function asWriter(borrows self): affine(Writer)

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

Closing the view half-closes the connection, so the peer sees the end of the stream, and leaves the connection open for reading.

Arguments
NameTypeDescription
borrows selfany

the connection to view

Returns
TypeDescription
affine(Writer)

the view, which may not outlive it

closemethod#

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.bindfunction#

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

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

io.Error?

why it could not bind, when unsuccessful

Raises

TypeCondition
string

when the options do not describe a bindable address

net.connectfunction#

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

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

io.Error?

why it could not connect, when unsuccessful

Raises

TypeCondition
string

when the options do not describe a reachable peer

net.listenfunction#

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

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

io.Error?

why it could not bind, when unsuccessful

Raises

TypeCondition
string

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

Raises

TypeCondition
string

when timeoutMs is not a non-negative integer