TCP delivers bytes. It does not preserve your message boundaries.
The common read: A socket write is a message send, so three writes are three arrivals and the receiver's read loop sees the messages I sent.
TCP is a stream of bytes, and your write calls define no boundaries in it: RFC 9293 guarantees "no correlation between the boundaries of TCP segments sent and received and the boundaries of the read or write buffers of user application data", so the stack may merge, split and re-chunk your writes anywhere on the path. UDP does not re-chunk: a datagram arrives whole or not at all, which is why the three UDP sends in the first screen arrived as three.
So over TCP, framing is the application's job: a length prefix, a delimiter, or a self-describing encoding. A handler that assumes one read is one message passes on localhost and in staging, then fails the week a proxy is added between the services; the ticket says "the proxy is corrupting our JSON", and the packet capture shows every byte arriving intact, because the reader split one message across two reads.
A length prefix is an encoding decision: it makes the stream parseable and authenticates nothing. The length is a number an attacker can choose, so check it against a maximum before you buffer that many bytes, or you replace a framing bug with a memory bug.

