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 run
ClientRoad

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 yours

Every 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 accept

Code 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 36

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