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
| Type | Kind | Description |
|---|---|---|
Address | type | Where a datagram came from, and where one may be sent. |
ConnectOptions | type | How a connection is opened. |
Datagram | record | A datagram socket. |
DatagramOptions | type | How a datagram socket is opened. |
DirectionView | record | A borrowed view of one direction of a connection. |
Listener | record | A listening socket. |
ListenOptions | type | How a listener is opened. |
Message | record | What one received datagram was. |
Stream | record | One connection. |
Functions
| Function | Kind | Description |
|---|---|---|
asReader | function | Borrows a connection's reading half through the shared contract. |
asWriter | function | Borrows a connection's writing half through the shared contract. |
bind | function | Opens a datagram socket. |
connect | function | Opens a connection, resolving the peer's name off this frame. |
listen | function | Opens a listener. |
pump | function | Advances 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
endA 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): integerThe port this socket actually bound.
Arguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
integer |
isReleased#
isReleased: function isReleased(self): booleanWhether this socket has been released.
Arguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
self | any | this socket |
exclusive destination | Buffer | where the bytes go |
maximum | integer | the most bytes to take from one datagram |
Returns
| Type | Description |
|---|---|
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#
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
| Name | Type | Description |
|---|---|---|
self | any | this socket |
address | net.Address | where to send it |
bytes | string | what to send |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
self | any | |
enable | boolean |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
self | any | |
ttl | integer |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
self | any | |
enable | boolean |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
self | any | |
group | string | |
interfaceAddress | string? |
Returns
| Type | Description |
|---|---|
boolean | |
string? |
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
endA 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#
Arguments
| Name | Type | Description |
|---|---|---|
self | any | |
count | integer |
Returns
| Type | Description |
|---|---|
string? | |
string? |
Raises
when count is not a positive integer
readSpan#
readSpan: function readSpan(self, exclusive destination: span.Writable<uint8>): integer?, string?Arguments
| Name | Type | Description |
|---|---|---|
self | any | |
exclusive destination | span.Writable<uint8> |
Returns
| Type | Description |
|---|---|
integer? | |
string? |
Raises
when the destination is empty
readInto#
readInto: function readInto(self, exclusive destination: Buffer, offset: integer?, count: integer?): integer?, string?Arguments
| Name | Type | Description |
|---|---|---|
self | any | |
exclusive destination | Buffer | |
offset | integer? | |
count | integer? |
Returns
| Type | Description |
|---|---|
integer? | |
string? |
Raises
when count is not a positive integer
transferTo#
transferTo: function transferTo(self, exclusive destination: Writer): integer?, string?Arguments
| Name | Type | Description |
|---|---|---|
self | any | |
exclusive destination | Writer |
Returns
| Type | Description |
|---|---|
integer? | |
string? |
write#
Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | any | |
bytes | string |
Returns
| Type | Description |
|---|---|
boolean | |
string? |
writeSpan#
writeSpan: function writeSpan(exclusive self, borrows source: span.ByteSpan): integer?, string?Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | any | |
borrows source | span.ByteSpan |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
boolean | |
string? |
close#
close: function close(takes self): nilEnds 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
| Name | Type | Description |
|---|---|---|
takes self | any |
Returns
| Type | Description |
|---|---|
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
endA listening socket.
Methods
port#
port: function port(self): integerThe port this listener actually bound, which is what a caller who asked for zero needs.
Arguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
integer |
isReleased#
isReleased: function isReleased(self): booleanWhether this listener has been released.
Arguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
boolean |
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#
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
truncated#
truncated: booleanWhether 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
endOne 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): booleanWhether this side has released the connection.
Arguments
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
boolean |
isEnded#
isEnded: function isEnded(self): booleanWhether 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
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
boolean |
pending#
pending: function pending(self): integerHow 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
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
self | any |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
self | any | this connection |
enable | boolean | whether to send small writes immediately |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
self | any | this connection |
enable | boolean | whether to probe |
delaySeconds | integer | idle seconds before the first probe |
Returns
| Type | Description |
|---|---|
boolean | whether the option was set |
string? | why it was not, when unsuccessful |
Raises
when the delay is not a positive integer
read#
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
| Name | Type | Description |
|---|---|---|
self | any | this connection |
count | integer | the most bytes to read |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
self | any | this connection |
exclusive destination | span.Writable<uint8> | the positive-sized range to fill |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
self | any | this connection |
exclusive destination | Buffer | the buffer to write into |
offset | integer? | where in the destination to start, or the beginning |
count | integer? | the most bytes to read |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
self | any | this connection |
exclusive destination | Writer | the writer to fill |
Returns
| Type | Description |
|---|---|
integer? | how many bytes moved, or nil on failure |
string? | why it could not, when unsuccessful |
write#
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
| Name | Type | Description |
|---|---|---|
exclusive self | any | this connection |
bytes | string | the bytes to append |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
exclusive self | any | this connection |
borrows source | span.ByteSpan | bytes valid for the duration of the call |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
self | any | this connection |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
self | any | this connection |
Returns
| Type | Description |
|---|---|
boolean | whether the sending half was ended |
string? | why it was not, when unsuccessful |
Functions#
net.asReaderfunction#
function net.asReader(borrows source: net.Stream): affine(net.DirectionView)Borrows a connection's reading half through the shared contract.
Arguments
| Name | Type | Description |
|---|---|---|
borrows source | net.Stream | the connection to view |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
borrows source | net.Stream | the connection to view |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
options | net.DatagramOptions | where to bind |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
options | net.ConnectOptions | who to reach and how much may be queued unsent |
Returns
| Type | Description |
|---|---|
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
| Name | Type | Description |
|---|---|---|
options | net.ListenOptions | where to bind and how deep a backlog to ask for |
Returns
| Type | Description |
|---|---|
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): nilAdvances the selected network reactor.
Arguments
| Name | Type | Description |
|---|---|---|
timeoutMs | integer |
Returns
| Type | Description |
|---|---|
nil |