A server is a loop and its siblings told the served half of networking: accept, session, fan-in. This page is the other half — the program that dials. It is the half most programs are: every service is somebody’s client, and the client’s questions are its own. How do I name the other end? How do I say my request is complete and still read the answer? How do I read a binary frame the wire delivered in pieces? And what reaches a wait that nobody answers?
The example is examples/ClientRoad
in the tutorial repository — one program whose small server tasks exist so
the client half has someone to talk to; nothing in the client half knows
that.
Getting the file
make -C examples/ClientRoad runClientRoad
The address, three ways
Loopback() 127.0.0.1
ParseAddress(10.1.2.3) 10.1.2.3
Resolve(localhost) 127.0.0.1
The conversation with an end
sent 3 lines, then the half-close said: that was all
the answer: 3 lines, 15 runes
The frame, assembled
asked ReadFully for 16 bytes: got 16, and they sum to 136
asked again: got 0 - the stream ended, cleanly
IsOpen after the peer left: 1 - open is local, and Close is yoursEvery number is computed by the program: the server counts what actually arrived, the client sums what it actually read.
The example, walked
An address, three ways
shown := Resolve("localhost") on fail use Loopback();Loopback() names this machine, ParseAddress reads a dotted quad from
text, and Resolve asks the name service. Resolve deserves its sentence:
it is the one call in the net unit that occupies its carrier while it
waits, because name resolution has no non-blocking form anywhere the
operating system offers one. That is why resolving is a call you write on
purpose, not something the dial does quietly — a program that resolves knows
that it did, and a program that dials a literal address pays nothing for a
facility it never used. A service that must not stall resolves its peers
first and dials addresses afterwards, which is exactly what DialTcpAt
(dial an Address) beside DialTcp (dial a host string) is for.
The half-close: “that was all”
WriteLine(address c, "listen") on fail continue;
Shutdown(address c, ShutdownWrite) on fail continue;
got := ReadLine(address c, address answer) on fail use False;The client sends its lines and then ends only the write direction. The
server’s ReadLine loop simply ends — an ended read direction looks exactly
like a peer that finished, because it is one — and the wire back is still
whole, so the server answers on it and the client reads the answer. One
connection, a request of any length, no sentinel line invented to mean
“done”: the transport itself says it. This is the shape request-response
protocols are made of, and Shutdown(…, ShutdownWrite) is its one moving
part.
The frame road: assembled, ended, or short
got := ReadFully(address c, address frame[0], 16) on fail use 0;Text has lines; binary protocols have frames, and a stream socket promises
nothing about the pieces a frame arrives in — the example’s server sends 16
bytes in two eights on purpose. ReadFully assembles the full count no
matter how the wire chopped it, and its answer carries the one distinction a
binary protocol lives on: the full count means a frame, fewer means the
stream ended first. The second ask answers 0 — ended, cleanly, distinguishable
from a truncated frame. (ReadBytes is the lower door — whatever the next
delivery happens to hold — for protocols that do their own assembling.)
And the last line of the output states a boundary worth having in words:
IsOpen reports your side. A peer’s departure does not close your
connection; Close is yours to call, and the descriptor is yours until you
do.
A deadline reaches a parked wait
Every wait on this page — the dial, the accept, every transfer — parks its task: the task suspends, the carrier moves on. What if nobody ever answers? A scope deadline reaches a parked network wait the same way it reaches any other suspended task. This program is complete and runs as shown:
task WaitForNobody(li : Listener);
var
c : Connection;
caught : OsError;
begin
c := Accept(li) on fail caught do
begin
WriteLn("accept left with code %lld", Ord(caught));
leave;
end;
end;
...
concurrent
ScopeDeadline(200);
schedule WaitForNobody(l);
end;accept left with code 35
the deadline reached the parked acceptCode 35 is Ecanceled: the wait ended, the task left, the scope closed —
on both architectures, exactly alike. One boundary is stated on the
known limitations page: a typed file
transfer in flight is the wait a deadline cannot reach; every network wait
here can be.
Try it
1. Delete the half-close. Remove the Shutdown line and rerun. The
program hangs — stop it with Ctrl-C. The server is parked in ReadLine
waiting for a next line that will never come, because nothing ever said
“that was all”; the client is parked waiting for an answer the server has
not started. Two honest waits, one missing sentence between them. Put it
back — or reread the deadline section for the tool that turns a forever
into a bounded wait.
2. Truncate the frame. In FrameServer, delete the second
WriteBytes (and its for loop), so only 8 of the 16 bytes are ever sent:
asked ReadFully for 16 bytes: got 8, and they sum to 36ReadFully answered fewer than asked: the stream ended mid-frame, and the
caller can tell — which is precisely the distinction the verb exists to
carry.
Next
The IPC road — the same verbs with a name instead of a port, for the services that live on one machine. Every example behind the series lives in the tutorial repository.