net.poll
import net
Everything here is re-exported by
net, soimport netis enough and the names are called asnet.*. Importingnet.pollon its own works too and reaches the same definitions.
Readiness polling: given a set of sockets, which of them can be read or written right now, without blocking.
Every other call in net blocks. TcpStream.read() waits until its own
socket has something to say and the thread does nothing else in the
meantime, which is right for a client and wrong for a server: a thread
that commits to one connection cannot serve another until that
connection lets it go. A server built only from blocking calls can hold
exactly as many concurrent connections as it has threads.
A Poller is the way out. It watches many sockets at once and tells you
which ones are ready, so a single thread can own hundreds of connections
and spend its time only on the ones with work:
import net
var server = net.TcpStream()
server.bind('127.0.0.1:8080')
var poller = net.Poller()
var clients = {}
var next_token = 1
# Token 0 watches the listener itself; an incoming connection makes
# it readable exactly the way data does on a connected socket.
poller.add(server, 0, net.READABLE)
while true {
for event in poller.wait(1000) {
var token = event[0]
if token == 0 {
var client = server.accept()
clients[next_token] = client
poller.add(client, next_token, net.READABLE)
next_token++
continue
}
var client = clients[token]
# A peer that has gone away is reported rather than left to be
# discovered by a read that returns nothing, forever.
if event[1] & (net.ERROR | net.HANGUP) != 0 {
poller.remove(token)
clients.remove(token)
client.close()
continue
}
echo client.read(1024).to_string()
}
}
Isolates
A Poller watches the sockets of the isolate that owns it, and nothing
about it crosses an isolate boundary. Several worker isolates each
keeping their own poller over their own connections is the intended
shape, and is exactly the shared-nothing model.
A socket that is closed, upgraded into a TlsStream, or moved to
another isolate stops being able to supply a descriptor. Rather than
this being a rule you have to remember, wait() reports such a socket
as ERROR against its own token, because the descriptor is resolved
fresh on every call and never remembered between them. It is not
possible for a poller to end up watching a descriptor that the operating
system has since handed to something else.
What polling does not do
Readiness is not a guarantee. A socket reported READABLE has data
now, but a read can still come up short, and a large write can still
be partial on a socket reported WRITABLE. Poll to decide what to work
on; keep handling short reads and writes the way you would anyway.
Constants
READABLE
net.READABLE: int = 1
Interest: tell me when this socket has data to read, a connection to accept, or an end-of-stream to observe.
WRITABLE
net.WRITABLE: int = 2
Interest: tell me when this socket can be written to without blocking.
ERROR
net.ERROR: int = 4
Readiness only: the socket has failed, or its handle can no longer supply a descriptor because it was closed, upgraded, or moved to another isolate. Deregister it.
HANGUP
net.HANGUP: int = 8
Readiness only: the peer has closed its end. Any data already sent is still readable, so drain the socket before closing it.
Classes
Poller
class net.Poller
A set of sockets to watch, each under a token of your choosing.
The token is whatever number is convenient for finding the socket again
-
an index into your own table, a connection id.
wait()hands back the tokens that are ready, never the sockets, so the mapping stays yours. -
printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.Poller()
Returns a new, empty Poller.
Poller.add()
net.Poller.add(socket, token: number, interest: ?number)
Starts watching socket under token.
Registering a token that is already present replaces it, so a caller that loses track cannot end up with one token naming two sockets.
Parameters
socket(TcpStream|TlsStream) — connected or bound; a bound listener becomes readable when a connection is waitingtoken(number) — your own identifier for this socketinterest(?number) —READABLE,WRITABLE, or both; defaults toREADABLE
Returns — Poller: this same instance, for chaining
Raises ValueError if interest names neither readability nor
writability
Poller.modify()
net.Poller.modify(token: number, interest: number)
Changes what token is watched for, leaving the socket alone.
This is how a connection with a reply still to write switches from waiting for a request to waiting for room to send, and back again.
Parameters
token(number)interest(number)
Returns — bool: whether the token was registered
Raises ValueError if interest names neither readability nor
writability
Poller.remove()
net.Poller.remove(token: number)
Stops watching token, and releases this poller’s reference to its
socket. Removing a token that is not registered does nothing rather than
failing.
Parameters
token(number)
Returns — bool: whether the token was registered
Poller.contains()
net.Poller.contains(token: number) -> bool
Whether token is currently registered.
Parameters
token(number)
Returns bool
Poller.interest()
net.Poller.interest(token: number) -> ?number
The interest currently registered for token, or nil.
Parameters
token(number)
Returns ?number
Poller.tokens()
net.Poller.tokens() -> list
Every registered token, in registration order.
Returns list
Poller.length()
net.Poller.length() -> number
How many sockets are being watched.
Returns number
Poller.is_empty()
net.Poller.is_empty() -> bool
Whether nothing is being watched.
Returns bool
Poller.clear()
net.Poller.clear()
Stops watching everything.
Returns — Poller: this same instance, for chaining
Poller.wait()
net.Poller.wait(timeout: ?number)
Waits until at least one registered socket is ready, and returns what happened.
Each entry is [token, flags], where flags is any combination of
READABLE, WRITABLE, ERROR and HANGUP. An empty list means the
timeout elapsed with nothing ready - and also that nothing is
registered, since waiting on an empty set could only ever sleep for the
full timeout to no purpose.
Parameters
timeout(?number) — milliseconds to wait;0polls without waiting, a negative number waits indefinitely. Defaults to waiting indefinitely.
Returns — list: [token, flags] pairs, one per ready socket
Raises Error on an underlying platform failure
Note: Test flags with
&, never==: a socket can be readable and hung up at the same time, which is what a peer that sent a final response and closed looks like.
Poller.to_string()
net.Poller.to_string()
Acceptor
class net.Acceptor
An accept loop that never parks the thread inside accept().
A blocking accept() waits inside the runtime, where nothing else on
the thread gets a turn: a signal trapped with os.on_signal() is not
delivered, and a flag telling the server to stop is not read, until a
connection happens to arrive. On an idle server that is never, which is
why Ctrl+C on one can appear to do nothing.
An Acceptor puts the listener into non-blocking mode and waits on a
Poller instead. next() hands over a connection when there is one,
and returns nil when the interval passes with nothing arriving, which
is the loop’s chance to look at whatever else it has to look at.
import net
import os
var listener = net.TcpStream()
listener.bind('127.0.0.1:8080')
var running = [true]
os.on_signal('INT', @() {
running[0] = false
return true
})
var acceptor = net.Acceptor(listener)
while running[0] {
var client = acceptor.next()
if client == nil {
continue
}
echo 'connection from ${client.peer_address()}'
client.close()
}
Nothing is added while connections are arriving: next() tries
accept() first, and only consults the poller when the queue is empty.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.Acceptor(listener, interval: ?number)
Takes over listener, which must already be bound.
The listener is put into non-blocking mode and left that way, so a
caller that also accepts from it directly gets nil when nothing is
waiting rather than a blocking wait.
Parameters
listener(TcpStream|UnixStream) — bound and listeninginterval(?number) — Millisecondsnext()waits before giving up on a round. Defaults to100. A connection that arrives wakes the wait at once, so this is the idle interval rather than added latency: it decides how soon a stopped server and a trapped signal are noticed.
Raises ValueError if interval is negative, which would wait
forever and defeat the point.
Raises Error if the listener is not bound, or cannot be put into
non-blocking mode.
Acceptor.next()
net.Acceptor.next() -> ?TcpStream|?UnixStream
The next connection, or nil when the interval passed with nothing
arriving.
nil means “go round again”, never “there will be no more”.
The connection comes back in blocking mode whatever mode the listener is in, since what usually follows is a request pipeline written against blocking reads bounded by timeouts.
Returns ?TcpStream|?UnixStream
Raises Error on an underlying accept failure, including the
listener having been closed.
Acceptor.wait_for_one()
net.Acceptor.wait_for_one() -> TcpStream|UnixStream
Waits for a connection however long it takes, the way a blocking
accept() would, while still coming up for air every interval.
Returns TcpStream|UnixStream
Raises Error on an underlying accept failure
Acceptor.listener()
net.Acceptor.listener() -> TcpStream|UnixStream
The listener this accepts from.
Returns TcpStream|UnixStream
Acceptor.to_string()
net.Acceptor.to_string()
2026, Richard Ore and Zuri contributors