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