Skip to main content
    Courses/C++/C++ Networking

    Lesson • Advanced

    C++ Networking

    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.

    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.

    Worked example: TCP vs UDP

    Read the comments, run it, and see when each protocol wins.

    Try it Yourself »
    C++
    // ⚠️ 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
    ...

    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: the socket call order

    The exact sequence for server and client, with each call explained.

    Try it Yourself »
    C++
    // ⚠️ 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 ord
    ...

    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: TCP echo SERVER

    socket → bind → listen → accept → echo loop → close, fully commented.

    Try it Yourself »
    C++
    // ⚠️ 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)
    ...

    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: TCP echo CLIENT

    socket → connect → send → recv → close, the mirror of the server.

    Try it Yourself »
    C++
    // ⚠️ 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 = A
    ...

    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.

    🎯 Your turn: parse a request line

    Pull method, path, and version out of a request line with >>. Runs in the editor.

    Try it Yourself »
    C++
    #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;
    
    
    ...

    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: blocking vs non-blocking

    The trade-off, and how to flip a socket non-blocking with fcntl().

    Try it Yourself »
    C++
    // ⚠️ 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 byt
    ...

    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.

    🎯 Your turn: length-prefix a message

    Prefix the payload with its size so the receiver knows where it ends. Runs in the editor.

    Try it Yourself »
    C++
    #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;   // 👉 payloa
    ...

    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

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

    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)

    Frequently Asked Questions

    Q: 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.

    Q: 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.

    Q: 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.

    Q: 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.

    Q: 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.

    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.

    🎯 Mini-Challenge: parse a KEY VALUE command

    Split 'SET name Alice' into command, key, and value with istringstream.

    Try it Yourself »
    C++
    #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 
    ...

    🎉 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
    • Next lesson: High-Performance Code — squeeze maximum speed out of your C++

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service