A stream has no boundaries, so the kind supplies them (transport plane, tcp)

TCP delivers bytes, not messages. Whatever write calls a sender makes, the receiver may see them merged, split, or both — so a stream transport that handed its reader’s buffer straight up would deliver half frames and double frames.

tcp_transport_t prefixes each frame with u32-LE length and reads it back in two steps: read four bytes, then read exactly that many. The prefix is transport framing — it is on the wire, it is not in the TLV, and nothing above the transport ever sees it.

What to notice

  • Why a FIXED-width prefix and not the TLV’s own length field. A variable-width header cannot be read without first buffering an unknown number of bytes, which is the problem the prefix exists to solve. Four bytes, always, then the body.

  • Coalesced. Two whole records in one write arrive as two frames, split at the right byte and in order.

  • Split. One record dribbled out in three writes — the four-byte prefix itself torn in half — arrives as one frame, and no fourth frame is invented from the fragments. This is the case the fixed width has to survive: the reader cannot even know the length until all four bytes are in hand.

  • The prefix is demonstrated, not asserted. The example reads a transport-sent frame off a raw socket and decodes the length itself, so the framing is visible on the wire rather than described.

  • The peer is a raw POSIX socket on purpose. A second tcp_transport_t would be the realistic peer and exactly the wrong tool: it would choose the write boundaries itself and hide the thing being shown.

  • The fourth stream hazard is deliberately elsewhere. A prefix above the effective cap tears the connection down, because a stream that has lost framing sync cannot be resynchronized — that is a lifecycle concept, and tcp_test’s oversize-prefix case owns it.

  • This target needs the TCP transport. It is built only when LIBTRACER_TRANSPORT_TCP is 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 — a STREAM kind has to supply the frame boundaries the wire does not
  9 *        have, and `tcp` does it with a 4-byte little-endian length prefix that is
 10 *        TRANSPORT framing: it is on the wire, it is not in the TLV, and nothing above the
 11 *        transport ever sees it.
 12 *
 13 * TCP delivers bytes, not messages. Whatever `write` calls a sender makes, the receiver may
 14 * see them merged, split, or both — so a stream transport that handed its reader's buffer
 15 * straight up would deliver half frames and double frames. `tcp_transport_t` prefixes each
 16 * frame with `u32-LE length` and reads it back in two steps: read four bytes, then read
 17 * exactly that many. That is why the prefix is a FIXED width and not the TLV's own length
 18 * field — a variable-width header cannot be read without first buffering an unknown number
 19 * of bytes, which is the problem it was supposed to solve.
 20 *
 21 * The three stream hazards, each provoked on purpose against a raw POSIX socket so the
 22 * boundaries are this example's to choose rather than the kernel's to decide:
 23 *
 24 *  1. **Coalesced.** Two whole records in ONE `write` arrive as TWO frames.
 25 *  2. **Split.** One record dribbled out in three `write`s — the length prefix itself torn
 26 *     in half — arrives as ONE frame.
 27 *  3. **Framed on egress too.** A frame the transport SENDS is read off the raw socket as
 28 *     `u32-LE length ++ bytes`, so the prefix is demonstrated on the wire and not merely
 29 *     asserted about.
 30 *
 31 * A prefix above the effective cap is the fourth case and is deliberately NOT here: it
 32 * tears the connection down, which is a lifecycle concept rather than a framing one, and it
 33 * is what `tcp_test`'s oversize-prefix case covers.
 34 *
 35 * Needs the TCP transport (`LIBTRACER_TRANSPORT_TCP`, on by default). Runs under ctest as
 36 * `example_net_tcp_stream_framing`; returns non-zero on any failed check.
 37 */
 38
 39#include <arpa/inet.h>
 40#include <netinet/in.h>
 41#include <poll.h>
 42#include <sys/socket.h>
 43#include <unistd.h>
 44
 45#include <chrono>
 46#include <condition_variable>
 47#include <cstddef>
 48#include <cstdint>
 49#include <cstdio>
 50#include <mutex>
 51#include <span>
 52#include <vector>
 53
 54#include "libtracer/transport_tcp.hpp"
 55
 56namespace {
 57
 58using namespace std::chrono_literals;
 59using tr::net::tcp_transport_t;
 60
 61/** @brief Report expectation @p what and record a failure on @p ok. */
 62void check(bool& ok, bool cond, const char* what) {
 63    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
 64    ok = ok && cond;
 65}
 66
 67/** @brief A thread-safe borrowed-span sink: the recv thread pushes, `main` waits. */
 68class sink_t {
 69   public:
 70    /** @brief The receiver callback — copies the span, which dies when it returns. */
 71    void operator()(std::span<const std::byte> frame) {
 72        {
 73            const std::lock_guard lock(m_);
 74            frames_.emplace_back(frame.begin(), frame.end());
 75        }
 76        cv_.notify_all();
 77    }
 78
 79    /** @brief Wait until at least @p n frames have landed, or @p budget expires. */
 80    [[nodiscard]] bool wait_for(std::size_t n, std::chrono::milliseconds budget) {
 81        std::unique_lock lock(m_);
 82        return cv_.wait_for(lock, budget, [&] { return frames_.size() >= n; });
 83    }
 84
 85    /** @brief How many frames have landed so far. */
 86    [[nodiscard]] std::size_t count() const {
 87        const std::lock_guard lock(m_);
 88        return frames_.size();
 89    }
 90
 91    /** @brief Frame @p i, by value. */
 92    [[nodiscard]] std::vector<std::byte> at(std::size_t i) const {
 93        const std::lock_guard lock(m_);
 94        return frames_.at(i);
 95    }
 96
 97   private:
 98    mutable std::mutex m_;
 99    std::condition_variable cv_;
100    std::vector<std::vector<std::byte>> frames_;
101};
102
103/**
104 * @brief A raw POSIX TCP client — this example's own hand on the wire.
105 *
106 * A second `tcp_transport_t` would be the realistic peer, and it is exactly the wrong tool
107 * here: it would choose the write boundaries itself and hide the framing being demonstrated.
108 */
109class raw_client_t {
110   public:
111    /** @brief Connect to `127.0.0.1:@p port`; @ref ok reports whether it succeeded. */
112    explicit raw_client_t(std::uint16_t port) {
113        fd_ = ::socket(AF_INET, SOCK_STREAM, 0);
114        sockaddr_in peer{};
115        peer.sin_family = AF_INET;
116        peer.sin_port = htons(port);
117        ::inet_pton(AF_INET, "127.0.0.1", &peer.sin_addr);
118        if (::connect(fd_, reinterpret_cast<sockaddr*>(&peer), sizeof(peer)) < 0) {
119            ::close(fd_);
120            fd_ = -1;
121        }
122    }
123    ~raw_client_t() {
124        if (fd_ >= 0) ::close(fd_);
125    }
126
127    raw_client_t(const raw_client_t&) = delete;
128    raw_client_t& operator=(const raw_client_t&) = delete;
129
130    /** @brief True iff the connect succeeded. */
131    [[nodiscard]] bool ok() const noexcept { return fd_ >= 0; }
132
133    /** @brief Push @p bytes as ONE write, resuming partials — one chosen boundary. */
134    void write(std::span<const std::byte> bytes) {
135        std::size_t off = 0;
136        while (off < bytes.size()) {
137            const ssize_t n = ::send(fd_, bytes.data() + off, bytes.size() - off, 0);
138            if (n <= 0) return;
139            off += static_cast<std::size_t>(n);
140        }
141    }
142
143    /** @brief Read up to @p want bytes within @p budget, answering whatever arrived. */
144    [[nodiscard]] std::vector<std::byte> read_within(std::size_t want,
145                                                     std::chrono::milliseconds budget) {
146        std::vector<std::byte> got;
147        const auto deadline = std::chrono::steady_clock::now() + budget;
148        while (got.size() < want) {
149            const auto left = std::chrono::duration_cast<std::chrono::milliseconds>(
150                deadline - std::chrono::steady_clock::now());
151            if (left.count() <= 0) break;
152            pollfd p{fd_, POLLIN, 0};
153            if (::poll(&p, 1, static_cast<int>(left.count())) <= 0) break;
154            std::byte buf[256];
155            const ssize_t n = ::recv(fd_, buf, sizeof(buf), 0);
156            if (n <= 0) break;
157            got.insert(got.end(), buf, buf + n);
158        }
159        return got;
160    }
161
162   private:
163    int fd_ = -1;
164};
165
166/** @brief @p n bytes counting up from @p seed — a stand-in for an encoded frame. */
167std::vector<std::byte> frame_of(std::size_t n, unsigned seed) {
168    std::vector<std::byte> f(n);
169    for (std::size_t i = 0; i < n; ++i) f[i] = static_cast<std::byte>(seed + i);
170    return f;
171}
172
173/** @brief One wire record: `u32-LE length ++ @p payload` — the framing, spelled by hand. */
174std::vector<std::byte> record(std::span<const std::byte> payload) {
175    const auto len = static_cast<std::uint32_t>(payload.size());
176    std::vector<std::byte> out;
177    for (unsigned shift = 0; shift < 32; shift += 8)
178        out.push_back(static_cast<std::byte>((len >> shift) & 0xFFu));
179    out.insert(out.end(), payload.begin(), payload.end());
180    return out;
181}
182
183/** @brief Decode a `u32-LE` at the front of @p bytes — the prefix, read back. */
184std::uint32_t le32(std::span<const std::byte> bytes) {
185    std::uint32_t v = 0;
186    for (unsigned i = 0; i < 4; ++i) v |= static_cast<std::uint32_t>(bytes[i]) << (8 * i);
187    return v;
188}
189
190}  // namespace
191
192int main() {
193    bool ok = true;
194
195    sink_t at_listener;
196    tcp_transport_t listener(std::uint16_t{0});
197    listener.set_receiver(at_listener);
198    check(ok, listener.ok(), "the listener bound an ephemeral port");
199
200    raw_client_t client(listener.local_port());
201    check(ok, client.ok(), "the raw client connected");
202
203    // 1. COALESCED. Two complete records in one write. A reader that trusted its read
204    // boundaries would deliver this as one frame of the wrong length.
205    std::printf("two records in one write:\n");
206    const auto a = frame_of(5, 0x10);
207    const auto b = frame_of(9, 0x20);
208    std::vector<std::byte> both = record(a);
209    const auto rec_b = record(b);
210    both.insert(both.end(), rec_b.begin(), rec_b.end());
211    client.write(both);
212    check(ok, at_listener.wait_for(2, 2s), "arrived as TWO frames");
213    check(ok, at_listener.at(0) == a && at_listener.at(1) == b,
214          "…split at the right byte, in order");
215
216    // 2. SPLIT. One record in three writes, with the 4-byte prefix itself torn in half — the
217    // case a fixed-width prefix has to survive, since the reader cannot even know the frame
218    // length until all four bytes are in hand.
219    std::printf("one record in three writes, prefix torn in half:\n");
220    const auto c = frame_of(12, 0x30);
221    const auto rec_c = record(c);
222    client.write(std::span(rec_c).first(2));
223    client.write(std::span(rec_c).subspan(2, 6));
224    client.write(std::span(rec_c).subspan(8));
225    check(ok, at_listener.wait_for(3, 2s), "reassembled into ONE frame");
226    check(ok, at_listener.at(2) == c, "…byte-identical to what was framed");
227    check(ok, at_listener.count() == 3, "and no fourth frame was invented from the fragments");
228
229    // 3. EGRESS. The prefix is not an internal convention — it is on the wire, and the raw
230    // client reads it there.
231    std::printf("the prefix, read off the wire:\n");
232    const auto out = frame_of(7, 0x40);
233    listener.send(out);
234    const auto wire = client.read_within(4 + out.size(), 2s);
235    check(ok, wire.size() == 4 + out.size(), "the transport wrote exactly prefix + frame");
236    check(ok, wire.size() >= 4 && le32(wire) == out.size(),
237          "…the first four bytes are the frame length, little-endian");
238    check(ok, std::vector<std::byte>(wire.begin() + 4, wire.end()) == out,
239          "…and the rest is the frame, untouched");
240
241    check(ok, listener.malformed_rx() == 0, "no prefix on this run was refused as malformed");
242
243    std::printf("tcp: %zu frames reassembled from 4 writes, 4 bytes of framing each\n",
244                at_listener.count());
245    return ok ? 0 : 1;
246}

See also: transport module · frame codec · data format reference · the datagram family, which needs none of this · dial and listen.