A TCP socket is a connected byte-stream endpoint. A listener is a different object: it queues connections that accept turns into new connected sockets. Keep listener ownership, connection ownership and application framing separate. A successful send does not create a matching receive-call boundary.
Bind explicitly and complete the connection
The teaching server binds 127.0.0.1 only and requests port zero. The kernel selects an available port, which getsockname reports in network byte order. Convert that field for the C++ interface, and convert the caller's host-order port when constructing the client's sockaddr_in. No fixed port is needed.
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <arpa/inet.h>
int main() {
std::uint16_t port=htons(12345);std::array<unsigned char,2> bytes{};std::memcpy(bytes.data(),&port,2);
std::printf("%u %u\n",unsigned(bytes[0]),unsigned(bytes[1]));
}A nonblocking connect may finish immediately or report EINPROGRESS. Retaining its descriptor is not proof that it connected. Wait for writable/error readiness, then read SO_ERROR: zero means the transition succeeded, while a positive errno means it failed. Failure of getsockopt itself is a separate wrapper error. SO_ERROR is read-and-clear state, not a general connection health probe.
Use accept4 with SOCK_NONBLOCK|SOCK_CLOEXEC on Linux. Do not assume the new socket inherited those flags from the listener. An empty accept queue can report EAGAIN normally; it is not a reason to block the entire event loop.
A stream needs an application protocol
Store the number of bytes already transferred and bound any length the peer announces. A read can contain part of one record or several records. The existing Linux I/O overview has a length-prefixed frame reader; it complements these socket lifecycle exercises.
EOF is directional
shutdown(SHUT_WR) ends the local sending direction. The peer receives queued bytes before a zero read marks that direction's EOF. The reverse direction can remain open for a reply. Closing the descriptor releases the local endpoint's ownership; half-close and close are different operations.
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
int main() {
int p[2];assert(::socketpair(AF_UNIX,SOCK_STREAM,0,p)==0);assert(::send(p[0],"ok",2,MSG_NOSIGNAL)==2);
assert(::shutdown(p[0],SHUT_WR)==0);char buffer[16];auto a=::recv(p[1],buffer,2,MSG_WAITALL);auto b=::recv(p[1],buffer,1,0);
assert(::send(p[1],"!",1,MSG_NOSIGNAL)==1);auto c=::recv(p[0],buffer,1,MSG_WAITALL);
std::printf("%ld %ld %ld\n",long(a),long(b),long(c));::close(p[0]);::close(p[1]);
}The stream example uses socketpair to isolate half-close semantics; build tests also create real TCP listeners and clients. Both stay inside bubblewrap's private network namespace, so they need no internet endpoint or service running on the host. The tests verify loopback binding, byte order, connect completion, accepted-socket flags, refusal and a request/reply after half-close. They do not benchmark throughput or emulate a remote unreliable network.
Before the fill, distinguish a getsockopt failure from a successful query whose SO_ERROR value says ECONNREFUSED. Continue with Unix descriptor passing, which shares open resources rather than only application bytes.