net lets a Shoddy program talk over a TCP/IP socket. TCP/IP is
the set of rules computers use to send data across a network, and a socket is
one live connection made under those rules. Your program can be a client
dialing out to a server, or a server listening for clients. You hand
Connect a host and a port and get back a handle — a plain Number,
just like a file handle. You Send strings down it,
Recv strings back, and Close it when you're done. On
the server side, Listen gives you a handle you can
Accept connections on. The bytes on the wire arrive as ordinary
Shoddy strings, one character per byte. So &, Mid,
Asc and Chr all work on them the same way they work on
anything else.
It sits on top of the raw TCP* builtins and adds the two things
you'd otherwise write by hand. The first is the polite polling loop
(RecvAll, WaitAccept) that turns the never-blocking
primitives into read-until-done convenience. The second is the
connect/do/close dance (Request, Serve1) that makes
sure a handle always gets closed.
TCP* word is off by default and raises "network is disabled"
unless the mill was run with --allow-net:
bin/mill run --allow-net myprogram.shoddy
This is the only builtin family that reaches past the machine's own files and
console, so it's opt-in on purpose. The flag sets SHODDY_ALLOW_NET
in the environment. A woven standalone program (dotnet myprogram.dll)
honours the same switch — set that variable to 1 before running
it.A network connection here is a handle you Send to and
Recv from — a number, like a file. That design decision was
taken at Berkeley in the early 1980s. TCP/IP needed a programming interface
for 4.2BSD Unix in 1983, and its authors made a network endpoint a file
descriptor — the same kind of numbered handle that names an open file.
You open something called a socket, you read and write it, you close it.
Nothing about that was inevitable. Rival systems of the day exposed the
network as terminals, as message queues, or as elaborate protocol machinery.
But the socket borrowed the familiarity of files, and it won completely.
Every operating system since has shipped the Berkeley interface, usually down
to the function names; even Windows copied it wholesale as Winsock in 1991.
Four decades on it remains computing's most durable API (programming
interface). The connect/send/recv/close dance this machine wraps is, give or
take a spelling, exactly the one a VAX programmer typed in 1983.
A great deal of what a program wants to do lives on the other end of a
socket: fetch a page, hit a small HTTP API, talk to a database or a message
broker, or stand up a tiny server of your own so two programs on the same
machine can talk. net is the door to all of that. Reach for it
whenever your program needs to send bytes to, or receive bytes from, something
that isn't a local file. Stay on the loopback address
(127.0.0.1 — the address that always means "this same machine")
while you're learning, where the only thing that can connect is you.
Every socket word returns immediately — none of them blocks, that is, stops and waits. This is the deliberate heart of the design, and it shapes how you use the machine:
TCPRecv hands back "" the instant there's nothing to
read right now — it never waits for the other side to say something.TCPAccept hands back 0 the instant no client is
queued — it never waits for one to knock.Blocking would freeze the program on its single thread, and with it any open
scribbler window or debug session. That is the same reason InKey
never blocks. The cost is that "" from TCPRecv is
ambiguous: it means both "nothing has arrived yet" and "the peer — the
program at the other end — has closed." TCPEof tells the two
apart. It's True only once the far end has actually hung up.
You rarely touch that ambiguity directly, because the machine's
Wait* helpers wrap it up. WaitRecv and
RecvAll poll — try, pause, try again — until data arrives or the
peer closes. They yield 5 ms between tries with Sleep, so
they don't spin the CPU. WaitAccept polls until a client
connects. They read as if they blocked. But a game loop or a busy server can
still poll by hand with Recv, Ready and
Accept when it needs the thread back between frames.
Request is the whole client story in one word. It connects,
sends your message, reads the entire reply until the server closes, and closes
the handle for you:
Include "net.shoddy"
Def Main()
Let page = HttpGet("example.com", "/")
Print(page) ' status line, headers, body
HttpGet is just a thin wrapper over Request. Here's
the same call written out, which is the shape of any request/response protocol:
Def Main()
Let reply = Request("example.com", 80,
"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
Print(reply)
When you want the handle in your own hands — to send several messages, or read in pieces — open it yourself and close it yourself:
Def Main()
Let s = Connect("127.0.0.1", 7000)
SendLine(s, "HELLO") ' appends \r\n
Print(WaitRecv(s)) ' block-until-something, politely
Close(s)
Listen binds the loopback address and returns a server handle.
Serve1 waits for one client, hands the connection to your function,
and closes it afterwards. Here's an echo server that shouts a line back
upper-cased:
Include "net.shoddy"
Include "str.shoddy"
Def Main()
Let srv = Listen(7000)
Serve1(srv, Fn(c) => Send(c, Upper(RecvAll(c))))
Close(srv)
To serve more than one client, call Serve1 in a recursion of
your own. To reach beyond this machine, bind an explicit address with
ListenOn("0.0.0.0", 7000) instead of Listen — but do
that only when you mean to be reachable from the network.
When you can't give up the thread — inside an animation loop, say — skip the
Wait* helpers and poll:
' once per frame, never blocking:
Let c = Accept(srv) ' 0 if nobody knocked
If c <> 0 Then
Each(...) ' register the new client
Let msg = Recv(client, 4096) ' "" if nothing this frame
If msg <> "" Then
... ' handle it
A few things worth knowing:
Connect is the one word that pauses — for the TCP handshake, the
brief hello that opens a connection. It gives up after ten seconds so a dead
host can't hang your program. Everything after it returns at once.Listen binds
127.0.0.1, reachable only from this machine. Widening that to the
network is a deliberate ListenOn call.Print and the file words, sending and receiving are effectful. Keep
them at the edges of your program and let the rest stay pure.Request and Serve1 close for
you; when you hold a handle yourself, Close it yourself.The thirteen socket words the runtime dispatches — not defined here, documented here
These are not net's Defs. The engine dispatches
them, and a Def whose name is a builtin is refused. They are
listed on this page because net is the friendly layer over
exactly these thirteen, and they need no Include. The same
thirteen are documented in machines/net.shoddy's own header
block.
Handles are Numbers, drawn from the same table of sixteen
that binary files use. Payloads are Strings carrying one byte per
character — the Latin-1 convention, the same one
ReadFile uses.
The whole family is gated behind --allow-net.
Two exceptions exist so it can be reached from guarded code at all:
NetAllowed, which has to stay askable when the answer is no, and
TryTcpConnect, which reports rather than dies. Everything else
aborts if the network is off. On a plain socket nothing blocks;
a secured one is the deliberate exception described above.
| Word | Description |
|---|---|
| NetAllowed() | Whether this mill was started with
--allow-net. Not itself gated, and that is the
point: without it nothing could tell "no network" from "no answer" without
ending the run. A program cannot turn the capability on; it can only
ask. |
| TCPConnect(host, port) | Connect and answer the handle. Bounded 10-second handshake; aborts if unreachable, if the network is off, or if all sixteen handles are taken. |
| TryTcpConnect(host, port) | TCPConnect with all
three of those deaths reported instead: Ok(handle), or
Err(why, 0) where why is
CANNOT REACH 'host:port' (…) ending in one of
NETWORK IS DISABLED, TOO MANY OPEN SOCKETS,
TIMED OUT or NO SUCH HOST OR REFUSED — a closed set
of Shoddy's own, never the platform's socket text. |
| TryTcpRequest(host, port, secure, msg) | A whole
conversation in one word: connect, optionally secure, send, read to
the end of the reply, close — answering Ok(reply) or the same
shape of Err. It exists because guarding the steps is
not enough. Send and Recv die eight ways between
them on ordinary network faults. So a caller composing guarded parts would
have to close the socket on each of a dozen error paths, and would leak it on
the one it missed. Here the socket cannot outlive the word. It is also the
only network read that cannot hang — it is bounded by a
deadline, where a poll-until-closed loop never returns against a keep-alive
peer. |
| Word | Description |
|---|---|
| TCPListen(host, port) | Bind and listen, answering the listening
handle. The host is an IP literal: "127.0.0.1" for loopback,
"0.0.0.0" for any. |
| TCPAccept(h) | A connection handle, or 0 when
none is waiting. Never blocks, which is what WaitAccept below
polls around. |
| Word | Description |
|---|---|
| TCPSend(h, s) | Send the bytes. |
| TCPRecv(h, max) | Up to max bytes; ""
when nothing is pending or the peer has closed. TCPEof is
what tells those two apart, and the pair is why a drain loop needs
both. |
| TCPEof(h) | Whether the peer has closed. On a secured handle this reads a flag rather than polling. |
| TCPPoll(h) | Whether Recv or Accept
would find something right now. Refused on a secured handle:
once TLS (the encryption layer that keeps a connection private) wraps the
stream in its own records, a TCP-level poll cannot answer honestly about the
plaintext, so it aborts rather than lie. |
| TCPPeer(h) | The remote end as "ip:port". |
| Word | Description |
|---|---|
| TCPClose(h) | Close the handle and free its slot. |
| TCPSecure(h, host) | Upgrade a connected socket to TLS in
place. The host drives both SNI (the name sent during the handshake so
the server knows which site you want) and the check that the server's
certificate carries that name. A failed handshake closes the handle. From this
moment that handle follows the secured contract above — Recv
blocks, Poll refuses. A program cannot ask a handle whether it is
secured, so know which kind you are holding. In practice you always do,
because you secured it. Server-side TLS is not supported. |
Every word this machine exports
| Word | Description |
|---|---|
| Connect(host, port) | Dials host:port and returns a
connection handle. Blocks only for the handshake, giving up after ten seconds;
aborts if the host can't be reached. |
| Send(sock, s) | Sends every byte of s. |
| SendLine(sock, s) | Sends s followed by
\r\n — the line terminator most text protocols expect. |
| Recv(sock, n) | Reads up to n bytes and returns them,
or "" if none have arrived yet. Never blocks. |
| WaitRecv(sock) | Polls (yielding 5 ms between tries) until at
least one chunk is readable, then returns it; "" once the peer has
closed. |
| RecvAll(sock) | Reads the whole response, chunk by chunk, until the peer closes, and returns it as one string. |
| Request(host, port, msg) | Connect, send msg, read the
full reply, close — the request/response shape with the handle managed for
you. |
| HttpGet(host, path) | A minimal HTTP/1.0 GET of
path from host on port 80, returning the raw response.
Proof the stack works end to end. |
| Word | Description |
|---|---|
| Listen(port) | Binds 127.0.0.1:port — loopback only —
and returns a server handle. |
| ListenOn(host, port) | Binds an explicit IP literal, e.g.
ListenOn("0.0.0.0", 8080) to accept from anywhere. Use only when
you mean to be reachable. |
| Accept(server) | Returns a connection handle for a waiting client,
or 0 if none is queued. Never blocks. |
| WaitAccept(server) | Polls (yielding 5 ms between tries) until a client connects, then returns its connection handle. |
| Serve1(server, handler) | Waits for one client, calls
handler with the connection, then closes it. |
| Word | Description |
|---|---|
| Ready(sock) | → Boolean. True when a Recv (or
Accept, on a listener) would find something now. |
| Eof(sock) | → Boolean. True once the peer has closed — what
tells ""-from-recv apart from "nothing yet." |
| Peer(sock) | → the remote end as "ip:port", or
"". Handy for logging. |
| Close(sock) | Shuts the socket down and frees its handle. Works on a connection or a listener. |
These wrap the raw builtins TCPConnect,
TCPListen, TCPAccept, TCPSend,
TCPRecv, TCPEof, TCPPoll,
TCPPeer and TCPClose — reach for those directly if you
want the primitives without the polling helpers.
Every word on this page is a plain verb — Connect,
Send, Recv, Close, Listen,
Accept, Ready, Eof, Peer.
That is deliberate: inside a program that is about the network they
read exactly right. But they are also the words every other machine wants.
turtle exports Close too, and including
both bare is an error, not a silent winner:
ERROR: duplicate definition of CLOSE across machines — include one of them under a namespace (AS)
When net shares a program with anything else, give
this machine the As and leave the other one bare. One
prefix settles Close and every future collision at once.
Qualifying the other machine settles only the word you tripped over. Prefer a
prefix that isn't already a word-family in the library. Sock
works; Net does not, because
neural already exports NetNew,
NetTrain, and its own NetClose:
Include "net.shoddy" As Sock
Include "turtle.shoddy"
Def Draw(t As Turtle) As Turtle
Close(Forward(t, 100)) ' bare — the turtle's
Def Hangup(s As Number)
Close In Sock (s) ' the socket's; SockClose(s) is the same word
The qualifier collapses to a single word, so Close In Sock and
SockClose are two spellings of one name — use whichever reads
better at the call site. Qualification is never required at the use
site. Connect, Send and the rest still answer to
their bare names here, because only a genuine collision between two namespaces
forces you to choose. See Namespaces in
the spec.
| User | How | |
|---|---|---|
| emley-moor | The server
side: Listen, Accept, WaitRecvFor,
Send, Close. | |
| https | The sockets and the
TLS upgrade under an HTTP client: RequestTls is what
HttpsGet is built on. | |
| weather-glass | Underneath https, and
nowhere else — the mill never touches a socket itself. |
| Machine | Why | |
|---|---|---|
| str | Join is what
turns those fragments back into one reply. |