C TCP/IP Socket Programming: Build Network Servers and Clients from Scratch

C TCP/IP Socket Programming: Build Network Servers and Clients from Scratch
Table of Contents
- The Network Stack: How TCP Fits In
- The Socket Connection Lifecycle
- Creating a TCP Server: Step by Step
- Creating a TCP Client
- Handling Multiple Clients: select() and poll()
- Non-Blocking Sockets and epoll (Linux)
- Sending and Receiving Complete Messages
- Socket Options: SO_REUSEADDR and TCP_NODELAY
- Building a Complete HTTP Echo Server
- Frequently Asked Questions
- Key Takeaway
The Network Stack: How TCP Fits In
The C socket API operates at the TCP/UDP layer. You don't manage IP routing or Ethernet framing — the OS kernel handles those. You deal with: connect endpoints (IP + port), send/receive byte streams (TCP), and file descriptor management.
The Socket Connection Lifecycle
Creating a TCP Server: Step by Step
Creating a TCP Client
Handling Multiple Clients: select() and poll()
The simple server above handles one client at a time. For multiple concurrent clients, use select() or poll():
[!NOTE]
select()has a limit ofFD_SETSIZE(typically 1024) file descriptors. For production servers handling thousands of connections, useepoll(Linux) orkqueue(macOS/BSD).
Non-Blocking Sockets and epoll (Linux)
For high-performance servers (10,000+ concurrent connections), epoll is the standard:
epoll with edge-triggered mode (EPOLLET) is the foundation of Nginx's event loop and the mechanism enabling its legendary 10,000+ concurrent connection handling.
Sending and Receiving Complete Messages
send() and recv() may not send/receive all bytes in one call for large messages. Production code must loop:
Socket Options: SO_REUSEADDR and TCP_NODELAY
Frequently Asked Questions
Why do I need htons() for port numbers?
Different CPU architectures store multi-byte numbers in different byte orders. x86-64 is little-endian; the internet uses big-endian (network byte order). htons() (host-to-network short) converts a 16-bit port number from your CPU's format to big-endian. Always use htons() for ports and htonl() for IP addresses.
What is the difference between send/recv and write/read on sockets?
On POSIX, write(fd, buf, n) and send(fd, buf, n, 0) are functionally identical for TCP sockets (with flags=0). send/recv provide the additional flags parameter for features like MSG_DONTWAIT (non-blocking) and MSG_PEEK (peek without consuming).
How does Nginx handle 10,000 concurrent connections?
Nginx uses a single-threaded event loop with epoll in edge-triggered mode. All sockets are non-blocking. When a socket has data, epoll notifies the event loop; the handler reads what's available and returns immediately (no blocking). One thread serves all connections through multiplexing.
What is the TIME_WAIT state?
After a TCP connection closes, the OS keeps the (source_ip, source_port, dest_ip, dest_port) tuple in TIME_WAIT for 2× Maximum Segment Lifetime (~60-120 seconds). This prevents old delayed packets from a previous connection being misinterpreted as belonging to a new connection on the same ports. SO_REUSEADDR bypasses this for the server's listening socket.
Key Takeaway
TCP socket programming is the Gateway to Networked Systems. The six-step server lifecycle (socket → bind → listen → accept → recv/send → close) is the template behind every web server, database network protocol, and distributed system message passing system ever written in C.
Once you understand blocking I/O with select/poll and non-blocking I/O with epoll, you have the foundation to understand (and contribute to) the Nginx, Redis, and PostgreSQL networking code at its core.
Read next: UDP Datagrams & Broadcast: Fast, Low-Latency Networking →
Part of the C Mastery Course — 30 modules from C basics to expert networking and systems engineering.
