net — UDP sockets
The reference for the bundle in extensions/net, and for the client and server that come with it.
This is not part of the language. solvm has no networking and gains it only
when a host asks for it by name:
make # builds build/extensions/net.so
solvm --extension=build/extensions/net.so program.sob
Without that flag a program that mentions net fails at the line that first
names it — undefined name ‘net’ — which is the whole arrangement working: the
capability exists and granting it is a decision somebody takes on a command
line. Why it is an extension rather than a machine, and why it may live in this
repository when GTK and SDL2 may not, is in
extensions/net/README.md.
Everything here is IPv4 UDP. There is no TCP, no IPv6 and no name resolution — see What is not here.
The five messages
net:udp(#port) |
a bound socket |
net:port(socket) |
the port it is bound to |
net:send(socket, host, #port, text) |
bytes written |
net:receive(socket) |
a packet, or nil |
net:waitFor(socket, #ms) |
true if one arrived in time |
There is no close, and that is deliberate. The collector closes a socket when
the program lets go of it, and machine teardown closes one still held —
including when a limit took the program away mid-flight, which is exactly when
an explicit close would not have run.
net:udp(#port)
Answers a bound UDP socket. #0 asks the system for a free port, which
net:port then reports. It binds on every interface, not on loopback, so
another machine can reach it.
sock := net:udp(#7777). ; a known port, for something to be found at
sock := net:udp(#0). ; any free one, for something doing the finding
The socket is non-blocking, which is why receive answers nil rather than
waiting and why waiting is a message of its own.
| refuses | |
|---|---|
net:udp("7777") |
udp expects an integer port |
net:udp(#70000) |
udp: a port is 0 to 65535, got 70000 |
| a port already in use | udp: cannot bind port 7777 |
net:port(socket)
The port the socket is bound to, as an integer. The only way to learn the one
the system chose for net:udp(#0).
| refuses | |
|---|---|
| anything that is not an open socket | port expects an open socket |
net:send(socket, host, #port, text)
Sends one datagram and answers how many bytes went out. host is an address
written out — "127.0.0.1" — and never a name.
net:send(sock, "127.0.0.1", #7777, "add 5"). ; answers #5
A datagram is one message: what one send writes, one receive reads, whole or
not at all. Nothing here frames anything, which is the half of TCP that UDP
hands back to you and the reason the example’s protocol is one line of text.
| refuses | |
|---|---|
| wrong arity or types | send expects a socket, an address, a port and a string |
net:send(sock, "nope", #1, "x") |
send: ‘nope’ is not an address – a name is not resolved here |
| a datagram the system would not take | send: the datagram was not sent |
net:receive(socket)
The packet waiting, or nil when none is. It never waits.
A packet is an object with three slots:
packet:host |
the address it came from, as a string |
packet:port |
the port it came from, as an integer |
packet:text |
the bytes, as a string |
So a reply is net:send(sock, packet:host, packet:port, "...") and reads as
one. That the packet says where it came from is the thing the socket in
the extension probe could not do, and it is
what separates two programs talking from two programs shouting.
A datagram carries at most 65,507 bytes over IPv4 and nothing here truncates one.
| refuses | |
|---|---|
| anything that is not an open socket | receive expects an open socket |
net:waitFor(socket, #ms)
Answers true if a datagram arrived inside the timeout and false if none
did. #0 asks whether one is waiting right now. Waits of more than a minute are
treated as a minute.
net:waitFor(sock, #2000):ifElse(
{ net:receive(sock):text:print },
{ "no answer in 2s":print }).
Why a timeout rather than a blocking read. A blocking read stops the only
thread there is — and it stops the dispatch loop, which is where --steps
counts and where --memory is checked. A program parked inside a primitive is a
program no limit can reach, so a blocking read would suspend the guarantee
6.33
makes for as long as the peer stayed silent. A bounded wait keeps that window a
window, and hands the decision about going round again back to the program.
| refuses | |
|---|---|
| wrong arity or types | waitFor expects a socket and a number of milliseconds |
net:waitFor(sock, #-1) |
waitFor: a wait is not negative |
The shape a program takes
Every program using this ends up the same way, and it is worth seeing once:
{ running }:whileTrue({
net:waitFor(sock, #1000):ifTrue({ | packet |
packet := net:receive(sock).
packet:notNil:ifTrue({ ... }) }) }).
Wait with a bound; if something came, take it; go round. The notNil is not
superstition — waitFor says a datagram was readable, and by the time
receive runs it may have been discarded, so the two are separate questions and
the program asks both.
The example
Two programs, and between them a counter that lives in the server.
solas extensions/net/server.sol -o /tmp/server.sob
solas extensions/net/client.sol -o /tmp/client.sob
solvm --extension=build/extensions/net.so /tmp/server.sob 7777 &
solvm --extension=build/extensions/net.so /tmp/client.sob 7777 "add 5" "add 37" get stop
client server
"add 5 -> 5" counter listening on #7777
"add 37 -> 42" add 5 from 127.0.0.1:60338 -> 5
"get -> 42" add 37 from 127.0.0.1:60338 -> 42
"stop -> 42" get from 127.0.0.1:60338 -> 42
stop from 127.0.0.1:60338 -> 42
counter stopping, total #42
The protocol is one line of text, because a datagram is one message and framing is the problem UDP hands back:
add 5 |
add five to the total, and answer it |
get |
answer the total |
stop |
answer it once more and leave |
The client binds #0 and the server answers whatever it was given. Nothing
in the client has to know its own address for a reply to find it, which is the
packet’s host and port doing their job.
A reply that never comes is an ordinary outcome. UDP promises nothing, so the client waits two seconds and says no answer in 2s rather than waiting forever. A program that needs delivery has to arrange it itself — which is a fair summary of what TCP is for, and is why the absence below is a real one rather than a shrug.
What is not here
| TCP | no streams, no accept, and nothing that retries |
| IPv6 | AF_INET only |
| Name resolution | send takes an address inet_pton accepts. Resolving a name means getaddrinfo, which blocks — the thing waitFor exists to avoid |
| Broadcast and multicast | no socket options are exposed at all |
Each is absent because no program has asked, which is the rule the rest of this language was built by. The entry that would grow is ideas.md’s networking entry, whose other half — sending code to a machine that is already running — is untouched and wants 3.4 and 6.32 first.
What holds this page honest
Every block above is a transcript rather than a checked claim, because the checker runs Solum and these need a bundle loaded. What stands behind them instead:
test_the_net_extension_carries_a_datagramintests/test_cli.cruns a real round trip through the real binary and the real bundle, underSOLUM_GC_STRESS=1, and asserts that the packet names the sender.- Every refusal quoted above was produced by running it.
- The client and server are in the tree and can be run as written.