Socket Programming in C: TCP and UDP for Production Systems
Master TCP and UDP socket programming in C with production-ready examples, debugging guides, and real-world incident analysis.
20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.
- ✓Basic knowledge of C programming (pointers, structs, functions).
- ✓Understanding of IP addresses and ports.
- ✓Familiarity with Linux/Unix command line and compilation with gcc.
- Sockets are endpoints for communication between processes over a network.
- TCP provides reliable, connection-oriented communication; UDP is connectionless and faster but unreliable.
- Key functions: socket(), bind(), listen(), accept(), connect(), send(), recv().
- Always check return values and handle errors gracefully.
- Use non-blocking I/O or select/poll for scalable servers.
Think of sockets like phone calls vs. text messages. TCP is like a phone call: you dial, connect, talk, and hang up. UDP is like sending a postcard: you drop it in the mail and hope it arrives. TCP ensures everything arrives in order; UDP is faster but can lose data.
Imagine you're building a chat application, a file transfer tool, or a multiplayer game. At the heart of these systems lies socket programming—the ability to send and receive data across a network. In C, sockets are the foundation of network communication, used by everything from web servers to database clients. This tutorial will take you from zero to production-ready TCP and UDP socket code. You'll learn the essential functions, handle errors like a pro, and avoid common pitfalls that cause crashes or security holes. We'll cover both TCP (reliable, ordered) and UDP (fast, lightweight) with real-world examples. By the end, you'll be able to write a simple HTTP server or a UDP-based game server. Let's dive into the world of Berkeley sockets.
1. Understanding Sockets: The Basics
A socket is an endpoint for communication between two machines. In C, socket programming uses the Berkeley sockets API. There are two main types: SOCK_STREAM (TCP) and SOCK_DGRAM (UDP). TCP provides reliable, ordered, error-checked delivery. UDP is connectionless and faster but unreliable. The socket() function creates a socket: int sock = socket(AF_INET, SOCK_STREAM, 0); AF_INET is for IPv4. Always check the return value; on error it returns -1. The socket descriptor is a file descriptor, so you can use read()/write() or send()/recv().
2. TCP Server: Bind, Listen, Accept
A TCP server follows these steps: create a socket, bind to an address and port, listen for incoming connections, and accept them. bind() associates the socket with a local address (struct sockaddr_in). listen() marks the socket as passive, ready to accept connections. accept() blocks until a client connects, returning a new socket for communication. Here's a simple echo server that sends back whatever it receives.
accept() with select() or epoll to handle multiple clients concurrently.3. TCP Client: Connect and Communicate
A TCP client creates a socket and connects to the server using connect(). It then sends and receives data. The client needs the server's IP address and port. Here's a client that connects to the echo server above.
4. UDP Server and Client: Connectionless Communication
UDP is connectionless. The server creates a socket, binds to a port, and then uses recvfrom() to receive data from any client. The client uses sendto() to send data. No connect() or accept() is needed. Here's a simple UDP echo server and client.
5. Handling Multiple Clients with select()
For a TCP server to handle multiple clients concurrently, you can use select() to monitor multiple file descriptors. select() blocks until one or more sockets are ready for I/O. This allows a single-threaded server to manage many connections. Here's a simple example that accepts new connections and echoes data from existing ones.
epoll() for better scalability with thousands of connections.6. Error Handling and Best Practices
Network programming is error-prone. Always check return values. Common errors: EINTR (interrupted system call), EAGAIN/EWOULDBLOCK (non-blocking socket no data), ECONNRESET (peer reset connection). Use perror() or strerror(errno) for logging. Set SO_REUSEADDR to avoid 'Address already in use'. For non-blocking sockets, handle EAGAIN properly. Also, be careful with byte ordering: use htons(), htonl(), ntohs(), ntohl() for port and IP. Always close sockets when done to avoid file descriptor leaks.
The Silent Connection Drop: A Production Outage
select() to handle multiple clients.- Always handle SIGPIPE in network servers to avoid silent crashes.
- Check return values of send/recv for errors and closed connections.
- Use non-blocking I/O or multiplexing for scalable servers.
- Test with concurrent clients to uncover race conditions.
- Log all errors with errno for debugging.
int opt=1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));netstat -tlnp | grep <port>| File | Command / Code | Purpose |
|---|---|---|
| create_socket.c | int main() { | 1. Understanding Sockets |
| tcp_server.c | int main() { | 2. TCP Server |
| tcp_client.c | int main() { | 3. TCP Client |
| udp_client.c | int main() { | 4. UDP Server and Client |
| tcp_select_server.c | int main() { | 5. Handling Multiple Clients with select() |
| error_handling.c | void handle_error(const char *msg) { | 6. Error Handling and Best Practices |
Key takeaways
Common mistakes to avoid
5 patternsForgetting to convert byte order with htons()
Not checking return values of send/recv
Using blocking sockets without handling EINTR
Not setting SO_REUSEADDR before bind
Ignoring SIGPIPE
Interview Questions on This Topic
Explain the steps to create a TCP server in C.
socket(), bind to address with bind(), listen for connections with listen(), accept a client with accept(), then send/recv data.Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.
That's C Basics. Mark it forged?
3 min read · try the examples if you haven't