Networking — how data reaches the server
In C++ code, a network request looks deceptively simple: one connect(), one send(). But behind that call lies an entire pipeline — DNS name resolution to an address, TCP three-way handshake, TLS handshake, packet routing, kernel buffers. When a connection "hangs", "sometimes fails", or "works but slowly", the cause is almost always this invisible layer.
The network is built in layers: each level adds its own header and guarantee, relying on the layer below. The application protocol (HTTP) knows nothing about routing; transport (TCP) knows nothing about request content. Understanding networking means understanding which layer is responsible for what and precisely where what you are observing breaks.
Why does a C++ developer need this? Backend service, game server, embedded device — everywhere you work directly with sockets, and abstractions are thinner than in high-level languages. You can only spot TIME_WAIT, port exhaustion, broken PMTUD, or a race on shutdown of a shared socket if you know the mechanics.
Network models and addressing
The reference OSI model describes 7 layers; the practical TCP/IP model has 4. The TCP/IP application layer merges OSI layers L5+L6+L7. TLS does not fit neatly into this scheme — it sits logically between transport (L4) and application (L7), so it is often called "L4.5".
An IP address identifies a node on the network. IPv4 is 32 bits (4 bytes), IPv6 is 128 bits. A subnet mask divides an address into network and host portions. IPv6 solves IPv4's main problem — address space exhaustion — and also simplifies headers and auto-configuration.
// An IP address is a number, and network byte order is big-endian.
// A host may be little-endian — convert explicitly:
uint32_t addr_host = ntohl(packet_addr); // network → host
uint16_t port_net = htons(8080); // host → network
Several important addressing details:
- NAT translates many private addresses into one public address — it delayed IPv4 exhaustion. Private ranges (RFC 1918):
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16. Home routers technically do PAT — port address translation too. NAT breaks P2P: a node behind NAT cannot receive inbound connections by default without port forwarding. - Port is a 16-bit number. A connection is determined not by a single port but by a 5-tuple (protocol, source IP and port, destination IP and port) — so one port
80serves many concurrent connections from different clients. Ports< 1024require privilege (or theCAP_NET_BIND_SERVICEcapability). TCP and UDP port spaces are independent. - Multicast in IPv4 is the range
224.0.0.0/4(up to239.255.255.255); "multicast" is encoded in the destination address itself, not a flag. - MTU — the maximum frame size at the link layer, typically 1500 bytes, but VPNs and tunnels shrink it.
MSS = MTU − IP and TCP header sizes. If a packet exceeds MTU, fragmentation occurs; blocking all ICMP on a firewall breaks Path MTU Discovery.
TCP and UDP
These are two transport-layer protocols with opposite design goals.
TCP — connection-oriented: reliable, ordered delivery of a byte stream, with acknowledgments, retransmission, flow control, and congestion control. UDP — connectionless: datagrams are delivered "best-effort", with no guarantees on order or delivery, and no connection overhead.
UDP is not "useless due to unreliability": QUIC (and thus HTTP/3) is built on top of UDP, adds reliability itself, and beats TCP on packet loss. UDP is noticeably faster than TCP only for high frequencies of tiny packets; for bulk transfers TCP is not slower. TCP is strictly point-to-point — broadcasting and multicast require UDP.
TCP manages a connection through flags in the segment header:
SYN— request to open a connection (sequence number synchronization).ACK— acknowledgment of received data.FIN— graceful closure of one direction.RST— immediate one-way abort.FINis polite "goodbye";RSTis "connection severed".PSH— hint to "deliver data to the application immediately", not a kernel buffer flush flag.URG— mark urgent data (rarely used in practice).
TCP connection
TCP opens a connection with a three-way handshake: client sends SYN, server replies SYN+ACK, client confirms with ACK. Data can only be sent after all three steps complete — not after the first SYN.
Within a connection, each byte is numbered. The acknowledgment number (ACK) is the sequence number of the next expected byte, not the last received. The connection closes with FIN/ACK exchanges from both sides; the side that closes actively enters TIME_WAIT for ~2×MSL. TIME_WAIT is not a bug but a safeguard: it ensures that stray segments from the old connection do not land in a new one. The real problem is port exhaustion from many short-lived connections.
Congestion control and flow control
These are two separate mechanisms, and they are constantly confused:
- Flow control protects the receiver. The receiver advertises a receive window
rwnd— how many bytes it is ready to accept. The sender must not overflow the receiver's buffer. - Congestion control protects the network. The sender estimates a congestion window
cwndbased on packet loss and delay. "Slow start" despite its name ramps upcwndexponentially and is quite aggressive.
The sender transmits no more than min(cwnd, rwnd) at any time — either window can be the bottleneck.
Sockets
A socket is an endpoint of a connection, represented by a file descriptor. Server pattern: socket() → bind() (bind to address and port) → listen() → accept(). Client: socket() → connect().
int fd = socket(AF_INET, SOCK_STREAM, 0); // TCP socket
int yes = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes); // see below
bind(fd, /* sockaddr */ ...);
listen(fd, SOMAXCONN);
Two interview traps. First is SO_REUSEADDR: without it, after a server restart the port stays in TIME_WAIT for ~2 minutes and bind() fails. Second is that recv() returns byte count, not a C string: you must null-terminate yourself or work by counter.
Separate issue: shutdown() vs close() on a socket shared via fork(). close() only decrements the reference count on the file descriptor in the current process — while another process holds a copy, the connection lives. shutdown() acts on the shared socket itself: it closes the specified direction and the peer immediately sees FIN.
Scaling to 100,000 connections
select() and poll() iterate the full file descriptor set on each call — O(n) complexity, hitting a wall (select historically limited to ~1024). At scale you need epoll (Linux) or kqueue (BSD) — they work in O(1) on active events. A thread-per-connection does not scale: thousands of threads drown the scheduler in context switches. And do not forget kernel limits — ulimit -n, ephemeral port range, socket buffer sizes.
HTTP
HTTP is a request-response application protocol over TCP. A request carries a method, and methods differ by two properties:
- Safe — does not change server state:
GET. - Idempotent — repeating it yields the same result:
GET,PUT,DELETEare idempotent;POSTis not.
You cannot use GET for state-changing operations — caches and prefetchers will execute it at the wrong time. PUT is full resource replacement, PATCH is partial modification.
HTTPS is HTTP over TLS. Important: HTTPS encrypts the channel, but does not make the server on the other end trustworthy — a compromised node could be there. Without the Strict-Transport-Security (HSTS) header, a browser can be downgraded to HTTP on the first visit.
Web authentication:
- A cookie with the
HttpOnlyflag is unavailable to JavaScript — protection against theft via XSS. - JWT in
localStorageshould not be stored — XSS will steal it; plan revocation strategy in advance.
CORS is a browser policy: a page from one origin cannot talk to another by default. For a "complex" request the browser sends a preflight OPTIONS; the server must handle it. Access-Control-Allow-Origin: * together with Allow-Credentials: true is forbidden — you must name the origin explicitly. And CORS does not defend against CSRF — use SameSite-cookies or CSRF tokens for that.
HTTP/2 and HTTP/3
HTTP/1.1 processes one request per connection at a time. HTTP/2 multiplexes many streams over one TCP connection — but head-of-line blocking remains at the TCP level: loss of one segment blocks all streams. HTTP/3 fixes this by replacing the transport: it runs on QUIC over UDP, where streams are independent. The cost is that some networks throttle or block UDP — HTTP/3 is not always faster.
TLS
TLS encrypts a connection and authenticates the server (the term "SSL" is obsolete — SSL 2.0/3.0 are broken; "SSL certificate" is really an X.509 certificate).
The TLS handshake agrees on a cipher suite, exchanges keys, and verifies the server certificate through a chain of trust to a root CA. TLS 1.2 took 2 round-trips; TLS 1.3 cut the handshake to 1-RTT and removed obsolete cipher suites.
TLS 1.3 added 0-RTT — sending data in the first packet when resuming a session. 0-RTT data is encrypted, so calling it "insecure" is imprecise — the specific issue is that it can be replayed. So 0-RTT is unsafe for non-idempotent requests (POST that changes state). Also, early data in 0-RTT does not provide forward secrecy.
Separately, certificate revocation: a compromised certificate remains trusted until it expires unless you check status via OCSP or CRL.
Application protocols
DNS translates a name into an IP address. Resolution is recursive: a resolver queries the root server, then the TLD server, then the authoritative domain server; the result is cached for TTL. Long TTL at deploy time means stale records; hardcoding IPs "to skip DNS" breaks when infrastructure changes.
WebSocket gives a persistent two-way connection. It starts as a normal HTTP request with an Upgrade header, then the connection transitions to full-duplex mode. Proxies and load balancers close idle connections — so you need ping/pong heartbeat frames.
REST vs gRPC. REST over HTTP with JSON is human-readable, simple to debug, and browser-friendly. gRPC uses binary Protobuf with a schema and runs over HTTP/2 — more compact and faster, but limited in browsers (needs gRPC-Web) and payload is not human-readable.
Load balancing comes in two flavors. An L4 balancer distributes by transport (IP and port) — fast, but does not see content. An L7 balancer parses the application layer (HTTP), so it can route by path or header and do canary deployments — but sees plaintext, so TLS termination happens right there.
Common mistakes and traps
| Mistake | Consequence |
|---|---|
| IP address as a number without byte order | Garbage address — need htonl/ntohl |
Missing SO_REUSEADDR on server socket | bind() fails ~2 minutes after restart (TIME_WAIT) |
Treating recv() output as a C string | Reading past buffer bounds — recv returns byte count |
Treating TIME_WAIT as a bug | It is a safeguard; the real issue is port exhaustion |
Sending data right after SYN | Data only flows after complete three-way handshake |
Confusing cwnd with rwnd | Network congestion ≠ receiver buffer overflow |
select/poll with tens of thousands of connections | O(n) per call, ceiling ~1024 — need epoll/kqueue |
Using GET for state-changing operations | Caches and prefetchers will execute it at the wrong time |
JWT in localStorage | Token theft via XSS |
Allow-Origin: * with Allow-Credentials: true | Forbidden by spec — must name the origin |
| Treating CORS as defense against CSRF | It is not — use SameSite-cookies or CSRF tokens |
| 0-RTT for non-idempotent requests | Replay attack risk — repeat alters state |
Thinking HTTP/2 ended head-of-line blocking | TCP-level blocking remains; only HTTP/3 solves it |
Interview relevance
Networking is a mandatory topic for backend and systems C++ interviews. Interviewers check not trivia about ports and acronyms, but comprehension: which layer is responsible for what, what guarantees does transport give, and where is the delay hidden.
What the interviewer checks:
- The difference between TCP and UDP and a reasoned choice between them
- Three-way handshake,
TIME_WAIT, byte numbering - Flow control vs congestion control (
rwndvscwnd) - Socket API and
shutdownvscloseon a shared socket - Scaling to tens of thousands of connections (
epoll, kernel limits) - HTTP: method idempotency, HTTPS vs HTTP, HTTP/1.1 → 2 → 3 evolution
- TLS handshake, what changed in TLS 1.3, and the risk of 0-RTT
Common questions:
- How does TCP differ from UDP and when do you choose UDP?
- How does the three-way handshake work and why do we need
TIME_WAIT? - How does flow control differ from congestion control?
- How does
shutdown()differ fromclose()on a socket afterfork()? - How do you scale a server to 100,000 connections?
- How does the TLS 1.3 handshake work and what is the danger of 0-RTT?
Typical mistake: listing protocols and commands without explaining the mechanics and layers. The interviewer is looking for understanding of layer boundaries, transport guarantees, and where delay or failure actually arises.