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

💡 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 default

Rule 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

Common Errors (and the fix)

📋 Quick Reference — Socket Call Order

StepServerClientPurpose
1socket()socket()Create the endpoint
2bind()—Claim an ip:port (port via htons)
3listen()—Mark socket passive, queue clients
4accept()connect()Establish the connection
5recv() / send()send() / recv()Exchange bytes (loop!)
6close()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

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

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.