Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 20 – Systems programming with C++

TCP sockets in C++: client and server on localhost

Write a TCP server and client in C++ with POSIX sockets on 127.0.0.1, give every socket an RAII owner and frame messages on a byte stream.

  • Advanced
  • 30 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 11 of 30 minutes free
Free: The calls, in order · An echo server and client on 127.0.0.1 · Localhost only, on purpose · 2 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 19 minutes.

What you will learn

  • Write a TCP server and client with POSIX sockets
  • Design framing for messages on a byte stream
  • Limit network experiments to the loopback address

Before you start

A TCP server in C++ is a handful of POSIX calls. socket creates an endpoint, bind gives it an address and a port, listen lets it take connections, and accept waits for a client and returns a new socket for that one connection. A client calls socket and then connect. After that, both sides move bytes with send and recv, and close ends the connection. Bound to 127.0.0.1, the loopback address, the server can be reached only from the same machine, which is exactly what experiments and tests need.

Two facts shape the rest. TCP carries a stream of bytes, not of messages, so every protocol on top of it needs framing to mark where one message ends. And every call can fail or move fewer bytes than asked, so each socket gets an owner that closes it and each transfer gets a loop. This lesson runs every server and client on the loopback interface of one Mac, on both compilers; nothing reaches another machine.

The calls, in order

A server calls socket, bind, listen and accept; a client calls socket and connect; they send and receive, then shut down and close.ServerClient1. socket(), bind(127.0.0.1, port 0),listen()2. accept() waits3. socket(), connect():the TCP handshake4. accept() returns a newsocket for this client5. send("hello")6. recv(), then send("HELLO")7. shutdown(SHUT_WR):the server's recv() returns 08. close(): the client'srecv() returns 0

The calls of a TCP server and client, in order

Text description of the diagram

A sequence diagram with two lifelines, the server on the left and the client on the right, read from top to bottom.

  1. The server calls socket(), then bind() with the address 127.0.0.1 and port 0, so the system picks a free port, then listen().
  2. The server calls accept(), which waits until a client connects.
  3. The client calls socket() and then connect(). An arrow to the server marks the TCP handshake, which happens inside connect().
  4. On the server, accept() returns a new socket for this one client; the listening socket stays open for the next one.
  5. The client sends "hello".
  6. The server receives it with recv() and sends back "HELLO".
  7. The client calls shutdown(SHUT_WR): it will send nothing more, so the server's next recv() returns 0.
  8. The server closes the connection with close(), and the client's next recv() returns 0.

A socket is a file descriptor, so the move-only FileDescriptor from the previous lesson owns it. net.hpp wraps the calls in four helpers: listen_on_loopback (socket, bind and listen), connect_to_loopback, send_all, which loops until everything is sent, and recv_exactly, which loops until it has as many bytes as asked for.

net.hpp and echo.cpp

C++ header · net.hpp

// net.hpp: TCP sockets on the loopback address 127.0.0.1, each owned by a FileDescriptor (fd.hpp).
#pragma once
#include "fd.hpp"

#include <cstdint>
#include <string>
#include <string_view>

#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>

// The address 127.0.0.1:port. The loopback interface never leaves this machine.
inline sockaddr_in loopback(std::uint16_t port) {
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);                   // network byte order: most significant byte first
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // 127.0.0.1
    return addr;
}

// socket, bind and listen. Port 0 asks the system for any free port; local_port() says which one it chose.
inline std::expected<FileDescriptor, std::error_code> listen_on_loopback(std::uint16_t port = 0, int backlog = 16) {
    FileDescriptor s(::socket(AF_INET, SOCK_STREAM, 0));
    if (!s) return std::unexpected(last_error());
    int on = 1;
    ::setsockopt(s.get(), SOL_SOCKET, SO_REUSEADDR, &on, sizeof on);
    sockaddr_in addr = loopback(port);
    if (::bind(s.get(), reinterpret_cast<const sockaddr*>(&addr), sizeof addr) != 0) return std::unexpected(last_error());
    if (::listen(s.get(), backlog) != 0) return std::unexpected(last_error());
    return s;
}

inline std::uint16_t local_port(int fd) {
    sockaddr_in addr{};
    socklen_t len = sizeof addr;
    ::getsockname(fd, reinterpret_cast<sockaddr*>(&addr), &len);
    return ntohs(addr.sin_port);
}

inline std::expected<FileDescriptor, std::error_code> connect_to_loopback(std::uint16_t port) {
    FileDescriptor s(::socket(AF_INET, SOCK_STREAM, 0));
    if (!s) return std::unexpected(last_error());
    sockaddr_in addr = loopback(port);
    if (::connect(s.get(), reinterpret_cast<const sockaddr*>(&addr), sizeof addr) != 0) return std::unexpected(last_error());
    return s;
}

// send() may send only part of the data: keep going. MSG_NOSIGNAL turns a write to a closed connection into the
// error EPIPE instead of the signal SIGPIPE, which would end the program.
inline std::error_code send_all(int fd, std::string_view data) {
    while (!data.empty()) {
        ssize_t n = ::send(fd, data.data(), data.size(), MSG_NOSIGNAL);
        if (n < 0) {
            if (errno == EINTR) continue;
            return last_error();
        }
        data.remove_prefix(static_cast<std::size_t>(n));
    }
    return {};
}

// Reads exactly n bytes. Returns fewer only if the other side closed the connection first.
inline std::expected<std::string, std::error_code> recv_exactly(int fd, std::size_t n) {
    std::string data(n, '\0');
    std::size_t got = 0;
    while (got < n) {
        ssize_t r = ::recv(fd, data.data() + got, n - got, 0);
        if (r == 0) break; // the connection was closed
        if (r < 0) {
            if (errno == EINTR) continue;
            return std::unexpected(last_error());
        }
        got += static_cast<std::size_t>(r);
    }
    data.resize(got);
    return data;
}

C++ · echo.cpp

// echo.cpp: a TCP server and a client in one program, talking over 127.0.0.1.
#include "net.hpp"

#include <cctype>
#include <format>
#include <print>
#include <string>
#include <thread>
#include <vector>

int main() {
    auto listener = listen_on_loopback(); // socket, bind to 127.0.0.1 with port 0, listen
    if (!listener) {
        std::println("listen: {}", listener.error().message());
        return 1;
    }
    std::uint16_t port = local_port(listener->get());
    std::vector<std::string> log; // what the server saw, printed at the end

    std::jthread server([&] {
        // accept waits for a client and returns a new socket for that one connection.
        FileDescriptor conn(::accept(listener->get(), nullptr, nullptr));
        log.push_back("accepted a connection");
        std::size_t total = 0;
        char buffer[1024];
        for (ssize_t n; (n = ::recv(conn.get(), buffer, sizeof buffer, 0)) > 0;) {
            for (ssize_t i = 0; i < n; ++i) buffer[i] = static_cast<char>(std::toupper(static_cast<unsigned char>(buffer[i])));
            (void)send_all(conn.get(), std::string_view(buffer, static_cast<std::size_t>(n)));
            total += static_cast<std::size_t>(n);
        }
        log.push_back(std::format("recv() returned 0 after {} bytes, so the client has finished sending", total));
    }); // conn is closed when the thread's function returns

    auto client = connect_to_loopback(port); // the TCP handshake happens inside connect
    if (!client) {
        std::println("connect: {}", client.error().message());
        return 1;
    }
    for (std::string_view message : {"hello", "sockets on localhost"}) {
        (void)send_all(client->get(), message);
        auto reply = recv_exactly(client->get(), message.size());
        std::println("client sent {:?}, got back {:?}", message, reply.value_or("?"));
    }
    ::shutdown(client->get(), SHUT_WR); // no more data from the client: the server's recv() returns 0
    auto rest = recv_exactly(client->get(), 1);
    std::println("client: recv() returned {} bytes, so the server has closed the connection", rest.value_or("?").size());
    server.join();
    for (const std::string& line : log) std::println("server: {}", line);
    std::println("port chosen by the system: {}", port != 0 ? "a free one, different on every run" : "none");
}

An echo server and client on 127.0.0.1

A server thread and a client in one program, on both compilers Python · echo_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined echo.cpp -o echo && ./echo
g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined echo.cpp -o echo-gcc && ./echo-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined echo.cpp -o echo && ./echo
client sent "hello", got back "HELLO"
client sent "sockets on localhost", got back "SOCKETS ON LOCALHOST"
client: recv() returned 0 bytes, so the server has closed the connection
server: accepted a connection
server: recv() returned 0 after 25 bytes, so the client has finished sending
port chosen by the system: a free one, different on every run
$ g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined echo.cpp -o echo-gcc && ./echo-gcc
client sent "hello", got back "HELLO"
client sent "sockets on localhost", got back "SOCKETS ON LOCALHOST"
client: recv() returned 0 bytes, so the server has closed the connection
server: accepted a connection
server: recv() returned 0 after 25 bytes, so the client has finished sending
port chosen by the system: a free one, different on every run

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 echo_session.py

The server runs in a std::jthread and the client in main, in one process, which keeps the run self-contained. Port 0 asks the system for any unused port, and getsockname tells the client which one it got. The client sends two messages and reads back exactly as many bytes as it sent, because a single recv may return less. Then it calls shutdown(SHUT_WR): it will send nothing more, so the server’s recv returns 0, the server closes its socket, and the client’s next recv returns 0 in turn. recv returning 0 is how both sides learn that the other has finished. The listening socket stays open the whole time, ready to accept the next client while the first one is served.

Port numbers and addresses travel in network byte order, most significant byte first, which is why htons and htonl convert them, while this Mac’s arm64 processor stores numbers least significant byte first.

Programmer Calculator (Bitwise) Enter 8080 at 16 bits and swap its bytes: 0x1F90 becomes 0x901F, the order htons() produces on a little-endian Mac.

Localhost only, on purpose

Bind experiments to 127.0.0.1

INADDR_LOOPBACK is 127.0.0.1; INADDR_ANY, written 0.0.0.0, means every network interface of the machine. A test server bound to the second can be reached by other computers on the same network, a café’s or an office’s included, unless a firewall stops them. Bind to the loopback address unless the program really must be reached from elsewhere.

The loopback interface delivers packets inside the machine without touching a network card. RFC 1122 reserves all of 127.0.0.0/8 for it, and packets for those addresses never appear on a network. Port 0 is the right default for tests as well: two test runs at the same time can never fight over one fixed port.

A stream of bytes, not of messages

TCP guarantees that the bytes arrive in order and without gaps, and says nothing about where one message ends. Two send calls can arrive in one recv, and one send can arrive in several, whatever sizes the sender used. On the loopback interface, small messages usually arrive exactly as they were sent, so code that treats one recv as one message passes every local test and fails on a real network. A protocol therefore marks its message boundaries itself, in one of four common ways:

  • A length prefix: a fixed-size number before each message says how many bytes follow.
  • A delimiter: a byte that cannot occur inside a message, such as a newline, ends each one.
  • A fixed size: every message has the same length, as in some binary protocols.
  • Self-describing headers: HTTP/1.1 sends header lines, and Content-Length says how long the body is.

Check yourself

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 What does accept() return on a listening socket?

    Choose one answer.

    Show the answer to question 1

    Answer: A new socket for the one connection it accepted; the listening socket stays open for the next client

    Each accepted connection gets a socket of its own, which the server reads, writes and closes. The listening socket keeps waiting for further clients until the server closes it.

  2. Question 2 of 5 A test server binds to INADDR_LOOPBACK (127.0.0.1) instead of INADDR_ANY (0.0.0.0). What changes?

    Choose one answer.

    Show the answer to question 2

    Answer: Only programs on the same machine can connect to it

    The loopback address exists only inside the machine, so no other computer can reach the server. 0.0.0.0 means every interface, so the server is reachable from the network unless a firewall stops it.

  3. Question 3 of 5 A client runs the two lines below on a connected TCP socket. The server then calls recv(fd, buf, 1024, 0) once. What can that call return?

    Read the code, then choose one answer.

    send(fd, "HELLO", 5, 0);
    send(fd, "WORLD", 5, 0);
    Show the answer to question 3

    Answer: Anything from 1 to 10 bytes, so the server cannot tell where HELLO ends without framing

    TCP delivers a stream of bytes in order and keeps no message boundaries. The lesson's run showed both cases: two sends arriving in one recv, and one send arriving in several.

  4. Question 4 of 5 recv() on a connected TCP socket returns 0. What does that mean?

    Choose one answer.

    Show the answer to question 4

    Answer: The other side has finished sending, with shutdown(SHUT_WR) or close()

    0 is the end of the stream. A socket with no data yet makes recv wait (or fail with EAGAIN if it is non-blocking), and an error is -1 with errno set.

  5. Question 5 of 5 HTTP/1.1 sends header lines, and a Content-Length header says how many bytes of body follow. Which kind of framing is that?

    Choose one answer.

    Show the answer to question 5

    Answer: Self-describing headers

    The header lines end with a delimiter (an empty line), and the body's length comes from the headers themselves, which is why such framing is called self-describing.

Length-prefixed framing, with merged and split messages reproduced Pro: opens with a Pro, Premium or Ultimate pass

Framing bugs hide on localhost, where small messages arrive whole. This part forces two messages into one recv and one message into many, then builds a frame reader that survives both and refuses frames that would exhaust memory.

In this part:

  • Merged and split messages, reproduced on purpose
  • A frame reader that survives both
  • Seven ways a stream can cut a frame, tested
  • Choosing the length field: size, byte order and limits

5 runnable examples (C++, C++ header, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Refused, in use, TIME_WAIT and SIGPIPE reproduced; then a key-value server Pro: opens with a Pro, Premium or Ultimate pass

Four failures come up in almost every program that uses sockets. Each is reproduced here on 127.0.0.1 with the reason and the fix, and the part ends with an exercise: a small key-value server that speaks length-prefixed frames.

In this part:

  • Four failures every socket program meets
  • Your turn: a key-value server on 127.0.0.1

4 runnable examples (C++, C++ header, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    Why should a development or test server bind to 127.0.0.1 rather than 0.0.0.0?

    127.0.0.1 is the loopback address: packets to it never leave the machine, so only programs on the same computer can connect. 0.0.0.0, INADDR_ANY, means every network interface, so a server bound to it can be reached by other computers on the same network, such as a café's or an office's Wi-Fi, unless a firewall stops them. A server under development usually has no authentication and may expose debug endpoints, so binding to the loopback address keeps it private by default. Tests should also bind to port 0, which asks the system for any unused port, so that two test runs never compete for one fixed port. A server that must be reachable from other machines binds to a specific interface address or to 0.0.0.0 on purpose, with authentication in place.

  2. Mid to senior

    A C++ service and its client exchange JSON messages over TCP. Every test on one machine passes, but in production some messages arrive merged with the next one or cut in half. Explain why, and design the fix and the tests that prove it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A C++ server disappears a few times a day without a log line or a core dump, usually when clients on mobile networks disconnect in the middle of a large reply. Its exit status is 141. What happened, and how do you fix it?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    Your server reads a 4-byte length and then that many bytes of payload. A security review says that a single client can take the server down. Name at least three ways it can, and design the defences.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A server calls socket, bind, listen and accept; a client calls socket and connect; both then send and recv and finally close. Each socket is a file descriptor with one owner.
  • Bind experiments and tests to 127.0.0.1, with port 0 when any unused port will do.
  • recv returning 0 means the other side has finished sending; shutdown(SHUT_WR) says so without closing.
  • TCP keeps the order of bytes but not the boundaries of messages, so every protocol frames them: a length prefix, a delimiter, a fixed size or self-describing headers.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.