DIAL and LISTEN are two constructors, not two types (transport plane)¶
The minimal pair, spelled with tcp_transport_t because it is the kind that makes the point
plainest: one class, two constructors, and nothing downstream can tell which one built the
object it holds.
tcp_transport_t(bind_port)listens. Pass0and the kernel picks the port;local_port()reports which.tcp_transport_t(peer_host, peer_port)dials, synchronously, inside the constructor.
Past bring-up the role is gone. The example’s exchange() helper takes a transport_t& and
drives either end in either direction, which is the whole claim in one signature.
What to notice¶
ok()is the CAME-UP predicate and it is role-specific (#1059): on LISTEN it answers did the bind succeed, on DIAL did the connect succeed. It is answered once, right after construction, and never reverts.link_up()is the other question — is this connection alive now. After a teardown the two diverge:ok()stays true (the link did come up),link_up()goes false. Confusing them is how a dead link gets treated as healthy.The failed bring-up is provoked deterministically. A second LISTEN on the port the first one already holds cannot succeed on any machine. Dialling a port nobody is expected to answer would be a guess about the host rather than a demonstration, and would flake.
Direction is not a property of the link. What makes a reply routable is the route the FWD plane grew into the frame’s
src, not which end opened the socket — see the src you accumulated is the way home.The ephemeral port is how the two halves rendezvous without a hard-coded number, which is also why every socket example in this tree binds
0: nothing here can collide with whatever else is running on the CI machine.This target needs the TCP transport. It is built only when
LIBTRACER_TRANSPORT_TCPis on (the default). Nothing in it is conditional at run time.
Source¶
1/*
2 * SPDX-License-Identifier: Apache-2.0
3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
4 */
5
6/**
7 * @file
8 * @brief ONE CONCEPT — DIAL and LISTEN are not two types, they are two CONSTRUCTORS of the
9 * same type; once a link is up the role has left the building and both ends are the
10 * same `transport_t`.
11 *
12 * This is the minimal pair, spelled with `tcp_transport_t` because it is the kind that
13 * makes the point plainest — one class, two constructors, and nothing downstream can tell
14 * which one built the object it holds:
15 *
16 * - `tcp_transport_t(bind_port)` LISTENS. Pass `0` and the kernel picks the port;
17 * `local_port()` reports which, which is how a dialer in the same process (a test, a
18 * demo, a supervisor that spawns both halves) finds it without a hard-coded number.
19 * - `tcp_transport_t(peer_host, peer_port)` DIALS, synchronously, in the constructor.
20 *
21 * Two properties follow from "the role is the constructor", and both are load-bearing:
22 *
23 * 1. **`ok()` is the CAME-UP predicate, and it is role-specific** (#1059): on LISTEN it
24 * answers "did the bind succeed", on DIAL "did the connect succeed". It is answered
25 * once, right after construction, and never reverts. `link_up()` is the different
26 * question — is the connection alive RIGHT NOW — and after a teardown the two diverge:
27 * `ok()` stays true (the link DID come up), `link_up()` goes false.
28 * 2. **Direction is not a property of the link.** The route the FWD plane grows into a
29 * frame's `src` is what makes a reply routable, not which end opened the socket, so
30 * both directions are demonstrated here over the one connection.
31 *
32 * The failed bring-up is provoked deterministically — a second LISTEN on a port the first
33 * one already holds — rather than by dialling a port nobody is expected to answer, which
34 * would be a guess about the machine rather than a demonstration.
35 *
36 * Needs the TCP transport (`LIBTRACER_TRANSPORT_TCP`, on by default). Runs under ctest as
37 * `example_net_dial_and_listen`; returns non-zero on any failed check.
38 */
39
40#include <chrono>
41#include <condition_variable>
42#include <cstddef>
43#include <cstdint>
44#include <cstdio>
45#include <mutex>
46#include <span>
47#include <vector>
48
49#include "libtracer/transport.hpp"
50#include "libtracer/transport_tcp.hpp"
51
52namespace {
53
54using namespace std::chrono_literals;
55using tr::net::tcp_transport_t;
56
57/** @brief Report expectation @p what and record a failure on @p ok. */
58void check(bool& ok, bool cond, const char* what) {
59 std::printf(" [%s] %s\n", cond ? "ok" : "FAIL", what);
60 ok = ok && cond;
61}
62
63/** @brief A thread-safe borrowed-span sink: the recv thread pushes, `main` waits. */
64class sink_t {
65 public:
66 /** @brief The receiver callback — copies the span, which dies when it returns. */
67 void operator()(std::span<const std::byte> frame) {
68 {
69 const std::lock_guard lock(m_);
70 frames_.emplace_back(frame.begin(), frame.end());
71 }
72 cv_.notify_all();
73 }
74
75 /** @brief Wait until at least @p n frames have landed, or @p budget expires. */
76 [[nodiscard]] bool wait_for(std::size_t n, std::chrono::milliseconds budget) {
77 std::unique_lock lock(m_);
78 return cv_.wait_for(lock, budget, [&] { return frames_.size() >= n; });
79 }
80
81 /** @brief Frame @p i, by value. */
82 [[nodiscard]] std::vector<std::byte> at(std::size_t i) const {
83 const std::lock_guard lock(m_);
84 return frames_.at(i);
85 }
86
87 private:
88 mutable std::mutex m_;
89 std::condition_variable cv_;
90 std::vector<std::vector<std::byte>> frames_;
91};
92
93/** @brief @p n bytes counting up from @p seed — a stand-in for an encoded frame. */
94std::vector<std::byte> frame_of(std::size_t n, unsigned seed) {
95 std::vector<std::byte> f(n);
96 for (std::size_t i = 0; i < n; ++i) f[i] = static_cast<std::byte>(seed + i);
97 return f;
98}
99
100/**
101 * @brief Everything past bring-up, written against the SEAM and not against either role.
102 *
103 * The whole point of the example in one signature: this function cannot tell which
104 * constructor made either argument, and does not need to.
105 */
106void exchange(bool& ok, tr::net::transport_t& from, sink_t& at_far_end, std::size_t nth,
107 unsigned seed, const char* what) {
108 const auto f = frame_of(6, seed);
109 from.send(f);
110 check(ok, at_far_end.wait_for(nth, 2s), what);
111 check(ok, at_far_end.at(nth - 1) == f, " …byte-identical");
112}
113
114} // namespace
115
116int main() {
117 bool ok = true;
118
119 // LISTEN. Port 0 asks the kernel for an ephemeral one; local_port() reports the answer.
120 sink_t at_listener;
121 tcp_transport_t listener(std::uint16_t{0});
122 listener.set_receiver(at_listener);
123 check(ok, listener.ok(), "LISTEN came up — the bind succeeded");
124 const std::uint16_t port = listener.local_port();
125 check(ok, port != 0, "local_port() resolved the ephemeral 0 to a real port");
126
127 // DIAL. The connect runs inside the constructor, so ok() answers for it on return.
128 sink_t at_dialer;
129 tcp_transport_t dialer("127.0.0.1", port);
130 dialer.set_receiver(at_dialer);
131 check(ok, dialer.ok(), "DIAL came up — the connect succeeded");
132
133 // Past bring-up neither end is privileged: the same call, both ways, through a reference
134 // that has forgotten which constructor ran.
135 std::printf("both directions over the one connection:\n");
136 exchange(ok, dialer, at_listener, 1, 0x10, "dialer -> listener");
137 exchange(ok, listener, at_dialer, 1, 0x20, "listener -> dialer");
138 exchange(ok, dialer, at_listener, 2, 0x30, "dialer -> listener, again");
139
140 check(ok, listener.link_up() && dialer.link_up(), "both ends report the link live");
141
142 // A bring-up that FAILS, provoked deterministically: the port is already held by the
143 // listener above, so the second bind cannot succeed on any machine.
144 std::printf("a refused bring-up:\n");
145 tcp_transport_t taken(port);
146 check(ok, !taken.ok(), "a second LISTEN on a held port did NOT come up");
147 check(ok, !taken.link_up(), "…and reports no live link either — nothing was spawned");
148
149 // ok() never reverts, which is what makes it a different question from link_up(). The
150 // listener is still up here, so this only states the invariant the two accessors carry;
151 // the divergence itself belongs to the teardown paths the transport tests cover.
152 check(ok, listener.ok() && dialer.ok(), "ok() is the came-up fact, answered once");
153
154 std::printf("one type, two constructors: listener on port %u, %s\n",
155 static_cast<unsigned>(port), ok ? "3 frames exchanged" : "FAILED");
156 return ok ? 0 : 1;
157}
See also: transport module · connection config · transports are vertices · the stream framing this kind adds · the multi-peer listener.