C++ Networking
Reviewed & published by Brayan K
By the end of this lesson you'll understand how programs talk over a network in C++: when to choose TCP or UDP, the exact order of the BSD socket calls, how a TCP echo client and server exchange bytes, the difference between blocking and non-blocking I/O, and which higher-level libraries (Boost.Asio, POCO) save you from writing it all by hand.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Choose between TCP (reliable stream) and UDP (fast datagrams)
- Recite the BSD socket call order: socket, bind, listen, accept, connect, send, recv
- Walk through a TCP echo client and server and the bytes they exchange
- Explain blocking vs non-blocking I/O and why it changes how servers scale
- Avoid the classic traps: ignored return codes, byte order, partial reads, leaked sockets
- Know when to drop raw sockets for Boost.Asio or POCO
💡 Real-World Analogy
Think of a socket as a telephone. A TCP connection is a phone call: you dial (connect), the other side picks up (accept), and then every word you speak arrives in order with nothing dropped — the line guarantees it. UDP is more like posting postcards: you scribble a message and drop it in the box (one sendto), with no call, no guarantee it arrives, and no promise that two cards arrive in the order you sent them — but it's fast and cheap. The port number is the extension you're dialling on a shared building's switchboard (the IP address). Knowing which "phone behaviour" you need is the first decision in any networked program.
1. TCP vs UDP — picking the right pipe
Every networked program starts with one choice: do you need reliability or raw speed? TCP (Transmission Control Protocol) gives you a reliable, ordered byte stream — the OS resends lost packets and reassembles them in order, so what you send is exactly what arrives. UDP (User Datagram Protocol) skips all that bookkeeping: each datagram is fired off independently, may be lost, and may arrive out of order — but with almost no overhead. You pick the protocol when you create the socket: SOCK_STREAM for TCP, SOCK_DGRAM for UDP.
// ⚠️ This is a WORKED EXAMPLE to read — the live editor has no network,
// so socket calls are shown commented. Run it to see the explanation print.
#include <iostream>
using namespace std;
int main() {
// TCP = a phone call. You dial, a connection is established, then
// every word arrives in order, with nothing lost. The OS resends
// anything that goes missing. Reliable but slightly slower to set up.
//
// UDP = posting letters. You just drop a datagram in the box. No
// handshake, no guarantee of arrival or order — but very fast and
// light. Great for live video/voice/games that prefer fresh data
// over perfect data.
// How you ASK for each one when you create the socket:
// TCP: socket(AF_INET, SOCK_STREAM, 0); // stream = TCP
// UDP: socket(AF_INET, SOCK_DGRAM, 0); // datagram = UDP
// AF_INET means "IPv4 addresses". The third arg (protocol) is 0 = default.
cout << "TCP -> reliable, ordered byte STREAM (SOCK_STREAM)" << endl;
cout << "UDP -> fast, unordered DATAGRAMs (SOCK_DGRAM)" << endl;
cout << "Pick TCP for web/chat/files, UDP for live media/games" << endl;
// ✅ Expected output:
// TCP -> reliable, ordered byte STREAM (SOCK_STREAM)
// UDP -> fast, unordered DATAGRAMs (SOCK_DGRAM)
// Pick TCP for web/chat/files, UDP for live media/games
return 0;
}2. The BSD Sockets API — call order matters
Nearly every language's networking sits on top of the BSD sockets API — a small set of C functions that originated in Berkeley Unix and now run everywhere (Windows calls its near-identical version Winsock). The functions must be called in a specific order. A server goes socket → bind → listen → accept, then exchanges data and closes. A client is shorter: socket → connect, then exchange and close. The key surprise: accept() returns a brand-new socket dedicated to that one client, leaving the original socket free to accept the next.
// ⚠️ WORKED EXAMPLE — real socket calls are commented (no network in the
// editor). The point is the ORDER of calls. Run it to print the recap.
#include <iostream>
using namespace std;
// Real headers you would include on Linux/macOS:
// #include <sys/socket.h> // socket(), bind(), listen(), accept()...
// #include <netinet/in.h> // sockaddr_in, htons()
// #include <arpa/inet.h> // inet_pton()
// #include <unistd.h> // close()
int main() {
cout << "SERVER call order:" << endl;
// int s = socket(AF_INET, SOCK_STREAM, 0); // 1) make an endpoint
cout << " 1. socket() -> create the endpoint" << endl;
// bind(s, ...); // 2) claim ip:port
cout << " 2. bind() -> claim a port (e.g. 0.0.0.0:8080)" << endl;
// listen(s, 16); // 3) start queueing clients
cout << " 3. listen() -> mark socket as passive, queue size 16" << endl;
// int c = accept(s, ...); // 4) BLOCK until a client arrives
cout << " 4. accept() -> wait, return a NEW socket per client" << endl;
// recv(c, ...); send(c, ...); // 5) talk on that new socket
cout << " 5. recv()/send() -> exchange bytes with the client" << endl;
// close(c); close(s); // 6) always close both
cout << " 6. close() -> release the sockets" << endl;
cout << "\nCLIENT call order:" << endl;
// int s = socket(AF_INET, SOCK_STREAM, 0);
cout << " 1. socket() -> create the endpoint" << endl;
// connect(s, serverAddr, ...); // 2) reach out to the server
cout << " 2. connect() -> dial the server's ip:port" << endl;
// send(s, ...); recv(s, ...);
cout << " 3. send()/recv() -> exchange bytes" << endl;
// close(s);
cout << " 4. close() -> hang up" << endl;
return 0;
}3. A TCP Echo Server & Client
The "hello world" of sockets is an echo service: the server sends back whatever the client sends it. Read the server first — note how it checks every return code, sets SO_REUSEADDR, converts the port with htons(), and loops on recv() until the client closes. Then read the matching client. These can't run in the editor (no live network), so the expected exchange is written at the bottom of each example.
// ⚠️ WORKED EXAMPLE: a TCP ECHO server (sends back whatever it receives).
// Real socket code is shown but cannot run in the editor's sandbox —
// study the flow and the comments. The expected EXCHANGE is at the bottom.
#include <iostream>
#include <cstring>
using namespace std;
// On Linux/macOS also: <sys/socket.h> <netinet/in.h> <arpa/inet.h> <unistd.h>
int main() {
// 1) Create a TCP socket. Returns -1 on failure — ALWAYS check it.
// int srv = socket(AF_INET, SOCK_STREAM, 0);
// if (srv < 0) { perror("socket"); return 1; }
// 2) Allow instant reuse of the port after a restart (avoids TIME_WAIT).
// int yes = 1;
// setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
// 3) Describe the address to bind to. htons() fixes byte order for the port.
// sockaddr_in addr{};
// addr.sin_family = AF_INET;
// addr.sin_addr.s_addr = INADDR_ANY; // listen on every interface
// addr.sin_port = htons(8080); // host -> network byte order!
// if (bind(srv, (sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; }
// 4) Start listening; the 16 is the backlog of waiting connections.
// listen(srv, 16);
cout << "[server] listening on port 8080..." << endl;
// 5) accept() BLOCKS until a client connects, then returns a new socket.
// int cli = accept(srv, nullptr, nullptr);
// 6) Echo loop: read a chunk, send the SAME bytes straight back.
// char buf[1024];
// ssize_t n;
// while ((n = recv(cli, buf, sizeof(buf), 0)) > 0) { // n == 0 => client closed
// send(cli, buf, n, 0); // echo exactly n bytes back
// }
// 7) Tidy up — forgetting to close() leaks the descriptor.
// close(cli);
// close(srv);
cout << "[server] client done, sockets closed" << endl;
// ✅ Expected exchange once a client connects:
// client sends: "hello"
// server echoes: "hello" (the same 5 bytes, unchanged)
// client sends: "world"
// server echoes: "world"
return 0;
}Here's the other half. The client creates a socket, connect()s to the server's IP and port, sends a message, and reads the echoed reply. Notice that send() and recv() are not guaranteed to move all the bytes in one call — robust code loops, which we'll lean on in the exercises.
// ⚠️ WORKED EXAMPLE: the matching TCP ECHO client. Commented socket calls,
// runnable summary at the end. Pair it with the echo server above.
#include <iostream>
#include <cstring>
using namespace std;
int main() {
// 1) Create the socket and check the result.
// int s = socket(AF_INET, SOCK_STREAM, 0);
// if (s < 0) { perror("socket"); return 1; }
// 2) Point at the server. inet_pton converts "127.0.0.1" text -> bytes.
// sockaddr_in addr{};
// addr.sin_family = AF_INET;
// addr.sin_port = htons(8080); // same port as the server
// inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
// 3) connect() performs the TCP handshake. -1 means it failed.
// if (connect(s, (sockaddr*)&addr, sizeof(addr)) < 0) { perror("connect"); return 1; }
cout << "[client] connected to 127.0.0.1:8080" << endl;
// 4) Send a message. send() can send FEWER bytes than asked, so robust
// code loops until all bytes are written — shown simplified here.
// const char* msg = "hello";
// send(s, msg, 5, 0);
// 5) recv() returns whatever has arrived so far — maybe a partial reply.
// Loop until you have all you expect. n == 0 means the peer closed.
// char buf[1024];
// ssize_t n = recv(s, buf, sizeof(buf), 0);
// cout << "[client] got back: " << string(buf, n) << endl;
// 6) Close when done.
// close(s);
cout << "[client] echoed reply received, socket closed" << endl;
// ✅ Expected output (with the echo server running):
// [client] connected to 127.0.0.1:8080
// [client] got back: hello
// [client] echoed reply received, socket closed
return 0;
}Your turn — and this one runs. After recv() hands you raw bytes, the very first thing real servers do is parse them. Below is the first line of an HTTP request; split it into its three fields with an istringstream. Fill in the blank, then run it.
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main() {
// 🎯 YOUR TURN — runnable plain logic: parse one HTTP request line.
// A request line looks like: "GET /index.html HTTP/1.1"
// Network code does exactly this after recv() hands you raw text.
string requestLine = "GET /index.html HTTP/1.1";
// istringstream lets you pull words out with >> , splitting on spaces.
istringstream iss(requestLine);
string method, path, version;
// 1) Read the three fields IN ORDER into the variables above.
iss >> ___ >> ___ >> ___; // 👉 method, then path, then version
cout << "Method: " << method << endl;
cout << "Path: " << path << endl;
cout << "Version: " << version << endl;
// ✅ Expected output:
// Method: GET
// Path: /index.html
// Version: HTTP/1.1
return 0;
}4. Blocking vs Non-Blocking I/O
By default, socket calls are blocking: recv() pauses your thread until data shows up. That's easy to reason about, but a single slow client stalls the whole thread — so blocking servers typically spawn one thread per connection. Non-blocking sockets return immediately; if nothing is ready, recv() hands back -1 with errno == EWOULDBLOCK instead of waiting. That lets a single thread watch thousands of sockets using an OS "readiness" mechanism — epoll on Linux, kqueue on BSD/macOS, IOCP on Windows. More scalable, but more complex to write correctly.
// ⚠️ WORKED EXAMPLE — fcntl()/select() are commented (no live sockets).
// Run it to print the trade-off recap.
#include <iostream>
using namespace std;
// Real: #include <fcntl.h> #include <sys/select.h>
int main() {
// BLOCKING (the default): a call like recv() PAUSES your whole thread
// until data arrives. Simple to read, but one slow client stalls you,
// so a blocking server usually needs one thread per connection.
// recv(s, buf, len, 0); // sleeps here until bytes show up
// NON-BLOCKING: the call returns IMMEDIATELY. If nothing is ready,
// recv() returns -1 with errno == EWOULDBLOCK instead of waiting.
// You flip a socket non-blocking like this:
// int flags = fcntl(s, F_GETFL, 0);
// fcntl(s, F_SETFL, flags | O_NONBLOCK);
// Then ONE thread can juggle thousands of sockets by asking the OS
// "which of these are ready?" via select()/poll()/epoll()/kqueue().
cout << "blocking -> simple, 1 thread per client, stalls on slow peers" << endl;
cout << "non-blocking -> 1 thread, thousands of clients, more complex code" << endl;
cout << "scale up with: epoll (Linux), kqueue (BSD/macOS), IOCP (Windows)" << endl;
// ✅ Expected output:
// blocking -> simple, 1 thread per client, stalls on slow peers
// non-blocking -> 1 thread, thousands of clients, more complex code
// scale up with: epoll (Linux), kqueue (BSD/macOS), IOCP (Windows)
return 0;
}Another runnable exercise. Because TCP can split or merge your messages, real protocols frame each message with its length, like 5|hello. Build that frame from a payload — fill in the blank to write the length prefix, then run it to watch it decode again.
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main() {
// 🎯 YOUR TURN — runnable plain logic: build a length-prefixed "frame".
// Because recv() can split or merge messages, real protocols prefix the
// payload with its length, like: "5|hello" (5 bytes, then the text).
string payload = "hello";
// 1) Build the frame: the payload's size, then '|', then the payload.
ostringstream oss;
oss << ___ << "|" << payload; // 👉 payload.size() gives the length
string frame = oss.str();
cout << "Frame on the wire: " << frame << endl;
// Now decode it back, the way a receiver would:
istringstream iss(frame);
int len;
char sep;
iss >> len >> sep; // reads 5 and the '|'
string body;
getline(iss, body); // the rest is the payload
cout << "Declared length: " << len << ", body: " << body << endl;
// ✅ Expected output:
// Frame on the wire: 5|hello
// Declared length: 5, body: hello
return 0;
}5. Higher-Level Libraries: Boost.Asio & POCO
Writing raw sockets teaches you what's happening, but in real projects you'll usually reach for a library that smooths over the sharp edges — non-blocking I/O, timeouts, SSL/TLS, and the differences between POSIX and Windows. The two most common in C++ are below.
🔎 Deep Dive: when to leave raw sockets behind
Boost.Asio is the de-facto standard for high-performance async networking in C++ (and the basis for the proposed std::net). You hand it work and callbacks (or coroutines), and its io_context event loop drives them — no manual epoll juggling. Great when you need to scale to many concurrent connections or want SSL built in.
POCO (the POrtable COmponents library) offers friendlier, more object-oriented classes like StreamSocket and ServerSocket, plus ready-made HTTP client/server components. It's an easier on-ramp when you want "just give me a working server" without learning an async model first.
// Boost.Asio sketch (conceptual):
// boost::asio::io_context io;
// tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));
// acceptor.async_accept([](error_code ec, tcp::socket sock){ /* handle */ });
// io.run(); // the event loop drives all callbacks
// POCO sketch (conceptual):
// Poco::Net::ServerSocket srv(8080);
// Poco::Net::StreamSocket conn = srv.acceptConnection();
// conn.sendBytes(data, len); // friendlier, blocking by defaultRule of thumb: learn raw sockets once, then let Boost.Asio or POCO handle production. They also hide the Winsock-vs-POSIX gap, so your code stays portable.
Pro Tips
- 💡 Check every return code: socket(), bind(), connect(), and friends return -1 on failure. Pair them with perror(...) so you see why it failed.
- 💡 Always htons() the port: ports and other multi-byte numbers must be converted to network byte order before they go on the wire.
- 💡 Treat TCP as a stream: never assume one send() equals one recv(). Frame messages with a length prefix or a delimiter.
- 💡 Set SO_REUSEADDR: it lets your server rebind its port instantly after a restart instead of waiting out TIME_WAIT.
Common Errors (and the fix)
- Not checking return codes: calling bind() or connect() and pressing on regardless means later calls fail mysteriously. Every socket call can return -1 — check it, and call perror("bind") to print the reason.
- Wrong byte order: addr.sin_port = 8080; puts the bytes in your CPU's order, so the server ends up on a different port. Always wrap it: addr.sin_port = htons(8080); (and use htonl() for 32-bit values).
- Assuming a complete read: treating one recv() as one whole message drops or splits data, because TCP is a byte stream. Loop on recv() into a buffer until you have a full message (by length prefix or delimiter); remember recv() returning 0 means the peer closed.
- Forgetting to close: not calling close() (or closesocket() on Windows) leaks file descriptors; do it enough and the server hits "Too many open files" and stops accepting. Close both the per-client socket and the listening socket.
- "Address already in use" on restart: the previous socket is still in TIME_WAIT. Set SO_REUSEADDR with setsockopt() before bind().
📋 Quick Reference — Socket Call Order
| Step | Server | Client | Purpose |
|---|---|---|---|
| 1 | socket() | socket() | Create the endpoint |
| 2 | bind() | — | Claim an ip:port (port via htons) |
| 3 | listen() | — | Mark socket passive, queue clients |
| 4 | accept() | connect() | Establish the connection |
| 5 | recv() / send() | send() / recv() | Exchange bytes (loop!) |
| 6 | close() | close() | Release the socket(s) |
Mini-Challenge: Parse a Command Line
No blanks this time — just a brief and an outline. This is the exact job a server does after recv(): turn a line of text into structured fields. It runs in the editor, so build it, run it, and check your output against the comments.
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main() {
// 🎯 MINI-CHALLENGE: parse a "KEY VALUE" config/command line
// Servers often read simple text commands. Parse: "SET name Alice"
//
// 1. Put the line "SET name Alice" in a string.
// 2. Use an istringstream and >> to pull out three words:
// command (SET), key (name), value (Alice).
// 3. Print: command=SET key=name value=Alice
// 4. BONUS: if command == "SET", also print Stored name -> Alice
//
// ✅ Expected output:
// command=SET key=name value=Alice
// Stored name -> Alice
// your code here
return 0;
}🎉 Lesson Complete
- ✅ TCP is a reliable ordered stream (SOCK_STREAM); UDP is fast unreliable datagrams (SOCK_DGRAM)
- ✅ Server order: socket → bind → listen → accept; client: socket → connect; then send/recv/close
- ✅ accept() returns a new socket per client; a TCP echo service just sends back whatever it receives
- ✅ Blocking I/O is simple but needs a thread per client; non-blocking + epoll/kqueue scales to thousands on one thread
- ✅ Watch the traps: check return codes, htons() the port, loop on partial reads, and always close()
- ✅ For production, lean on Boost.Asio or POCO instead of hand-rolling sockets
Practice quiz
When should you choose TCP over UDP?
- When you need every byte to arrive in order without loss (web, files, chat)
- When you want the lowest possible latency for live video
- When you never care about ordering
- When you are sending a single broadcast packet
Answer: When you need every byte to arrive in order without loss (web, files, chat). TCP gives a reliable, ordered byte stream — ideal for web pages, file transfers, and chat where missing data breaks things.
Which socket type creates a TCP socket?
- SOCK_DGRAM
- SOCK_STREAM
- SOCK_RAW
- SOCK_SEQPACKET
Answer: SOCK_STREAM. TCP uses SOCK_STREAM (a reliable ordered stream); UDP uses SOCK_DGRAM (datagrams).
What is the correct server-side call order in the BSD sockets API?
- socket, connect, send, recv
- bind, socket, listen, accept
- socket, bind, listen, accept
- listen, bind, socket, accept
Answer: socket, bind, listen, accept. A server goes socket -> bind -> listen -> accept, then exchanges data and closes.
What does accept() return when a client connects?
- The same listening socket, now connected
- A brand-new socket dedicated to that one client
- The client's IP address as a string
- An error code only
Answer: A brand-new socket dedicated to that one client. accept() returns a new socket for that client, leaving the original listening socket free to accept the next.
What does htons() do, and why is it needed for a port?
- It hashes the port for security
- It converts a 16-bit value from host byte order to network (big-endian) order
- It resolves a hostname to an IP
- It opens the port in the firewall
Answer: It converts a 16-bit value from host byte order to network (big-endian) order. htons ('host to network short') converts a 16-bit value like a port to network byte order; skipping it corrupts the port on the wire.
Why might recv() return only part of a message?
- TCP is a byte stream, not a message queue — one send can arrive across several recv calls
- recv() is buggy and should be avoided
- The OS limits recv() to 1 byte
- Because UDP guarantees full messages but TCP does not exist
Answer: TCP is a byte stream, not a message queue — one send can arrive across several recv calls. TCP is a stream: sends can split or merge, so you must loop on recv() into a buffer until you have a complete message.
Parsing the request line 'GET /index.html HTTP/1.1' with iss >> method >> path >> version yields which path value?
- GET
- /index.html
- HTTP/1.1
- index.html
Answer: /index.html. >> splits on whitespace into three fields: method=GET, path=/index.html, version=HTTP/1.1.
How does a non-blocking recv() behave when no data is ready?
- It blocks until data arrives
- It returns -1 with errno == EWOULDBLOCK instead of waiting
- It returns 0 and closes the socket
- It throws a C++ exception
Answer: It returns -1 with errno == EWOULDBLOCK instead of waiting. A non-blocking call returns immediately; if nothing is ready, recv() returns -1 with errno EWOULDBLOCK.
Why does a server fail to restart with 'Address already in use'?
- The port number is invalid
- The previous socket is still in TIME_WAIT; set SO_REUSEADDR before bind()
- Another program permanently owns the port
- You forgot to call listen()
Answer: The previous socket is still in TIME_WAIT; set SO_REUSEADDR before bind(). TCP keeps the port in TIME_WAIT after close; setting SO_REUSEADDR with setsockopt() before bind() lets you reuse it immediately.
Which OS readiness mechanism lets one thread watch thousands of sockets on Linux?
- epoll
- kqueue
- IOCP
- select only
Answer: epoll. epoll is the Linux mechanism; kqueue is BSD/macOS and IOCP is Windows. They let a single thread scale to many connections.
Continue this course
- Previous: Inline Assembly & Low-Level CPU Instructions (Intro)
- Next: Writing High-Performance Code: Cache Lines, Branch Prediction, SIMD — Optimise for CPU cache, branch prediction, and SIMD vectorisation
- Quick reference: C++ cheat sheet
Frequently asked questions
Should I use TCP or UDP?
Use TCP when you need every byte to arrive in order without loss — web pages, file transfers, chat, anything where a missing message breaks things. TCP handles retransmission and ordering for you. Use UDP when speed matters more than perfection and you can tolerate the odd lost packet — live video, voice, and fast-paced games often pick UDP and handle any gaps themselves.
Why does my recv() only return part of the message?
TCP is a byte stream, not a message queue. One send() of 1000 bytes can arrive as several recv() calls, and two sends can arrive merged into one recv(). recv() returns 'whatever has arrived so far'. You must loop, appending to a buffer, until you have a complete message — usually marked by a length prefix or a delimiter like a newline.
What does htons() actually do?
htons means 'host to network short'. Networks agree to send multi-byte numbers big-endian (most significant byte first), but your CPU may store them little-endian. htons() converts a 16-bit value (like a port) from your machine's order to network order; htonl() does the same for 32-bit values. Skipping them is why a port like 8080 sometimes turns into a nonsense number on the wire.
Why does my server fail to restart with 'Address already in use'?
After a socket closes, TCP keeps the port in a TIME_WAIT state for up to a minute so stray packets can drain. A fresh bind() to the same port is rejected during that window. Set the SO_REUSEADDR option with setsockopt() before you bind(), and the OS will let you reuse the port immediately.
Should I write raw socket code or use a library?
Learn the raw BSD sockets API once so you understand what is really happening — it makes every higher-level tool make sense. For real projects, reach for a library like Boost.Asio or POCO. They handle non-blocking I/O, timeouts, SSL, and cross-platform differences (Windows Winsock vs POSIX) that are tedious and error-prone to get right by hand.