CodeRigor

FREE LESSON

TCP sockets, connection state & half-close

Bind a loopback listener to an ephemeral port, complete a nonblocking connect with SO_ERROR, accept safely, and distinguish TCP byte streams, EOF and half-close.

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.

A nonblocking connect has an intermediate state
A returned socket is not proof of connectivity. After writable/error readiness, SO_ERROR decides the completion transition.
A nonblocking connect has an intermediate state
A returned socket is not proof of connectivity. After writable/error readiness, SO_ERROR decides the completion transition.
C++
#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.

TCP carries a byte sequence, not send boundaries
Two sends of abc and def may be read as ab, cdef. Every receive loop must keep a byte offset or a framing parser.
TCP carries a byte sequence, not send boundaries
Two sends of abc and def may be read as ab, cdef. Every receive loop must keep a byte offset or a framing parser.

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.

Half-close ends one direction
After the client shuts down writing, the server drains request bytes and sees EOF. The server can still send a response to the client’s receive side.
Half-close ends one direction
After the client shuts down writing, the server drains request bytes and sees EOF. The server can still send a response to the client’s receive side.
C++
#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.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Review the socket/bind/listen/accept and client/server lifecycle. This lesson adds C++ ownership, private loopback tests, nonblocking connect completion, atomic accepted-socket flags and half-close.