TCP client-server programming in C is a critical skill for systems developers, backend engineers, and anyone dealing with low-level networking. It’s the foundation of everything from chat servers and IoT systems to custom network daemons.
This guide will take you from basic theory to a multithreaded TCP server, explaining every step in between.
What is TCP Client-Server Programming?
In TCP client-server architecture, one program (server) waits for connections, and others (clients) initiate communication.
- TCP ensures reliable, ordered delivery of data.
- Server runs continuously, listens on a port.
- Client connects to the server, sends and receives data.
This is connection-oriented programming—both sides perform a “handshake” to establish a session.

Understanding TCP/IP and Sockets
Before writing code, let’s understand what happens under the hood.
Layer | Protocol | Purpose |
Application | HTTP, FTP, Custom | You write this! |
Transport | TCP | Reliable byte stream |
Network | IP | Routing and addressing |
Link | Ethernet, Wi-Fi | Hardware transmission |
Sockets are programming abstractions to communicate between these layers.
In C, we use system calls like:
- socket(): create a socket
- bind(): assign IP/port
- listen(): wait for connections
- accept(): accept a connection
- connect(): client connects to server
- send()/recv() or read()/write(): data exchange
ALSO READ:
Socket API in C (Cheat Sheet)
Function | Role |
socket() | Create socket |
bind() | Bind socket to IP/port |
listen() | Listen for incoming connections |
accept() | Accept a new client |
connect() | Initiate a connection |
send()/recv() | Send/receive data |
close() | Close socket |
All of them return -1 on failure and set errno.
Simple TCP Server Code
// tcp_server.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #define PORT 9090 #define BUFFER_SIZE 1024 int main() { int server_fd, new_socket; struct sockaddr_in address; char buffer[BUFFER_SIZE] = {0}; int opt = 1; socklen_t addrlen = sizeof(address); // 1. Create socket server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 2. Set socket options (optional but recommended) setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 3. Bind address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); bind(server_fd, (struct sockaddr *)&address, sizeof(address)); // 4. Listen listen(server_fd, 5); printf("Server is listening on port %d...\n", PORT); // 5. Accept new_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen); read(new_socket, buffer, BUFFER_SIZE); printf("Client says: %s\n", buffer); // 6. Reply send(new_socket, "Welcome to the server!", 23, 0); // 7. Close close(new_socket); close(server_fd); return 0; }
Simple TCP Client Code
// tcp_client.c #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #define PORT 9090 int main() { int sock; struct sockaddr_in serv_addr; char buffer[1024] = {0}; char *msg = "Hello, Server!"; sock = socket(AF_INET, SOCK_STREAM, 0); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr); connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); send(sock, msg, strlen(msg), 0); read(sock, buffer, 1024); printf("Server reply: %s\n", buffer); close(sock); return 0; }
How to Compile and Run
gcc tcp_server.c -o server gcc tcp_client.c -o client
Open two terminals and run the following commands:
- Run the server: ./server
- Run the client: ./client

Handling Multiple Clients with Threads
Let’s make it a true server that can handle many clients simultaneously using POSIX threads.
// multi_server.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <arpa/inet.h> #define PORT 9090 void* handle_client(void* socket_desc) { int sock = *(int*)socket_desc; char buffer[1024]; read(sock, buffer, 1024); printf("Client: %s\n", buffer); send(sock, "Response from threaded server", 30, 0); close(sock); free(socket_desc); return NULL; } int main() { int server_fd, new_socket; struct sockaddr_in address; socklen_t addrlen = sizeof(address); server_fd = socket(AF_INET, SOCK_STREAM, 0); address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); bind(server_fd, (struct sockaddr*)&address, sizeof(address)); listen(server_fd, 10); printf("Threaded server listening on port %d...\n", PORT); while (1) { new_socket = accept(server_fd, (struct sockaddr*)&address, &addrlen); int *new_sock = malloc(sizeof(int)); *new_sock = new_socket; pthread_t thread_id; pthread_create(&thread_id, NULL, handle_client, (void*)new_sock); pthread_detach(thread_id); // auto-free thread } close(server_fd); return 0; }

Best Practices & Error Handling
- Always check return values of socket functions.
- Use perror() or strerror(errno) for diagnostics.
- Use setsockopt() to avoid “address already in use” errors.
- Gracefully close sockets using shutdown() + close().
Real-World Applications
Use Case | How |
Chat apps | Socket server with message loop |
IoT gateway | Clients are sensors reporting data |
Multiplayer games | Game loop over TCP |
Remote shells | Use bidirectional send/recv |
File transfer tools | Read and write byte streams |
Conclusion
You’ve just completed the ultimate guide to TCP client-server programming in C. From theory to a real-world multithreaded server, you’ve seen how TCP sockets are used to build reliable communication systems.
Now It’s Your Turn:
- Modify the client to send user input
- Make the server broadcast to all connected clients
- Add logging, message timestamps, and authentication