LEARN CPP NETWORK PROGRAMMING
Learn C++ Network Programming: From Sockets to Scalable Systems
Goal: Master network programming in C++—from raw Berkeley sockets to high-performance async servers using epoll, io_uring, and C++20 coroutines. Build real networked applications with proper security, serialization, and concurrency.
Why C++ Network Programming Matters
C++ remains the language of choice for high-performance networking: game servers, trading systems, web servers (nginx), databases, and infrastructure software. Understanding network programming at this level gives you:
- Complete control over memory, buffers, and system calls
- Maximum performance for latency-sensitive applications
- Deep understanding of how the internet actually works
- Skills that transfer to any systems programming task
After completing these projects, you will:
- Read and write raw socket code without libraries
- Build servers handling 10,000+ concurrent connections
- Implement protocols from RFCs (HTTP, WebSocket, SOCKS5)
- Integrate TLS/SSL security properly
- Use modern C++ concurrency (threads, executors, coroutines)
- Understand the trade-offs between blocking, non-blocking, and async I/O
Core Concept Analysis
The Network Programming Stack
┌─────────────────────────────────────────────────────────────┐
│ Your Application │
├─────────────────────────────────────────────────────────────┤
│ Serialization (JSON, Protobuf) │ Protocols (HTTP, WS) │
├─────────────────────────────────────────────────────────────┤
│ TLS/SSL (OpenSSL, BoringSSL) │
├─────────────────────────────────────────────────────────────┤
│ I/O Multiplexing (select/poll/epoll/io_uring) │
├─────────────────────────────────────────────────────────────┤
│ Sockets API (Berkeley/POSIX) │
├─────────────────────────────────────────────────────────────┤
│ TCP / UDP │
├─────────────────────────────────────────────────────────────┤
│ IP Layer │
└─────────────────────────────────────────────────────────────┘
Fundamental Concepts
- Socket Basics
- A socket is a file descriptor representing a network endpoint
socket()→bind()→listen()→accept()(server)socket()→connect()(client)- TCP = reliable stream, UDP = unreliable datagrams
- Blocking vs Non-Blocking
- Blocking:
recv()waits until data arrives (simple but doesn’t scale) - Non-blocking:
recv()returns immediately with EAGAIN/EWOULDBLOCK - Non-blocking requires I/O multiplexing to know when to read/write
- Blocking:
- I/O Multiplexing Evolution
select (1983) → poll (1997) → epoll (2002) → io_uring (2019) select: O(n) scan, 1024 fd limit, copies fd_set each call poll: O(n) scan, no fd limit, still copies array epoll: O(1) for ready events, kernel maintains state io_uring: True async I/O, batched syscalls, zero-copy possible - Concurrency Models
- Thread-per-connection: Simple but doesn’t scale (context switching)
- Event loop + non-blocking: Single thread handles thousands (nginx model)
- Thread pool + event loop: Best of both worlds
- Coroutines: Async code that reads like sync code (C++20)
- Protocol Layers
- TCP: Connection-oriented, ordered, reliable
- UDP: Connectionless, unordered, unreliable but fast
- HTTP: Text protocol over TCP (request-response)
- WebSocket: Full-duplex over HTTP upgrade
- TLS: Encryption layer between TCP and application
Project Progression Overview
| Phase | Projects | What You Learn |
|---|---|---|
| Phase 1: Foundations | 1-4 | Raw sockets, TCP/UDP basics, blocking I/O |
| Phase 2: Protocols | 5-8 | HTTP, parsing, error handling, serialization |
| Phase 3: Scaling | 9-12 | Non-blocking, epoll, thread pools |
| Phase 4: Security | 13-14 | TLS/SSL integration with OpenSSL |
| Phase 5: Modern C++ | 15-17 | io_uring, coroutines, production patterns |
| Final Project | 18 | Everything combined |
Phase 1: Socket Foundations
Project 1: DNS Query Tool (UDP Basics)
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: C, Rust, Go
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 1: Beginner
- Knowledge Area: UDP Sockets, DNS Protocol, Binary Parsing
- Software or Tool: dig/nslookup replacement
- Main Book: “TCP/IP Illustrated, Volume 1” by W. Richard Stevens
What you’ll build: A command-line tool that sends DNS queries over UDP, parses the binary response, and displays the resolved IP addresses—without using any DNS library.
Why it teaches network programming: DNS is the simplest real-world UDP protocol. You’ll learn socket creation, sendto()/recvfrom() for UDP, binary message construction, and parsing network byte order. UDP has no connection state, making it perfect for understanding raw socket operations.
Core challenges you’ll face:
- Creating and configuring a UDP socket → maps to socket() and addressing structures
- Constructing DNS query packets in binary → maps to network byte order (htons/htonl)
- Parsing variable-length DNS responses → maps to pointer arithmetic and buffer management
- Handling timeouts for unreliable UDP → maps to setsockopt() and SO_RCVTIMEO
Key Concepts:
- Socket Creation: “The Linux Programming Interface” Chapter 56 - Michael Kerrisk
- UDP Semantics: “TCP/IP Illustrated, Volume 1” Chapter 11 - W. Richard Stevens
- DNS Wire Format: RFC 1035 Section 4
- Network Byte Order: “UNIX Network Programming” Section 3.4 - W. Richard Stevens
Difficulty: Beginner Time estimate: Weekend Prerequisites: Basic C++ (classes, pointers, arrays), understanding of hexadecimal, familiarity with command line
Real world outcome:
$ ./dnsquery google.com
Querying DNS server 8.8.8.8 for google.com (A record)...
Response received (52 bytes):
Transaction ID: 0xAB12
Flags: 0x8180 (Response, No Error)
Questions: 1, Answers: 4
Answer 1: google.com. 300 IN A 142.250.80.46
Answer 2: google.com. 300 IN A 142.250.80.47
Answer 3: google.com. 300 IN A 142.250.80.78
Answer 4: google.com. 300 IN A 142.250.80.110
$ ./dnsquery -t MX gmail.com
Querying DNS server 8.8.8.8 for gmail.com (MX record)...
Answer 1: gmail.com. 3600 IN MX 5 gmail-smtp-in.l.google.com.
Answer 2: gmail.com. 3600 IN MX 10 alt1.gmail-smtp-in.l.google.com.
Implementation Hints:
The DNS message format is fixed-header + variable sections:
+---------------------+
| Header | 12 bytes (fixed)
+---------------------+
| Question | Variable (your query)
+---------------------+
| Answer | Variable (response records)
+---------------------+
| Authority | Variable (NS records)
+---------------------+
| Additional | Variable (extra info)
+---------------------+
Start by defining structures for the DNS header. Remember: network byte order is big-endian, your CPU is probably little-endian. Use htons() to convert 16-bit values before sending.
For the socket:
- Create:
socket(AF_INET, SOCK_DGRAM, 0)— SOCK_DGRAM means UDP - Set timeout:
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, ...) - Send:
sendto(sock, buffer, len, 0, &server_addr, sizeof(server_addr)) - Receive:
recvfrom(sock, buffer, sizeof(buffer), 0, ...)
Domain names in DNS use length-prefixed labels: www.google.com becomes \x03www\x06google\x03com\x00.
Ask yourself:
- How do I convert “www.google.com” to DNS wire format?
- What happens if the UDP response is lost? How do I detect this?
- How do I parse a variable-length domain name from the response?
Learning milestones:
- You send a query and receive bytes back → You understand UDP socket basics
- You parse the header correctly → You understand network byte order
- You decode domain names with compression → You understand DNS wire format
- You handle timeouts and retries → You understand UDP unreliability
Project 2: TCP Echo Server & Client (Blocking Sockets)
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: C, Rust, Go
- Coolness Level: Level 2: Practical but Forgettable
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 1: Beginner
- Knowledge Area: TCP Sockets, Client-Server Model
- Software or Tool: Network debugging tool
- Main Book: “UNIX Network Programming, Volume 1” by W. Richard Stevens
What you’ll build: A server that listens on a port and echoes back whatever clients send, plus a client that connects, sends messages, and displays responses. The server handles one client at a time (blocking).
Why it teaches network programming: This is the “Hello World” of TCP. You’ll learn the complete socket lifecycle: socket() → bind() → listen() → accept() for servers, socket() → connect() for clients. Understanding blocking behavior is essential before learning why non-blocking exists.
Core challenges you’ll face:
- Setting up the server socket correctly → maps to bind(), listen(), accept() sequence
- Handling partial reads/writes → maps to TCP is a byte stream, not message-oriented
- Detecting client disconnection → maps to read() returning 0, handling SIGPIPE
- Proper resource cleanup → maps to close() and RAII patterns
Key Concepts:
- TCP Socket Lifecycle: “UNIX Network Programming, Volume 1” Chapter 4 - Stevens
- The listen() Backlog: “The Linux Programming Interface” Section 56.6.1 - Kerrisk
- Partial I/O: “UNIX Network Programming, Volume 1” Section 3.9 - Stevens
- RAII for Sockets: “Effective C++” Item 13 - Scott Meyers
Difficulty: Beginner Time estimate: Weekend Prerequisites: Project 1, understanding of file descriptors
Real world outcome:
# Terminal 1: Start server
$ ./echo_server 9000
Echo server listening on port 9000...
Client connected from 127.0.0.1:52431
Received: "Hello, server!"
Sent: "Hello, server!"
Received: "Testing 123"
Sent: "Testing 123"
Client disconnected.
# Terminal 2: Run client
$ ./echo_client localhost 9000
Connected to localhost:9000
> Hello, server!
Server: Hello, server!
> Testing 123
Server: Testing 123
> ^C
Disconnected.
# Also works with netcat/telnet:
$ echo "Hello from nc" | nc localhost 9000
Hello from nc
Implementation Hints:
The server socket setup sequence (every TCP server does this):
// 1. Create socket
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 2. Allow address reuse (avoid "Address already in use")
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 3. Bind to address
struct sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY; // Listen on all interfaces
addr.sin_port = htons(port);
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
// 4. Listen (backlog = pending connection queue size)
listen(server_fd, 10);
// 5. Accept loop
while (true) {
int client_fd = accept(server_fd, ...); // BLOCKS here
handle_client(client_fd); // Also blocks
close(client_fd);
}
Critical insight: TCP is a byte stream. If the client sends “Hello” and “World” separately, you might receive “HelloWorld” in one read, or “Hel” and “loWorld” in two reads. Never assume one send() = one recv().
Use RAII to manage socket lifetimes:
class Socket {
int fd_;
public:
explicit Socket(int fd) : fd_(fd) {}
~Socket() { if (fd_ >= 0) close(fd_); }
Socket(Socket&& other) noexcept : fd_(other.fd_) { other.fd_ = -1; }
// ... delete copy operations
};
Ask yourself:
- What happens if the client sends data faster than the server can echo?
- Why do we need SO_REUSEADDR?
- How does the server know the client disconnected?
Learning milestones:
- Server accepts a connection → You understand the listen/accept dance
- Echo works correctly → You understand read/write on sockets
- Client can reconnect after disconnect → You understand SO_REUSEADDR
- No resource leaks (valgrind clean) → You understand RAII for sockets
Project 3: Multi-Client Chat Server (select/poll)
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: C, Rust, Go
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 2: Intermediate
- Knowledge Area: I/O Multiplexing, Event-Driven Programming
- Software or Tool: IRC-like chat server
- Main Book: “UNIX Network Programming, Volume 1” by W. Richard Stevens
What you’ll build: A chat server that handles multiple simultaneous clients using select() or poll(), broadcasting messages from any client to all others. Clients can set nicknames and see who’s online.
Why it teaches network programming: Your echo server can only handle one client at a time. Real servers handle thousands. This project introduces I/O multiplexing—the technique that lets a single thread monitor multiple sockets. Understanding select() and poll() is foundational before learning epoll.
Core challenges you’ll face:
- Managing multiple file descriptors → maps to fd_set for select, pollfd array for poll
- Handling the accept socket alongside client sockets → maps to mixing listening and connected sockets
- Broadcasting to all clients → maps to tracking connected clients, handling partial sends
- Detecting disconnects in a multiplexed loop → maps to read() returning 0, poll POLLHUP
Key Concepts:
- select() System Call: “The Linux Programming Interface” Section 63.2 - Kerrisk
- poll() vs select(): “UNIX Network Programming, Volume 1” Section 6.3 - Stevens
- Event Loop Pattern: “Linux System Programming” Chapter 2 - Robert Love
- fd_set Limitations: 1024 fd limit on many systems
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Project 2, comfort with C++ containers (vector, map)
Real world outcome:
# Terminal 1: Start server
$ ./chat_server 9000
Chat server started on port 9000
[12:00:01] Client connected from 127.0.0.1:52431 (fd=4)
[12:00:05] Client connected from 127.0.0.1:52432 (fd=5)
[12:00:10] <Alice> Hello everyone!
[12:00:12] <Bob> Hey Alice!
[12:00:30] Client disconnected (fd=4)
# Terminal 2: Client 1
$ ./chat_client localhost 9000
Connected! Enter /nick <name> to set nickname.
/nick Alice
You are now known as Alice
Hello everyone!
<Bob> Hey Alice!
# Terminal 3: Client 2
$ ./chat_client localhost 9000
/nick Bob
<Alice> Hello everyone!
Hey Alice!
Implementation Hints:
The select() approach (simpler to understand):
fd_set master_set; // All fds we care about
FD_ZERO(&master_set);
FD_SET(listen_fd, &master_set);
int max_fd = listen_fd;
while (true) {
fd_set read_set = master_set; // select() modifies the set!
select(max_fd + 1, &read_set, nullptr, nullptr, nullptr);
for (int fd = 0; fd <= max_fd; fd++) {
if (!FD_ISSET(fd, &read_set)) continue;
if (fd == listen_fd) {
// New connection
int client_fd = accept(listen_fd, ...);
FD_SET(client_fd, &master_set);
max_fd = std::max(max_fd, client_fd);
} else {
// Client data
char buf[1024];
int n = read(fd, buf, sizeof(buf));
if (n <= 0) {
// Client disconnected
FD_CLR(fd, &master_set);
close(fd);
} else {
// Broadcast to all other clients
broadcast(buf, n, fd);
}
}
}
}
The poll() approach (scales better):
std::vector<pollfd> fds;
fds.push_back({listen_fd, POLLIN, 0});
while (true) {
poll(fds.data(), fds.size(), -1);
for (auto& pfd : fds) {
if (!(pfd.revents & POLLIN)) continue;
// Handle read...
}
}
For the client, you need to monitor both stdin (user input) AND the socket (server messages). Use poll() on both:
pollfd fds[2];
fds[0] = {STDIN_FILENO, POLLIN, 0};
fds[1] = {socket_fd, POLLIN, 0};
Ask yourself:
- Why can’t we just use multiple threads instead of select/poll?
- What happens if one client’s send buffer is full?
- How do you handle a client that sends a partial line?
Learning milestones:
- Server accepts multiple clients → You understand multiplexing the listen socket
- Messages broadcast correctly → You understand iterating over connected fds
- Disconnects handled cleanly → You understand removing fds from the set
- Client receives while typing → You understand multiplexing stdin and socket
Project 4: TCP Port Scanner
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: C, Rust, Go
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Connection States, Timeouts, Concurrent Connections
- Software or Tool: nmap-like tool
- Main Book: “TCP/IP Illustrated, Volume 1” by W. Richard Stevens
What you’ll build: A command-line tool that scans a range of ports on a target host, determining which are open, closed, or filtered. Supports concurrent scanning for speed.
Why it teaches network programming: Port scanning forces you to understand TCP connection states (SYN, SYN-ACK, RST), timeouts, and non-blocking connect(). You’ll learn why some connections succeed immediately, some fail immediately, and some hang. This is essential knowledge for debugging real applications.
Core challenges you’ll face:
- Non-blocking connect() → maps to EINPROGRESS and using select/poll to wait
- Detecting open vs closed vs filtered → maps to connection success, RST, timeout
- Concurrent connection attempts → maps to managing many pending connections
- Reasonable timeout handling → maps to getsockopt() SO_ERROR after select
Key Concepts:
- TCP Connection Establishment: “TCP/IP Illustrated, Volume 1” Chapter 18 - Stevens
- Non-blocking connect(): “UNIX Network Programming, Volume 1” Section 16.3 - Stevens
- Connection Timeouts: “The Linux Programming Interface” Section 61.3 - Kerrisk
- TCP State Machine: RFC 793
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Project 2-3, understanding of TCP three-way handshake
Real world outcome:
$ ./portscan scanme.nmap.org 1-1000
Scanning scanme.nmap.org (45.33.32.156) ports 1-1000...
Concurrent connections: 100
Timeout: 3 seconds
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
135/tcp filtered msrpc
139/tcp filtered netbios-ssn
445/tcp filtered microsoft-ds
...
Scan complete: 997 closed, 2 open, 3 filtered
Time: 4.2 seconds
$ ./portscan -p 22,80,443,8080 192.168.1.1
PORT STATE
22/tcp open
80/tcp open
443/tcp closed
8080/tcp closed
Implementation Hints:
Non-blocking connect() pattern:
// 1. Create non-blocking socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
int flags = fcntl(sock, F_GETFL, 0);
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
// 2. Start connection
int ret = connect(sock, &addr, sizeof(addr));
if (ret == 0) {
// Immediate success (rare, usually localhost)
return OPEN;
} else if (errno != EINPROGRESS) {
// Immediate failure
return CLOSED;
}
// 3. Connection in progress - wait with poll
pollfd pfd = {sock, POLLOUT, 0};
int ready = poll(&pfd, 1, timeout_ms);
if (ready == 0) {
return FILTERED; // Timeout - probably firewall
}
// 4. Check if connection succeeded
int error = 0;
socklen_t len = sizeof(error);
getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len);
if (error == 0) return OPEN;
if (error == ECONNREFUSED) return CLOSED;
return FILTERED;
For concurrent scanning, manage an array of “pending connections”:
struct PendingConnection {
int socket;
int port;
std::chrono::steady_clock::time_point start_time;
};
std::vector<PendingConnection> pending;
Maintain up to N concurrent connections (e.g., 100). When one completes, start the next port.
Ask yourself:
- Why does a closed port respond immediately but a filtered port times out?
- What happens at the TCP level when connect() gets ECONNREFUSED?
- How would you implement SYN scanning (requires raw sockets)?
Learning milestones:
- You detect open ports → You understand connect() success
- You detect closed ports → You understand ECONNREFUSED / RST
- You detect filtered ports → You understand timeouts and firewalls
- Scanning is fast (100+ ports/second) → You understand concurrent non-blocking I/O
Phase 2: Protocol Implementation
Project 5: HTTP/1.1 Client (From Scratch)
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: Rust, Go, Python
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: HTTP Protocol, Text Parsing, Keep-Alive
- Software or Tool: curl replacement
- Main Book: “HTTP: The Definitive Guide” by David Gourley
What you’ll build: A command-line HTTP client that can fetch web pages, submit forms, and download files—implementing HTTP/1.1 from raw sockets, including chunked transfer encoding and keep-alive connections.
Why it teaches network programming: HTTP is text-based but deceptively complex. You’ll learn to parse headers, handle multiple response formats, manage connection reuse, and deal with the messy reality of real-world servers that don’t always follow specs perfectly.
Core challenges you’ll face:
- Parsing HTTP response headers → maps to line-by-line reading, header field extraction
- Determining response body length → maps to Content-Length vs chunked vs connection close
- Implementing chunked transfer decoding → maps to state machine parsing
- Connection keep-alive → maps to reusing sockets, connection pooling
Key Concepts:
- HTTP/1.1 Protocol: RFC 7230-7235 (or RFC 2616 for simpler overview)
- Chunked Transfer Encoding: “HTTP: The Definitive Guide” Chapter 15 - Gourley
- Connection Management: “HTTP: The Definitive Guide” Chapter 4 - Gourley
- URL Parsing: RFC 3986
Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Project 2, string parsing skills
Real world outcome:
$ ./httpclient https://httpbin.org/get
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 256
{
"args": {},
"headers": {
"Host": "httpbin.org",
"User-Agent": "MyHttpClient/1.0"
},
"origin": "1.2.3.4",
"url": "https://httpbin.org/get"
}
$ ./httpclient -X POST -d "name=test" https://httpbin.org/post
HTTP/1.1 200 OK
...
$ ./httpclient -o image.png https://example.com/image.png
Downloaded 45,234 bytes to image.png
$ ./httpclient -v https://example.com
> GET / HTTP/1.1
> Host: example.com
> User-Agent: MyHttpClient/1.0
> Connection: keep-alive
>
< HTTP/1.1 200 OK
< Content-Type: text/html
< Transfer-Encoding: chunked
<
<!DOCTYPE html>...
Implementation Hints:
An HTTP/1.1 request is text:
GET /path HTTP/1.1\r\n
Host: example.com\r\n
User-Agent: MyClient/1.0\r\n
Connection: keep-alive\r\n
\r\n
The response format:
HTTP/1.1 200 OK\r\n
Content-Type: text/html\r\n
Content-Length: 1234\r\n
\r\n
<body bytes here>
Key parsing challenges:
- Headers end at
\r\n\r\n- but you might receive this across multiplerecv()calls - Body length can be specified three ways:
Content-Length: N→ read exactly N bytesTransfer-Encoding: chunked→ parse chunk sizes- Neither → read until connection closes (HTTP/1.0 style)
Chunked encoding format:
1a\r\n <- chunk size in hex (26 bytes)
<26 bytes of data>\r\n <- data + CRLF
10\r\n <- next chunk (16 bytes)
<16 bytes of data>\r\n
0\r\n <- final chunk (0 = end)
\r\n <- trailing CRLF
Build a response parser class:
class HttpResponseParser {
enum State { PARSING_STATUS, PARSING_HEADERS, PARSING_BODY };
State state_ = PARSING_STATUS;
std::string buffer_;
public:
// Feed data as it arrives, returns true when complete
bool feed(const char* data, size_t len);
int status_code() const;
std::string header(const std::string& name) const;
std::string body() const;
};
Ask yourself:
- How do you handle a server that doesn’t send
Content-Length? - What if headers are split across multiple
recv()calls? - How do you reuse a connection for multiple requests?
Learning milestones:
- Simple GET works → You understand HTTP request format
- Content-Length responses parse correctly → You understand body delimiting
- Chunked encoding works → You understand streaming responses
- Keep-alive works → You understand connection reuse
Project 6: HTTP/1.1 Server (From Scratch)
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: Rust, Go, C
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: HTTP Protocol, Request Routing, Static File Serving
- Software or Tool: nginx/Apache alternative
- Main Book: “HTTP: The Definitive Guide” by David Gourley
What you’ll build: A web server that serves static files, handles multiple concurrent connections (using poll/select from Project 3), supports keep-alive, and returns proper HTTP status codes and headers.
Why it teaches network programming: Building a web server synthesizes everything: socket management, protocol parsing, file I/O, MIME types, error handling, and concurrent connection management. This is a foundational systems programming project.
Core challenges you’ll face:
- Parsing HTTP requests → maps to handling partial reads, buffer management
- Routing and serving files → maps to path validation, preventing directory traversal
- MIME type detection → maps to file extension mapping
- Proper error responses → maps to HTTP status codes, error pages
Key Concepts:
- HTTP Request Parsing: RFC 7230 Section 3
- Static File Serving: “HTTP: The Definitive Guide” Chapter 18 - Gourley
- Security (Path Traversal): Prevent
/../../../etc/passwdattacks - MIME Types: “HTTP: The Definitive Guide” Chapter 17 - Gourley
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 3 and 5
Real world outcome:
$ ./httpserver -p 8080 -d ./www
HTTP server listening on port 8080
Serving files from ./www
# In browser: http://localhost:8080/
# Or with curl:
$ curl -v http://localhost:8080/index.html
> GET /index.html HTTP/1.1
> Host: localhost:8080
>
< HTTP/1.1 200 OK
< Content-Type: text/html
< Content-Length: 1234
< Connection: keep-alive
<
<!DOCTYPE html>...
$ curl http://localhost:8080/notfound.html
HTTP/1.1 404 Not Found
Content-Type: text/html
<h1>404 Not Found</h1>
$ curl http://localhost:8080/../../../etc/passwd
HTTP/1.1 403 Forbidden
Implementation Hints:
Request parsing pseudo-structure:
struct HttpRequest {
std::string method; // GET, POST, etc.
std::string path; // /index.html
std::string version; // HTTP/1.1
std::map<std::string, std::string> headers;
std::string body;
};
// Parse: "GET /index.html HTTP/1.1\r\n"
// Then parse headers until "\r\n\r\n"
Security: Always validate paths!
std::string resolve_path(const std::string& doc_root, const std::string& request_path) {
// Use realpath() to resolve symlinks and ".."
// Verify the result starts with doc_root
char resolved[PATH_MAX];
std::string full = doc_root + request_path;
if (!realpath(full.c_str(), resolved)) {
return ""; // File doesn't exist
}
if (strncmp(resolved, doc_root.c_str(), doc_root.length()) != 0) {
return ""; // Path traversal attempt!
}
return resolved;
}
MIME types (basic):
std::string get_mime_type(const std::string& path) {
if (ends_with(path, ".html")) return "text/html";
if (ends_with(path, ".css")) return "text/css";
if (ends_with(path, ".js")) return "application/javascript";
if (ends_with(path, ".png")) return "image/png";
if (ends_with(path, ".jpg")) return "image/jpeg";
return "application/octet-stream";
}
Connection handling (simplified flow):
void handle_request(int client_fd, const std::string& doc_root) {
HttpRequest req = parse_request(client_fd);
std::string file_path = resolve_path(doc_root, req.path);
if (file_path.empty()) {
send_error(client_fd, 404, "Not Found");
return;
}
std::ifstream file(file_path, std::ios::binary);
// Read file, send response with headers
}
Ask yourself:
- How do you handle a POST request with a request body?
- What if a file is 10GB? Can you stream it without loading into memory?
- How do you handle If-Modified-Since for caching?
Learning milestones:
- Serves index.html → You understand request parsing and file serving
- Multiple concurrent clients work → You’ve integrated I/O multiplexing
- Path traversal attacks fail → You understand security validation
- Keep-alive works with multiple requests → You understand connection persistence
Project 7: JSON-RPC Server
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: Rust, Go, Python
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 2: Intermediate
- Knowledge Area: RPC, JSON Serialization, Protocol Design
- Software or Tool: Microservice backend
- Main Book: “Designing Data-Intensive Applications” by Martin Kleppmann
What you’ll build: A server implementing JSON-RPC 2.0 over TCP—clients send JSON requests with method names and parameters, server executes the method and returns JSON results. Include error handling per the spec.
Why it teaches network programming: RPC is how distributed systems communicate. You’ll learn serialization (converting C++ objects to/from JSON), framing (knowing where one message ends and another begins), and error handling across network boundaries.
Core challenges you’ll face:
- Message framing → maps to length-prefix or delimiter-based framing
- JSON parsing and generation → maps to using nlohmann/json or similar
- Method dispatch → maps to runtime function lookup
- Error codes and responses → maps to JSON-RPC 2.0 error format
Key Concepts:
- JSON-RPC 2.0 Specification: https://www.jsonrpc.org/specification
- Message Framing: “Designing Data-Intensive Applications” Chapter 4 - Kleppmann
- nlohmann/json Library: https://github.com/nlohmann/json
- RPC Patterns: “Patterns of Enterprise Application Architecture” - Fowler
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Project 2, familiarity with JSON
Real world outcome:
$ ./jsonrpc_server 9000
JSON-RPC server listening on port 9000
Registered methods: add, subtract, multiply, divide, echo
# Client sends (with 4-byte length prefix):
{"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1}
# Server responds:
{"jsonrpc": "2.0", "result": 3, "id": 1}
# Error case:
{"jsonrpc": "2.0", "method": "divide", "params": [1, 0], "id": 2}
# Response:
{"jsonrpc": "2.0", "error": {"code": -32000, "message": "Division by zero"}, "id": 2}
# Unknown method:
{"jsonrpc": "2.0", "method": "unknown", "params": [], "id": 3}
# Response:
{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": 3}
Implementation Hints:
Message framing (length-prefix approach):
+----------------+------------------+
| 4-byte length | JSON payload |
| (big-endian) | (UTF-8) |
+----------------+------------------+
Reading a framed message:
std::string read_message(int fd) {
uint32_t length;
read_exact(fd, &length, 4);
length = ntohl(length); // Network to host byte order
std::string message(length, '\0');
read_exact(fd, message.data(), length);
return message;
}
Method dispatch with function pointers:
using RpcMethod = std::function<json(const json& params)>;
std::map<std::string, RpcMethod> methods;
methods["add"] = [](const json& params) {
return params[0].get<int>() + params[1].get<int>();
};
methods["echo"] = [](const json& params) {
return params[0];
};
// Dispatch
json handle_request(const json& request) {
std::string method = request["method"];
auto it = methods.find(method);
if (it == methods.end()) {
return json{{"jsonrpc", "2.0"},
{"error", {{"code", -32601}, {"message", "Method not found"}}},
{"id", request["id"]}};
}
try {
json result = it->second(request["params"]);
return json{{"jsonrpc", "2.0"}, {"result", result}, {"id", request["id"]}};
} catch (const std::exception& e) {
return json{{"jsonrpc", "2.0"},
{"error", {{"code", -32000}, {"message", e.what()}}},
{"id", request["id"]}};
}
}
Ask yourself:
- How do you handle batch requests (array of RPC calls)?
- What if a method takes a long time? How do you avoid blocking other clients?
- How would you add authentication?
Learning milestones:
- Basic method calls work → You understand JSON-RPC format
- Errors return correctly → You understand error propagation
- Multiple clients can call concurrently → You’ve integrated multiplexing
- Clean architecture → You understand separating transport from logic
Project 8: Protocol Buffers Message Queue
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: Go, Rust, Java
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 3: Advanced
- Knowledge Area: Binary Serialization, Message Queues, Pub/Sub
- Software or Tool: Redis/RabbitMQ alternative
- Main Book: “Designing Data-Intensive Applications” by Martin Kleppmann
What you’ll build: A simple message queue server using Protocol Buffers for serialization. Clients can publish messages to topics and subscribe to receive them. Messages are persisted to disk for durability.
Why it teaches network programming: JSON is human-readable but slow and bloated. Protocol Buffers are compact and fast—used by Google for all internal services. You’ll learn schema definition, code generation, and efficient binary serialization. Message queues also teach pub/sub patterns essential for distributed systems.
Core challenges you’ll face:
- Protocol Buffer schema design → maps to .proto files and protoc compiler
- Efficient binary framing → maps to varint length prefixes
- Topic routing → maps to subscriber management per topic
- Message persistence → maps to append-only log files
Key Concepts:
- Protocol Buffers: https://protobuf.dev/programming-guides/encoding/
- Message Queue Semantics: “Designing Data-Intensive Applications” Chapter 11 - Kleppmann
- Pub/Sub Pattern: “Enterprise Integration Patterns” - Hohpe & Woolf
- Append-Only Logs: “Designing Data-Intensive Applications” Chapter 3 - Kleppmann
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 3 and 7, understanding of binary formats
Real world outcome:
$ ./msgqueue_server 9000
Message queue server listening on port 9000
# Terminal 2: Subscriber
$ ./msgqueue_client subscribe notifications
Subscribed to 'notifications'
Waiting for messages...
[notifications] User "alice" logged in at 2024-01-15T10:30:00Z
[notifications] New order #1234 received
# Terminal 3: Publisher
$ ./msgqueue_client publish notifications '{"event": "login", "user": "alice"}'
Published to 'notifications'
# Server stats
$ ./msgqueue_client stats
Topics: 3
notifications: 2 subscribers, 145 messages
orders: 1 subscriber, 89 messages
logs: 0 subscribers, 10,234 messages
Messages/sec: 1,245
Implementation Hints:
Define your protocol in .proto:
syntax = "proto3";
message Envelope {
uint32 type = 1;
bytes payload = 2;
}
message PublishRequest {
string topic = 1;
bytes message = 2;
}
message SubscribeRequest {
string topic = 1;
}
message Message {
string topic = 1;
bytes payload = 2;
uint64 timestamp = 3;
uint64 sequence = 4;
}
Compile with: protoc --cpp_out=. messages.proto
Binary framing with varint (protobuf style):
void write_varint(std::vector<uint8_t>& buf, uint32_t value) {
while (value > 127) {
buf.push_back((value & 0x7F) | 0x80);
value >>= 7;
}
buf.push_back(value);
}
uint32_t read_varint(int fd) {
uint32_t result = 0;
int shift = 0;
uint8_t byte;
do {
read(fd, &byte, 1);
result |= (byte & 0x7F) << shift;
shift += 7;
} while (byte & 0x80);
return result;
}
Subscriber management:
struct Topic {
std::string name;
std::vector<int> subscriber_fds; // Connected clients
std::deque<Message> messages; // Recent messages
std::ofstream log_file; // Persistence
};
std::map<std::string, Topic> topics;
Ask yourself:
- How do you handle a slow subscriber that can’t keep up?
- What happens if the server crashes? How do you replay messages?
- How would you implement message acknowledgment?
Learning milestones:
- Protobuf compiles and serializes → You understand code generation
- Pub/sub works → You understand topic routing
- Messages persist across restarts → You understand durability
- Multiple subscribers receive same message → You understand fanout
Phase 3: Scaling Up
Project 9: High-Performance Server with epoll
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: C, Rust
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Linux epoll, Non-Blocking I/O, Event Loop
- Software or Tool: nginx-style event loop
- Main Book: “The Linux Programming Interface” by Michael Kerrisk
What you’ll build: Rewrite your echo/chat server using Linux’s epoll interface, capable of handling 10,000+ concurrent connections with a single thread. Implement proper edge-triggered handling.
Why it teaches network programming: select() and poll() don’t scale—they’re O(n) per call. epoll is O(1) for checking readiness and is how real high-performance servers (nginx, Redis) handle massive concurrency. Understanding edge-triggered vs level-triggered is crucial for writing correct, efficient code.
Core challenges you’ll face:
- epoll API usage → maps to epoll_create, epoll_ctl, epoll_wait
- Edge-triggered mode → maps to draining sockets completely, handling EAGAIN
- Non-blocking everything → maps to accept4() with SOCK_NONBLOCK
- Connection state management → maps to per-connection context objects
Key Concepts:
- epoll: “The Linux Programming Interface” Chapter 63 - Kerrisk
- Edge vs Level Triggered: “The Linux Programming Interface” Section 63.4.6 - Kerrisk
- C10K Problem: http://www.kegel.com/c10k.html
- Event Loop Design: Redis source code (ae.c)
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Projects 3-4, Linux system
Real world outcome:
$ ./epoll_echo_server 9000
epoll echo server on port 9000 (edge-triggered)
# Benchmark with many connections:
$ wrk -t4 -c1000 -d10s http://localhost:9000/
Running 10s test @ http://localhost:9000/
4 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.23ms 0.45ms 12.3ms 98.2%
Req/Sec 25.1k 1.2k 28.3k 89.1%
1,000,000 requests in 10.00s
Requests/sec: 100,000
# Server stats:
$ ./epoll_echo_server 9000 --stats
Connections: 1,000 active
Bytes in: 45 MB/s
Bytes out: 45 MB/s
Events/sec: 250,000
Implementation Hints:
epoll setup:
int epoll_fd = epoll_create1(0);
// Add listening socket
epoll_event ev;
ev.events = EPOLLIN; // Level-triggered for accept
ev.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);
// Event loop
epoll_event events[MAX_EVENTS];
while (true) {
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == listen_fd) {
// Accept new connections
while (true) {
int client = accept4(listen_fd, ..., SOCK_NONBLOCK);
if (client < 0) break; // No more pending
epoll_event cev;
cev.events = EPOLLIN | EPOLLET; // Edge-triggered!
cev.data.fd = client;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client, &cev);
}
} else {
// Handle client data (edge-triggered)
handle_client_et(events[i].data.fd);
}
}
}
Edge-triggered reading (CRITICAL):
void handle_client_et(int fd) {
// Must drain ALL available data!
while (true) {
char buf[4096];
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
// Process data
write(fd, buf, n); // Echo
} else if (n == 0) {
// Connection closed
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, nullptr);
close(fd);
break;
} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
// No more data right now
break;
} else {
// Error
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, nullptr);
close(fd);
break;
}
}
}
Connection context (for stateful protocols):
struct Connection {
int fd;
std::string read_buffer;
std::string write_buffer;
// Protocol state...
};
std::unordered_map<int, std::unique_ptr<Connection>> connections;
Ask yourself:
- Why is edge-triggered more efficient but harder to use correctly?
- What happens if you forget to drain the socket in ET mode?
- How do you handle write buffer filling up (EPOLLOUT)?
Learning milestones:
- Server handles 1000+ connections → You understand epoll basics
- No data loss in edge-triggered mode → You understand draining sockets
- wrk benchmark shows high throughput → You’ve optimized the event loop
- Memory usage stays flat → You understand connection lifecycle
Project 10: HTTP Load Tester
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: Go, Rust
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 3: Advanced
- Knowledge Area: Concurrent Connections, Statistics, Benchmarking
- Software or Tool: wrk/ab alternative
- Main Book: “Systems Performance” by Brendan Gregg
What you’ll build: A load testing tool that generates thousands of concurrent HTTP requests, measures latency percentiles (p50, p95, p99), throughput, and error rates. Supports configurable request rates and connection pools.
Why it teaches network programming: Building a load tester requires managing many concurrent connections efficiently. You’ll learn connection pooling, rate limiting, accurate timing, and statistical analysis of network performance.
Core challenges you’ll face:
- Managing many concurrent connections → maps to connection pool, epoll
- Accurate timing measurements → maps to high-resolution clocks, avoiding skew
- Rate limiting → maps to token bucket or leaky bucket algorithms
- Computing latency percentiles → maps to histogram data structures
Key Concepts:
- Latency Percentiles: “Systems Performance” Chapter 2 - Gregg
- Connection Pooling: HTTP keep-alive reuse
- Rate Limiting: Token bucket algorithm
- HDR Histogram: https://hdrhistogram.org/
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 5, 9
Real world outcome:
$ ./loadtest http://localhost:8080/api/users -c 100 -d 30s -r 1000
Load testing http://localhost:8080/api/users
Concurrency: 100 connections
Duration: 30 seconds
Target rate: 1000 req/s
Running...
Summary:
Total requests: 30,000
Successful: 29,847 (99.5%)
Failed: 153 (0.5%)
Latency:
Min: 0.5ms
Mean: 2.3ms
p50: 1.8ms
p95: 5.2ms
p99: 12.4ms
Max: 145.3ms
Throughput:
Requests/sec: 995.8
Bytes/sec: 4.2 MB
Errors:
Connection refused: 45
Timeout: 108
$ ./loadtest http://api.example.com/search -c 500 --duration 60s --rate 5000
Implementation Hints:
Overall architecture:
class LoadTester {
std::vector<std::unique_ptr<Connection>> pool_;
int epoll_fd_;
// Statistics
HdrHistogram latency_histogram_;
std::atomic<uint64_t> requests_sent_{0};
std::atomic<uint64_t> requests_completed_{0};
std::atomic<uint64_t> errors_{0};
// Rate limiting
std::chrono::steady_clock::time_point start_time_;
uint64_t target_rate_; // requests per second
};
Rate limiting (token bucket):
class TokenBucket {
double tokens_;
double rate_; // tokens per second
std::chrono::steady_clock::time_point last_time_;
public:
bool try_acquire() {
auto now = std::chrono::steady_clock::now();
double elapsed = duration_cast<duration<double>>(now - last_time_).count();
last_time_ = now;
tokens_ = std::min(tokens_ + elapsed * rate_, rate_); // Cap at rate_
if (tokens_ >= 1.0) {
tokens_ -= 1.0;
return true;
}
return false;
}
};
Latency measurement:
struct PendingRequest {
std::chrono::steady_clock::time_point start_time;
// ... request state
};
// When response received:
auto latency = std::chrono::steady_clock::now() - pending.start_time;
histogram.record(latency.count());
Simple percentile calculation:
std::vector<double> latencies; // All recorded latencies
std::sort(latencies.begin(), latencies.end());
double p50 = latencies[latencies.size() * 0.50];
double p95 = latencies[latencies.size() * 0.95];
double p99 = latencies[latencies.size() * 0.99];
For production, use an HDR Histogram library for memory-efficient percentiles.
Ask yourself:
- How do you ensure timing is accurate despite system load?
- What if the server can’t keep up with your rate?
- How do you distinguish network latency from server processing time?
Learning milestones:
- Tool generates target request rate → You understand rate limiting
- Percentiles are accurate → You understand latency measurement
- Tool handles 10,000+ connections → You’ve applied epoll skills
- Results match other tools (wrk, ab) → You’ve validated correctness
Project 11: Thread Pool TCP Server
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: Rust, Go
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Concurrency, Thread Pool, Work Distribution
- Software or Tool: Application server pattern
- Main Book: “C++ Concurrency in Action” by Anthony Williams
What you’ll build: Combine epoll event loop with a thread pool: the main thread handles I/O multiplexing, worker threads process requests. This is how production servers handle CPU-intensive work without blocking the event loop.
Why it teaches network programming: Pure event loops are great for I/O-bound work but CPU-intensive tasks block everything. Real servers use thread pools to parallelize work. You’ll learn thread-safe queues, work distribution, and the producer-consumer pattern.
Core challenges you’ll face:
- Thread-safe work queue → maps to mutex + condition variable
- Distributing work to threads → maps to producer-consumer pattern
- Returning results to event loop → maps to eventfd for thread wakeup
- Graceful shutdown → maps to poison pills, joining threads
Key Concepts:
- Thread Pools: “C++ Concurrency in Action” Chapter 9 - Williams
- Condition Variables: “C++ Concurrency in Action” Chapter 4 - Williams
- eventfd for Thread Wakeup: “The Linux Programming Interface” Section 22.11 - Kerrisk
- Producer-Consumer: Classic concurrency pattern
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 9, familiarity with std::thread
Real world outcome:
$ ./threadpool_server -p 8080 -t 8
Starting server on port 8080 with 8 worker threads
# Simulating CPU-intensive work (e.g., JSON parsing, crypto):
$ ./benchmark_client localhost 8080 --requests 10000
Results:
Throughput: 15,000 req/s
Latency p99: 8.2ms
# Compare to single-threaded:
$ ./single_thread_server -p 8080
$ ./benchmark_client localhost 8080 --requests 10000
Results:
Throughput: 2,000 req/s # 7.5x slower!
Latency p99: 45ms
Implementation Hints:
Thread-safe queue:
template<typename T>
class ThreadSafeQueue {
std::queue<T> queue_;
std::mutex mutex_;
std::condition_variable cv_;
bool shutdown_ = false;
public:
void push(T item) {
{
std::lock_guard lock(mutex_);
queue_.push(std::move(item));
}
cv_.notify_one();
}
std::optional<T> pop() {
std::unique_lock lock(mutex_);
cv_.wait(lock, [this] { return !queue_.empty() || shutdown_; });
if (shutdown_ && queue_.empty()) return std::nullopt;
T item = std::move(queue_.front());
queue_.pop();
return item;
}
void shutdown() {
{
std::lock_guard lock(mutex_);
shutdown_ = true;
}
cv_.notify_all();
}
};
Thread pool:
class ThreadPool {
std::vector<std::thread> workers_;
ThreadSafeQueue<std::function<void()>> tasks_;
public:
ThreadPool(size_t num_threads) {
for (size_t i = 0; i < num_threads; i++) {
workers_.emplace_back([this] {
while (auto task = tasks_.pop()) {
(*task)();
}
});
}
}
void submit(std::function<void()> task) {
tasks_.push(std::move(task));
}
~ThreadPool() {
tasks_.shutdown();
for (auto& w : workers_) w.join();
}
};
Waking the event loop from worker threads (using eventfd):
int event_fd = eventfd(0, EFD_NONBLOCK);
// Add to epoll...
// Worker thread signals completion:
uint64_t val = 1;
write(event_fd, &val, sizeof(val));
// Event loop wakes up:
if (events[i].data.fd == event_fd) {
uint64_t val;
read(event_fd, &val, sizeof(val));
// Process completed work
process_completed_tasks();
}
Ask yourself:
- How do you avoid lock contention on the work queue?
- What if a worker thread crashes? How do you handle it?
- How do you match responses to the original connections?
Learning milestones:
- Work distributes across threads → You understand the queue pattern
- Event loop stays responsive → You understand separating I/O from compute
- No data races (ThreadSanitizer clean) → You understand thread safety
- Graceful shutdown works → You understand thread lifecycle
Project 12: WebSocket Chat Server
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: Rust, Go, JavaScript (Node)
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: WebSocket Protocol, HTTP Upgrade, Framing
- Software or Tool: Real-time communication backend
- Main Book: “High Performance Browser Networking” by Ilya Grigorik
What you’ll build: A WebSocket server that upgrades HTTP connections, handles the WebSocket framing protocol, and enables real-time bidirectional chat. Works with browser WebSocket clients.
Why it teaches network programming: WebSocket is a real protocol upgrade—it starts as HTTP, then transforms into a different binary framing format. You’ll learn the upgrade handshake, frame parsing (opcode, masking, fragmentation), and maintaining persistent connections.
Core challenges you’ll face:
- HTTP upgrade handshake → maps to Sec-WebSocket-Key, SHA-1, Base64
- Frame parsing → maps to opcode, length, masking
- Ping/pong for keepalive → maps to control frames
- Handling fragmented messages → maps to assembling frames
Key Concepts:
- WebSocket Protocol: RFC 6455
- HTTP Upgrade: “High Performance Browser Networking” Chapter 17 - Grigorik
- Framing Format: RFC 6455 Section 5
- SHA-1 and Base64: Standard cryptographic libraries
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Project 6, understanding of HTTP
Real world outcome:
$ ./websocket_server 8080
WebSocket server listening on port 8080
# Browser connects to ws://localhost:8080
# HTML client:
<script>
const ws = new WebSocket('ws://localhost:8080');
ws.onmessage = (e) => console.log('Received:', e.data);
ws.onopen = () => ws.send('Hello from browser!');
</script>
# Server logs:
[12:00:01] HTTP upgrade request from 127.0.0.1:54321
[12:00:01] WebSocket handshake complete
[12:00:02] Received TEXT: "Hello from browser!"
[12:00:02] Broadcasting to 3 clients
[12:00:05] Sending PING to 127.0.0.1:54321
[12:00:05] Received PONG
Implementation Hints:
WebSocket handshake:
// Client sends:
// GET /chat HTTP/1.1
// Upgrade: websocket
// Connection: Upgrade
// Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
// Sec-WebSocket-Version: 13
std::string compute_accept_key(const std::string& client_key) {
// Concatenate with magic GUID
std::string concat = client_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
// SHA-1 hash
unsigned char hash[20];
SHA1(concat.c_str(), concat.length(), hash);
// Base64 encode
return base64_encode(hash, 20);
}
// Server responds:
// HTTP/1.1 101 Switching Protocols
// Upgrade: websocket
// Connection: Upgrade
// Sec-WebSocket-Accept: <computed accept key>
WebSocket frame format:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+-------------------------------+
| Masking-key (if MASK==1) |
+-------------------------------+-------------------------------+
| Payload Data |
+---------------------------------------------------------------+
Opcodes:
- 0x1 = Text frame
- 0x2 = Binary frame
- 0x8 = Close
- 0x9 = Ping
- 0xA = Pong
Client-to-server frames are always masked:
void unmask(uint8_t* data, size_t len, const uint8_t mask[4]) {
for (size_t i = 0; i < len; i++) {
data[i] ^= mask[i % 4];
}
}
Ask yourself:
- Why are client frames masked but server frames aren’t?
- How do you handle a frame that spans multiple recv() calls?
- What happens if you receive a ping while sending a large message?
Learning milestones:
- Browser connects successfully → You understand the handshake
- Text messages work bidirectionally → You understand basic framing
- Ping/pong works → You understand control frames
- Large messages work → You understand extended length fields
Phase 4: Security
Project 13: HTTPS Client with OpenSSL
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: Rust (rustls), Go
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 3: Advanced
- Knowledge Area: TLS/SSL, Certificate Validation, OpenSSL API
- Software or Tool: Secure HTTP client
- Main Book: “Bulletproof SSL and TLS” by Ivan Ristić
What you’ll build: Extend your HTTP client (Project 5) to support HTTPS using OpenSSL. Properly validate server certificates, handle the TLS handshake, and send encrypted requests.
Why it teaches network programming: TLS is essential for production systems. You’ll learn the OpenSSL API (notoriously complex), certificate chain validation, and how TLS wraps an existing TCP connection. Understanding TLS errors is critical for debugging production issues.
Core challenges you’ll face:
- OpenSSL context setup → maps to SSL_CTX_new, loading CA certificates
- TLS handshake → maps to SSL_connect, error handling
- Certificate validation → maps to hostname verification, chain building
- Reading/writing through SSL → maps to SSL_read, SSL_write vs raw sockets
Key Concepts:
- OpenSSL API: “Network Security with OpenSSL” - Viega, Messier, Chandra
- TLS Handshake: “Bulletproof SSL and TLS” Chapter 2 - Ristić
- Certificate Validation: “Bulletproof SSL and TLS” Chapter 6 - Ristić
- SNI (Server Name Indication): Required for most HTTPS servers
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Project 5, basic understanding of certificates
Real world outcome:
$ ./https_client https://www.google.com/
TLS Handshake:
Protocol: TLSv1.3
Cipher: TLS_AES_256_GCM_SHA384
Certificate: CN=www.google.com
Issuer: CN=GTS CA 1C3
Valid: 2024-01-01 to 2024-03-25
HTTP/1.1 200 OK
Content-Type: text/html
...
$ ./https_client https://expired.badssl.com/
Error: certificate verify failed
Reason: certificate has expired
$ ./https_client https://wrong.host.badssl.com/
Error: certificate verify failed
Reason: hostname mismatch
$ ./https_client https://self-signed.badssl.com/
Error: certificate verify failed
Reason: self signed certificate
Implementation Hints:
OpenSSL context setup:
#include <openssl/ssl.h>
#include <openssl/err.h>
SSL_CTX* create_ssl_context() {
// Initialize OpenSSL
SSL_library_init();
SSL_load_error_strings();
// Create context
SSL_CTX* ctx = SSL_CTX_new(TLS_client_method());
// Load system CA certificates
SSL_CTX_set_default_verify_paths(ctx);
// Require certificate verification
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr);
return ctx;
}
Connecting with TLS:
SSL* ssl_connect(SSL_CTX* ctx, int socket_fd, const std::string& hostname) {
SSL* ssl = SSL_new(ctx);
SSL_set_fd(ssl, socket_fd);
// Set SNI hostname (required for most servers)
SSL_set_tlsext_host_name(ssl, hostname.c_str());
// Enable hostname verification
SSL_set1_host(ssl, hostname.c_str());
// Perform handshake
if (SSL_connect(ssl) != 1) {
ERR_print_errors_fp(stderr);
return nullptr;
}
return ssl;
}
Reading and writing:
// Replace read()/write() with:
int bytes_read = SSL_read(ssl, buffer, sizeof(buffer));
int bytes_written = SSL_write(ssl, data, length);
// Check for errors:
if (bytes_read <= 0) {
int err = SSL_get_error(ssl, bytes_read);
// Handle SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE, etc.
}
Certificate inspection:
void print_certificate_info(SSL* ssl) {
X509* cert = SSL_get_peer_certificate(ssl);
if (!cert) return;
char* subject = X509_NAME_oneline(X509_get_subject_name(cert), 0, 0);
char* issuer = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0);
printf("Subject: %s\n", subject);
printf("Issuer: %s\n", issuer);
OPENSSL_free(subject);
OPENSSL_free(issuer);
X509_free(cert);
}
Ask yourself:
- What happens if you don’t set SNI?
- Why is hostname verification separate from certificate validation?
- How do you handle SSL_ERROR_WANT_READ/WANT_WRITE?
Learning milestones:
- HTTPS to Google works → You understand basic TLS setup
- Bad certificates are rejected → You understand certificate validation
- SNI works → You understand virtual hosting with TLS
- TLSv1.3 negotiates → You understand protocol versions
Project 14: TLS Chat Server with Client Certificates
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: Rust, Go
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 3. The “Service & Support”
- Difficulty: Level 4: Expert
- Knowledge Area: Mutual TLS, PKI, Certificate Generation
- Software or Tool: Secure internal communication
- Main Book: “Bulletproof SSL and TLS” by Ivan Ristić
What you’ll build: A secure chat server that requires client certificates for authentication (mutual TLS). Generate your own CA, issue certificates, and build both server and client that verify each other.
Why it teaches network programming: Mutual TLS (mTLS) is how microservices authenticate each other in production systems (Kubernetes service mesh, zero-trust networks). You’ll learn to be a mini certificate authority, understand the full PKI chain, and implement server-side TLS.
Core challenges you’ll face:
- Certificate generation → maps to openssl commands, X.509 structure
- Server-side TLS → maps to SSL_accept, loading server cert/key
- Client certificate validation → maps to SSL_CTX_set_verify with callback
- Extracting client identity → maps to reading CN from client cert
Key Concepts:
- Mutual TLS: “Bulletproof SSL and TLS” Chapter 8 - Ristić
- Certificate Authority: “Bulletproof SSL and TLS” Chapter 12 - Ristić
- OpenSSL Server Setup: “Network Security with OpenSSL” Chapter 5 - Viega
- Zero Trust Architecture: NIST SP 800-207
Difficulty: Expert Time estimate: 2 weeks Prerequisites: Project 13, understanding of PKI
Real world outcome:
# Generate CA and certificates (setup script)
$ ./generate_certs.sh
Created: ca.crt, ca.key
Created: server.crt, server.key
Created: alice.crt, alice.key
Created: bob.crt, bob.key
# Start server
$ ./tls_chat_server --cert server.crt --key server.key --ca ca.crt
Secure chat server on port 9443
Requiring client certificates signed by our CA
# Client connects with certificate
$ ./tls_chat_client --cert alice.crt --key alice.key --ca ca.crt localhost 9443
Connected as alice@example.com (verified by server)
# Server log:
[12:00:01] TLS connection from 127.0.0.1:54321
[12:00:01] Client certificate: CN=alice@example.com
[12:00:01] Verified by CA: CN=My Chat CA
[12:00:02] <alice> Hello everyone!
# Unauthenticated client rejected:
$ ./tls_chat_client localhost 9443
Error: handshake failed - client certificate required
Implementation Hints:
Certificate generation (bash script with openssl):
# Generate CA
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 365 \
-out ca.crt -subj "/CN=My Chat CA"
# Generate server cert
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj "/CN=localhost"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 365 -sha256
# Generate client cert
openssl genrsa -out alice.key 2048
openssl req -new -key alice.key -out alice.csr -subj "/CN=alice@example.com"
openssl x509 -req -in alice.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out alice.crt -days 365 -sha256
Server TLS setup:
SSL_CTX* create_server_context() {
SSL_CTX* ctx = SSL_CTX_new(TLS_server_method());
// Load server certificate and key
SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM);
SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM);
// Load CA for verifying clients
SSL_CTX_load_verify_locations(ctx, "ca.crt", nullptr);
// Require client certificate
SSL_CTX_set_verify(ctx,
SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
nullptr);
return ctx;
}
Accept TLS connection:
SSL* ssl_accept(SSL_CTX* ctx, int client_fd) {
SSL* ssl = SSL_new(ctx);
SSL_set_fd(ssl, client_fd);
if (SSL_accept(ssl) != 1) {
ERR_print_errors_fp(stderr);
SSL_free(ssl);
return nullptr;
}
return ssl;
}
Extract client identity:
std::string get_client_cn(SSL* ssl) {
X509* cert = SSL_get_peer_certificate(ssl);
if (!cert) return "";
X509_NAME* subject = X509_get_subject_name(cert);
char cn[256];
X509_NAME_get_text_by_NID(subject, NID_commonName, cn, sizeof(cn));
X509_free(cert);
return cn;
}
Ask yourself:
- What happens if the client presents a cert signed by a different CA?
- How do you revoke a compromised client certificate?
- How does this compare to password authentication?
Learning milestones:
- Server requires client certs → You understand mTLS setup
- Bad client certs rejected → You understand validation
- Client identity extracted → You understand X.509 structure
- You generated your own CA → You understand PKI fundamentals
Phase 5: Modern C++ Networking
Project 15: io_uring Echo Server
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: C, Rust (with io-uring crate)
- Coolness Level: Level 5: Pure Magic
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 4: Expert
- Knowledge Area: io_uring, True Async I/O, Ring Buffers
- Software or Tool: Next-generation server runtime
- Main Book: “io_uring tutorial” by Shuveb Hussain (Lord of the io_ring)
What you’ll build: Reimplement your echo server using Linux’s io_uring interface—true asynchronous I/O with zero syscall overhead per operation. Use liburing for a cleaner API.
Why it teaches network programming: io_uring (introduced in Linux 5.1) is the future of Linux I/O. Unlike epoll (which tells you when to do I/O), io_uring does the actual I/O asynchronously. You’ll understand submission queues, completion queues, and how to batch syscalls.
Core challenges you’ll face:
- Understanding ring buffers → maps to SQ (submission) and CQ (completion)
- Submitting operations → maps to SQE preparation, submission
- Handling completions → maps to CQE processing, chaining
- Efficient buffer management → maps to registered buffers, buffer rings
Key Concepts:
- io_uring Fundamentals: https://unixism.net/loti/ (Lord of the io_ring)
- liburing API: https://github.com/axboe/liburing
- Zero-Copy I/O: io_uring and the future of async I/O - Jens Axboe (YouTube)
- Performance Comparison: io_uring vs epoll benchmarks
Difficulty: Expert Time estimate: 2 weeks Prerequisites: Project 9, Linux 5.10+
Real world outcome:
$ ./io_uring_echo_server 9000
io_uring echo server on port 9000
Ring size: 256 entries
Features: IORING_FEAT_NODROP, IORING_FEAT_SUBMIT_STABLE
# Benchmark comparison:
$ echo "Running epoll version..."
$ wrk -c 500 -t 4 -d 10 http://localhost:9001/ # epoll server
Requests/sec: 145,000
$ echo "Running io_uring version..."
$ wrk -c 500 -t 4 -d 10 http://localhost:9000/ # io_uring server
Requests/sec: 185,000 # ~27% faster!
# Server stats:
Submissions: 1,850,000
Completions: 1,850,000
Syscalls: 15,000 # Batched!
Ops/syscall: 123 average
Implementation Hints:
io_uring setup with liburing:
#include <liburing.h>
struct io_uring ring;
void setup_io_uring() {
io_uring_queue_init(256, &ring, 0); // 256 entries
}
The io_uring model:
┌─────────────────┐ ┌─────────────────┐
│ Application │ │ Kernel │
│ │ │ │
│ ┌───────────┐ │ Submit (no syscall) ┌───────────┐ │
│ │ SQ │──┼──────────────────>│ │ SQ │ │
│ │ (submit) │ │ │ │ │ │
│ └───────────┘ │ │ └───────────┘ │
│ │ │ │
│ ┌───────────┐ │ Complete (no syscall) ┌──────────┐ │
│ │ CQ │<─┼───────────────────│ │ CQ │ │
│ │ (complete)│ │ │ │ │ │
│ └───────────┘ │ │ └──────────┘ │
└─────────────────┘ └─────────────────┘
Submit accept operation:
void submit_accept(int listen_fd) {
struct io_uring_sqe* sqe = io_uring_get_sqe(&ring);
io_uring_prep_accept(sqe, listen_fd, nullptr, nullptr, 0);
sqe->user_data = ACCEPT_OP; // Tag to identify in completion
io_uring_submit(&ring);
}
Submit read operation:
void submit_read(int client_fd, void* buf, size_t len) {
struct io_uring_sqe* sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv(sqe, client_fd, buf, len, 0);
sqe->user_data = (uint64_t)client_fd | READ_OP;
io_uring_submit(&ring);
}
Event loop:
void event_loop() {
submit_accept(listen_fd); // Start accepting
while (true) {
struct io_uring_cqe* cqe;
io_uring_wait_cqe(&ring, &cqe); // Wait for completion
uint64_t data = cqe->user_data;
int result = cqe->res;
if (data == ACCEPT_OP) {
int client_fd = result;
submit_read(client_fd, get_buffer(client_fd), BUFFER_SIZE);
submit_accept(listen_fd); // Accept more
} else if ((data & OP_MASK) == READ_OP) {
int client_fd = data & FD_MASK;
if (result > 0) {
// Echo: submit write, then another read
submit_write(client_fd, get_buffer(client_fd), result);
} else {
close(client_fd);
}
} else if ((data & OP_MASK) == WRITE_OP) {
int client_fd = data & FD_MASK;
submit_read(client_fd, get_buffer(client_fd), BUFFER_SIZE);
}
io_uring_cqe_seen(&ring, cqe); // Mark as processed
}
}
Ask yourself:
- How does io_uring reduce syscall overhead compared to epoll?
- What happens if the submission queue is full?
- How do you link multiple operations (read then write)?
Learning milestones:
- Accept and echo work → You understand SQE/CQE basics
- Performance beats epoll → You understand batching benefits
- Linked operations work → You understand op chaining
- Registered buffers work → You understand zero-copy optimization
Project 16: C++20 Coroutine HTTP Server
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++20
- Alternative Programming Languages: Rust (async/await), Go (goroutines)
- Coolness Level: Level 5: Pure Magic
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 4: Expert
- Knowledge Area: C++20 Coroutines, Executors, Async Patterns
- Software or Tool: Modern async server framework
- Main Book: “C++20 - The Complete Guide” by Nicolai Josuttis
What you’ll build: An HTTP server using C++20 coroutines where handlers are written with co_await—looking like synchronous code but running asynchronously. Build on top of your epoll/io_uring code.
Why it teaches network programming: Coroutines are the future of C++ async programming. They let you write async code that looks synchronous, avoiding callback hell. You’ll learn the coroutine machinery (promise_type, awaitable), and how to integrate with I/O systems.
Core challenges you’ll face:
- Understanding coroutine mechanics → maps to promise_type, coroutine_handle
- Building awaitable types → maps to await_ready, await_suspend, await_resume
- Integrating with event loop → maps to resuming coroutines on I/O completion
- Error propagation → maps to exceptions in coroutines
Key Concepts:
- C++20 Coroutines: “C++20 - The Complete Guide” Chapters 14-16 - Josuttis
- Awaitable Types: “C++ Concurrency in Action, 2nd Ed” Chapter 13 - Williams
- Coroutine Theory: Lewis Baker’s blog posts (essential reading)
- Integration Patterns: Asio with coroutines
Difficulty: Expert Time estimate: 3 weeks Prerequisites: Projects 9 or 15, solid C++17 knowledge
Real world outcome:
// Handler code looks synchronous but runs async!
Task<HttpResponse> handle_request(HttpRequest req) {
std::string body = co_await read_file_async(req.path);
co_return HttpResponse{200, "OK", body};
}
Task<void> handle_connection(TcpStream stream) {
while (true) {
HttpRequest req = co_await stream.read_request();
HttpResponse resp = co_await handle_request(req);
co_await stream.write_response(resp);
}
}
$ ./coro_http_server 8080
C++20 coroutine HTTP server on port 8080
# Benchmark (looks like sync code, performs like async):
$ wrk -c 1000 -t 4 -d 10 http://localhost:8080/
Requests/sec: 120,000
# Server handles thousands of concurrent connections with readable code
Implementation Hints:
Basic coroutine task type:
template<typename T>
class Task {
public:
struct promise_type {
T value_;
std::exception_ptr exception_;
std::coroutine_handle<> continuation_;
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
auto final_suspend() noexcept {
struct Awaiter {
bool await_ready() noexcept { return false; }
std::coroutine_handle<> await_suspend(
std::coroutine_handle<promise_type> h) noexcept {
return h.promise().continuation_
? h.promise().continuation_
: std::noop_coroutine();
}
void await_resume() noexcept {}
};
return Awaiter{};
}
void return_value(T value) { value_ = std::move(value); }
void unhandled_exception() { exception_ = std::current_exception(); }
};
// Awaitable interface
bool await_ready() { return false; }
auto await_suspend(std::coroutine_handle<> continuation) {
handle_.promise().continuation_ = continuation;
return handle_; // Transfer to this coroutine
}
T await_resume() {
if (handle_.promise().exception_) {
std::rethrow_exception(handle_.promise().exception_);
}
return std::move(handle_.promise().value_);
}
private:
std::coroutine_handle<promise_type> handle_;
};
Awaitable for async I/O:
class AsyncRead {
int fd_;
void* buf_;
size_t len_;
EventLoop* loop_;
ssize_t result_ = -1;
public:
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
loop_->add_read(fd_, [this, h](int result) {
result_ = result;
h.resume(); // Resume coroutine when I/O completes
});
}
ssize_t await_resume() { return result_; }
};
// Usage:
ssize_t n = co_await AsyncRead{socket, buffer, sizeof(buffer), &loop};
Ask yourself:
- How do coroutines relate to threads?
- What’s the overhead of a coroutine compared to a callback?
- How do you cancel a suspended coroutine?
Learning milestones:
- Simple coroutine compiles → You understand basic machinery
- co_await chains work → You understand continuation passing
- I/O integration works → You understand the event loop bridge
- Error handling works → You understand exception propagation
Project 17: SOCKS5 Proxy Server
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++
- Alternative Programming Languages: Go, Rust
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Proxying, SOCKS Protocol, Traffic Forwarding
- Software or Tool: SSH alternative / privacy tool
- Main Book: “TCP/IP Illustrated, Volume 1” by W. Richard Stevens
What you’ll build: A SOCKS5 proxy server that accepts client connections, performs the SOCKS handshake, connects to target servers, and bidirectionally forwards traffic. Supports username/password authentication.
Why it teaches network programming: Proxy servers must manage pairs of connections and forward data efficiently in both directions. You’ll implement a real protocol (SOCKS5 is binary and well-specified), handle authentication, and manage bidirectional data flow.
Core challenges you’ll face:
- SOCKS5 handshake → maps to version negotiation, auth methods
- Connection establishment → maps to parsing connect requests, making outbound connections
- Bidirectional forwarding → maps to two connections per client, epoll on both
- Authentication → maps to username/password checking
Key Concepts:
- SOCKS5 Protocol: RFC 1928
- Username/Password Auth: RFC 1929
- Proxy Patterns: Man-in-the-middle architecture
- Connection Pairing: Managing related file descriptors
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 3, 9
Real world outcome:
$ ./socks5_proxy -p 1080 --auth users.txt
SOCKS5 proxy listening on port 1080
Authentication: username/password required
# Configure browser to use SOCKS5 proxy at localhost:1080
# Or use curl:
$ curl --socks5-hostname localhost:1080 -U user:pass https://ifconfig.me
203.0.113.45 # Traffic goes through proxy
$ curl --socks5-hostname localhost:1080 https://example.com
HTTP/1.1 200 OK
...
# Proxy logs:
[12:00:01] Client 127.0.0.1:54321 authenticated as 'user'
[12:00:01] CONNECT example.com:443
[12:00:02] Connection established, forwarding
[12:00:03] Transferred: 15.2 KB up, 45.6 KB down
[12:00:05] Connection closed
Implementation Hints:
SOCKS5 negotiation flow:
Client → Proxy:
+----+----------+----------+
|VER | NMETHODS | METHODS |
+----+----------+----------+
| 1 | 1 | 1 to 255 |
+----+----------+----------+
0x05 0x02 0x00 0x02 (version 5, 2 methods: no auth, user/pass)
Proxy → Client:
+----+--------+
|VER | METHOD |
+----+--------+
| 1 | 1 |
+----+--------+
0x05 0x02 (version 5, selected: user/pass)
Client → Proxy (if user/pass):
+----+------+----------+------+----------+
|VER | ULEN | UNAME | PLEN | PASSWD |
+----+------+----------+------+----------+
| 1 | 1 | 1 to 255 | 1 | 1 to 255 |
+----+------+----------+------+----------+
Proxy → Client:
+----+--------+
|VER | STATUS |
+----+--------+
| 1 | 1 |
+----+--------+
0x01 0x00 (auth version, success)
Connect request:
Client → Proxy:
+----+-----+-------+------+----------+----------+
|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | X'00' | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
CMD: 0x01 = CONNECT
ATYP: 0x01 = IPv4, 0x03 = Domain name, 0x04 = IPv6
Bidirectional forwarding (simplified):
void forward_loop(int client_fd, int target_fd) {
pollfd fds[2];
fds[0] = {client_fd, POLLIN, 0};
fds[1] = {target_fd, POLLIN, 0};
char buf[4096];
while (true) {
poll(fds, 2, -1);
if (fds[0].revents & POLLIN) {
int n = read(client_fd, buf, sizeof(buf));
if (n <= 0) break;
write(target_fd, buf, n); // Forward to target
}
if (fds[1].revents & POLLIN) {
int n = read(target_fd, buf, sizeof(buf));
if (n <= 0) break;
write(client_fd, buf, n); // Forward to client
}
}
}
Ask yourself:
- How do you handle DNS resolution for domain names?
- What if the target server is slow to connect?
- How do you handle TLS pass-through (CONNECT method)?
Learning milestones:
- SOCKS handshake works → You understand the protocol
- HTTP through proxy works → You understand connection establishment
- HTTPS through proxy works → You understand tunneling
- Auth works → You understand the auth sub-protocol
Final Project
Project 18: Production HTTP/2 Server with Complete Features
- File: LEARN_CPP_NETWORK_PROGRAMMING.md
- Main Programming Language: C++20
- Alternative Programming Languages: Rust, Go
- Coolness Level: Level 5: Pure Magic
- Business Potential: 5. The “Industry Disruptor”
- Difficulty: Level 5: Master
- Knowledge Area: HTTP/2, All Previous Concepts Combined
- Software or Tool: Full production server
- Main Book: “HTTP/2 in Action” by Barry Pollard
What you’ll build: A complete HTTP/2 server combining everything you’ve learned: io_uring or epoll, TLS with ALPN, HTTP/2 binary framing, multiplexed streams, header compression (HPACK), flow control, a thread pool for handlers, and C++20 coroutines for clean async code.
Why this is the capstone: This project requires everything: low-level socket management, binary protocol parsing, cryptographic integration, concurrency, and modern C++ features. HTTP/2 is significantly more complex than HTTP/1.1—it’s multiplexed, binary, and stateful. Building this proves mastery.
Core challenges you’ll face:
- ALPN negotiation → maps to TLS extension for protocol selection
- HTTP/2 frame parsing → maps to binary frames, variable-length headers
- Stream multiplexing → maps to multiple request/response pairs on one connection
- HPACK compression → maps to dynamic table, Huffman coding
- Flow control → maps to window updates, backpressure
Key Concepts:
- HTTP/2 Protocol: RFC 7540
- HPACK Compression: RFC 7541
- HTTP/2 Framing: “HTTP/2 in Action” Chapter 3 - Pollard
- ALPN: “Bulletproof SSL and TLS” - Ristić
Difficulty: Master Time estimate: 1-2 months Prerequisites: All previous projects, especially 6, 13-16
Real world outcome:
$ ./h2_server --port 443 --cert server.crt --key server.key \
--threads 8 --io-backend io_uring
HTTP/2 server starting...
Port: 443
TLS: enabled (ALPN: h2, http/1.1)
Backend: io_uring
Threads: 8
# Verify with curl:
$ curl -k --http2 https://localhost/
HTTP/2 200
content-type: text/html
...
# Check protocol:
$ curl -k -sI https://localhost | grep -i protocol
HTTP/2 200
# Load test:
$ h2load -n 100000 -c 100 -t 4 https://localhost/
finished in 2.5s
requests: 100000 total, 100000 started, 100000 done, 100000 succeeded
status codes: 100000 2xx
traffic: 45.00MB total, 44.80MB data
min/max/mean/sd: 0.5ms/25.3ms/2.1ms/1.8ms
requests/sec: 40,000
# Server handles concurrent streams:
$ nghttp -v https://localhost/page1 https://localhost/page2 &
[Stream 1] /page1 - 200 OK
[Stream 3] /page2 - 200 OK
# Both on same TCP connection!
Implementation Hints:
HTTP/2 frame format:
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) |
+---------------------------------------------------------------+
Frame types:
- DATA (0x0): Request/response bodies
- HEADERS (0x1): Headers (HPACK compressed)
- SETTINGS (0x4): Connection configuration
- WINDOW_UPDATE (0x8): Flow control
- GOAWAY (0x7): Graceful shutdown
ALPN negotiation:
// During TLS setup:
const unsigned char protos[] = "\x02h2\x08http/1.1";
SSL_CTX_set_alpn_protos(ctx, protos, sizeof(protos) - 1);
// Server callback:
SSL_CTX_set_alpn_select_cb(ctx, [](SSL*, const unsigned char** out,
unsigned char* outlen, const unsigned char* in, unsigned int inlen, void*) {
// Select h2 if client supports it
if (find_protocol(in, inlen, "h2")) {
*out = (unsigned char*)"h2";
*outlen = 2;
return SSL_TLSEXT_ERR_OK;
}
return SSL_TLSEXT_ERR_NOACK;
}, nullptr);
Connection preface:
// Client sends magic string + SETTINGS
const char* PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
// Server must verify preface, then send own SETTINGS
Stream management:
struct Stream {
uint32_t id;
StreamState state; // idle, open, half-closed, closed
std::vector<HeaderField> headers;
std::vector<char> body;
int32_t window; // Flow control
};
struct Connection {
SSL* ssl;
std::map<uint32_t, Stream> streams;
HpackDecoder hpack_decoder;
HpackEncoder hpack_encoder;
int32_t connection_window;
};
This is a massive project. Consider starting with a minimal implementation that passes basic h2load tests, then incrementally adding features.
Ask yourself:
- How does stream multiplexing differ from HTTP/1.1 pipelining?
- What happens when a stream’s flow control window is exhausted?
- How do you handle a GOAWAY frame mid-request?
Learning milestones:
- Connection preface accepted → You understand HTTP/2 bootstrap
- SETTINGS exchange works → You understand connection setup
- Single stream works → You understand basic framing
- Multiple concurrent streams → You understand multiplexing
- HPACK compression works → You understand header efficiency
- Flow control works → You understand backpressure
- h2load benchmark passes → Production-ready!
Project Comparison Table
| Project | Difficulty | Time | Depth of Understanding | Fun Factor |
|---|---|---|---|---|
| 1. DNS Query Tool | Beginner | Weekend | Medium | ⭐⭐⭐ |
| 2. TCP Echo Server | Beginner | Weekend | High (foundational) | ⭐⭐ |
| 3. Multi-Client Chat | Intermediate | 1 week | High | ⭐⭐⭐⭐ |
| 4. Port Scanner | Intermediate | 1 week | Medium | ⭐⭐⭐⭐ |
| 5. HTTP/1.1 Client | Intermediate | 1-2 weeks | High | ⭐⭐⭐ |
| 6. HTTP/1.1 Server | Advanced | 2 weeks | Very High | ⭐⭐⭐⭐ |
| 7. JSON-RPC Server | Intermediate | 1 week | Medium | ⭐⭐⭐ |
| 8. Protobuf Message Queue | Advanced | 2 weeks | High | ⭐⭐⭐ |
| 9. epoll Echo Server | Advanced | 1-2 weeks | Very High | ⭐⭐⭐⭐ |
| 10. HTTP Load Tester | Advanced | 2 weeks | High | ⭐⭐⭐⭐⭐ |
| 11. Thread Pool Server | Advanced | 2 weeks | Very High | ⭐⭐⭐ |
| 12. WebSocket Server | Advanced | 2 weeks | High | ⭐⭐⭐⭐⭐ |
| 13. HTTPS Client | Advanced | 1-2 weeks | High | ⭐⭐⭐ |
| 14. mTLS Chat Server | Expert | 2 weeks | Very High | ⭐⭐⭐⭐ |
| 15. io_uring Server | Expert | 2 weeks | Extreme | ⭐⭐⭐⭐⭐ |
| 16. Coroutine HTTP Server | Expert | 3 weeks | Extreme | ⭐⭐⭐⭐⭐ |
| 17. SOCKS5 Proxy | Advanced | 2 weeks | High | ⭐⭐⭐⭐ |
| 18. HTTP/2 Server | Master | 1-2 months | Maximum | ⭐⭐⭐⭐⭐ |
Recommended Learning Path
If you’re a complete beginner to network programming:
Start with Projects 1 → 2 → 3 → 4 → 5 → 6 in order. This builds foundational understanding.
If you already know basic sockets:
Start with Project 3 or 5, then 9 (epoll), then choose based on interest.
If you want to focus on performance:
Follow 9 → 10 → 11 → 15 → 16 for scaling techniques.
If you want to focus on security:
Follow 6 → 13 → 14 for TLS integration.
If you want to focus on modern C++:
Follow 11 → 16 for threading and coroutines.
Time-constrained (1 month intensive):
Projects 1, 2, 3, 5, 9, 13 give you solid fundamentals.
Comprehensive mastery (3-6 months):
All projects in order, culminating in Project 18.
Summary
| # | Project Name | Main Language |
|---|---|---|
| 1 | DNS Query Tool | C++ |
| 2 | TCP Echo Server & Client | C++ |
| 3 | Multi-Client Chat Server (select/poll) | C++ |
| 4 | TCP Port Scanner | C++ |
| 5 | HTTP/1.1 Client | C++ |
| 6 | HTTP/1.1 Server | C++ |
| 7 | JSON-RPC Server | C++ |
| 8 | Protocol Buffers Message Queue | C++ |
| 9 | High-Performance Server with epoll | C++ |
| 10 | HTTP Load Tester | C++ |
| 11 | Thread Pool TCP Server | C++ |
| 12 | WebSocket Chat Server | C++ |
| 13 | HTTPS Client with OpenSSL | C++ |
| 14 | TLS Chat Server with Client Certificates | C++ |
| 15 | io_uring Echo Server | C++ |
| 16 | C++20 Coroutine HTTP Server | C++20 |
| 17 | SOCKS5 Proxy Server | C++ |
| 18 | Production HTTP/2 Server | C++20 |
Essential Resources
Books (Priority Order)
- “UNIX Network Programming, Volume 1” by W. Richard Stevens - The sockets bible
- “The Linux Programming Interface” by Michael Kerrisk - Linux systems reference
- “TCP/IP Illustrated, Volume 1” by W. Richard Stevens - Protocol deep-dive
- “C++ Concurrency in Action, 2nd Ed” by Anthony Williams - Modern C++ threading
- “Bulletproof SSL and TLS” by Ivan Ristić - TLS security
- “High Performance Browser Networking” by Ilya Grigorik - HTTP/WebSocket/HTTP2
- “C++20 - The Complete Guide” by Nicolai Josuttis - Coroutines
Online Resources
- Beej’s Guide to Network Programming: https://beej.us/guide/bgnet/
- io_uring Tutorial (Lord of the io_ring): https://unixism.net/loti/
- Lewis Baker’s Coroutine Posts: Essential for C++20 coroutines
- RFC Editor: https://www.rfc-editor.org/ - Primary protocol specs
Tools for Development
- Wireshark: Packet capture and protocol analysis
- netcat (nc): Quick TCP/UDP testing
- curl: HTTP testing
- nghttp2: HTTP/2 testing tools
- wrk/h2load: Load testing
- valgrind/AddressSanitizer: Memory debugging
- ThreadSanitizer: Data race detection
This learning path will take you from network programming novice to someone who can build production-grade networked systems in C++. The key is consistent practice—each project builds on the last, and the final projects combine everything.