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 mica-container/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. While the name service works, Resolve parks only the calling task, as every wait in the net unit does: the carrier runs the program’s other tasks, and a deadline or a cancellation of the scope ends the lookup on time. Resolving is still 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 resolves its peers and dials the addresses, 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: save it as Deadline.mica and run it.

program Deadline;

imp
    WriteLn, Ord, ScopeDeadline : std;
    Listener, Connection, ListenTcp, Accept, CloseListener : net;
    OsError : posix;

var
    l : Listener;

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;

begin
    l := ListenTcp(0) on fail use l;
    concurrent
        ScopeDeadline(200);
        schedule WaitForNobody(l);
    end;
    WriteLn("the deadline reached the parked accept");
    CloseListener(address l);
end.
mica Deadline.mica
the deadline reached the parked accept

The deadline ended the parked accept and took its task out before the failure handler’s first statement: a handler meets only the failures its call reports, and a wait that a deadline ended is not one of them. The scope closed, and the program went on after its block — on both architectures, exactly alike. A deadline reaches every wait here, and a typed file’s wait on a pipe or a terminal just the same.

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.