The Machines · Networking

net

TCP/IP Client & Server — machines/net.shoddy

the net machine's icon

Summary

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.

The network is a gated capability. Every 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 Brief History of the Socket

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.

Why It's Useful

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.

The one thing to understand: nothing blocks

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:

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.

User's Guide

A client: fetch something

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)

A server: answer one client

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.

Polling by hand

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:

Builtins

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.

Asking, and connecting

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

Serving

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

Talking, and asking about the stream

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

Closing, and TLS

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

Word Reference

Every word this machine exports

Client

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

Server

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

Common to both

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

Include It Under a Namespace

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.

Who Uses It

UserHow
emley-moorThe server side: Listen, Accept, WaitRecvFor, Send, Close.
httpsThe sockets and the TLS upgrade under an HTTP client: RequestTls is what HttpsGet is built on.
weather-glassUnderneath https, and nowhere else — the mill never touches a socket itself.

The Machines It Uses

MachineWhy
strJoin is what turns those fragments back into one reply.