In our Project 1, you successfully built a simple chat application, allowing a client and server to communicate continuously. However, that server could only handle one client at a time. As soon as a client connected, the server would be entirely occupied with that single conversation, ignoring any other incoming connection requests. This limitation is a major bottleneck for any real-world network application that needs to serve multiple users, like a web server or a multi-user chat room.
This article addresses the critical challenge of server concurrency: how to design your server to handle many clients simultaneously. we will explore a powerful technique called I/O multiplexing using the select()
function and build a multi-client chat server that can broadcast messages to all connected users.
Recall our `chat_server.c` from Project 1. After binding and listening, the server executed `accept()`:
new_socket = accept(server_fd, ...); // Blocking call
This `accept()` call is blocking. It means the server process literally stops and waits indefinitely until a client connects. Once connected, the server enters its chat loop (`recv()` then `send()`, both blocking). During this entire conversation, if another client tries to connect, their connection request will be queued (up to the `backlog` limit you specify in `listen()`). The server won't `accept()` that new client until the current client disconnects. This is clearly insufficient for a concurrent environment.
To handle multiple clients without using separate processes or threads (which we will briefly discuss later), we employ a technique called I/O Multiplexing. This allows a single process to monitor multiple file descriptors (which include sockets, as sockets are treated like files in Unix-like systems) and wait for an event (like data ready to be read, or a new connection request) to occur on any of them.
The `select()` function is a classic way to achieve I/O multiplexing. It essentially asks the operating system, "Tell me which of these given sockets have data ready to be read, or if a new connection is waiting".
#include <sys/select.h> // For select() and fd_set macros
The `select()` function's signature is:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds
: The highest-numbered file descriptor in any of the three sets, plus one. (Often `max_fd + 1`). This parameter is ignored on Windows.
readfds
: A set of file descriptors to check for readability.
writefds
: A set of file descriptors to check for writability. (Less common for simple servers).
exceptfds
: A set of file descriptors to check for exceptional conditions. (Rarely used for basic sockets).
timeout
: A `timeval` structure that specifies how long `select()` should wait.
`NULL`: `select()` blocks indefinitely until an event occurs.
`{0, 0}`: `select()` returns immediately (non-blocking poll).
`{seconds, microseconds}`: `select()` blocks for a specified duration.
Return Value: On success, `select()` returns the number of file descriptors contained in the three returned descriptor sets (those ready for I/O). `0` indicates timeout. `-1` indicates an error.
To manipulate `fd_set` structures, you use these macros:
`FD_ZERO(&fdset)`: Clears all file descriptors from `fdset`.
`FD_SET(fd, &fdset)`: Adds `fd` to `fdset`.
`FD_CLR(fd, &fdset)`: Removes `fd` from `fdset`.
`FD_ISSET(fd, &fdset)`: Checks if `fd` is in `fdset` (returns non-zero if true).
While `select()` is widely available, it has limitations, notably the maximum number of file descriptors it can monitor (often 1024) and its efficiency (it iterates through all descriptors). For very high-performance servers with thousands of connections:
`poll()`: A more scalable alternative that uses an array of structures, overcoming the `fd_set` size limit.
`epoll()` (Linux-specific) / `kqueue()` (BSD/macOS): Highly efficient, event-driven mechanisms that notify you only of the ready file descriptors, avoiding the linear scan. These are used in production-grade servers.
For this article, we will focus on `select()` for its cross-platform availability and clear demonstration of I/O multiplexing.
Our objective is to create a more robust chat application where:
The server can accept and manage connections from multiple clients simultaneously.
When a client sends a message, the server broadcasts that message to all other currently connected clients.
Clients can send and receive messages continuously, similar to a real-time chat room.
The `select()`-based server will follow this general logic:
Initialize the listening socket (server_fd
) as before (`socket`, `bind`, `listen`).
Maintain a dynamic list (or array) of all currently connected client sockets.
In a main loop:
Create a temporary `fd_set` (because `select()` modifies it).
Add the listening socket and all connected client sockets to this `fd_set`.
Call `select()` to wait for activity on any of these sockets.
After `select()` returns, iterate through all possible socket descriptors:
If the listening socket is ready: This means a new client is trying to connect. Call `accept()`, add the new client's socket to your list of connected clients.
If a client socket is ready: This means data is available to be read from that client (or the client has disconnected).
Call `recv()` to read the message.
If `recv()` returns `0`, the client has disconnected; remove their socket from your list and close it.
If data is received, broadcast it: iterate through your list of other connected clients and `send()` the message to each one.
The client-side code will be largely similar to our `chat_client.c` from Project 1. It needs to continuously read user input and send messages, and also continuously receive and display messages that are broadcast by the server. Since `recv()` is a blocking call, a sophisticated client might also need `select()` if it wants to simultaneously monitor user input (`stdin`) and the network socket. However, for simplicity in this article, we will keep the client logic sequential (send then receive), which will still work for our chat demonstration.
Lower Resource Overhead: Compared to `fork()`, `select()` avoids creating new processes for each client, reducing memory and CPU context switching overhead.
Easier IPC (within the app): Since all connections are handled within a single process, sharing data (like the list of client sockets for broadcasting) is simpler.
Widely Available: `select()` is a standard POSIX function and is available on virtually all Unix-like systems and Windows (as part of Winsock).
Single Thread: Simplifies debugging of server logic (though complex event handling can still be tricky).
Complexity: The code structure for managing `fd_set` and iterating through all descriptors can become complex as the number of clients grows.
`fd_set` Size Limit (FD_SETSIZE): There is a compile-time limit to the number of file descriptors that `select()` can monitor simultaneously (often 1024 on Linux, but can vary). For very high-scale applications, this is a limitation.
Linear Scan: After `select()` returns, you still have to iterate through all possible file descriptors in the `fd_set` to find out which ones are ready. This can be inefficient with many idle connections.
Portability Issues: While `select()` is portable, its exact behavior (e.g., timeout precision, error codes) can sometimes have subtle differences across platforms.
Here is the code for our `select()` based multi-client chat server. The client code is largely the same as in Project 1.
multi_chat_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// --- Platform-specific includes and definitions ---
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib") // Link with ws2_32.lib
#define CLOSE_SOCKET closesocket
#define GET_LAST_ERROR WSAGetLastError
// Define ssize_t for Windows compatibility if not already defined
#ifndef _SSIZE_T_DEFINED
typedef SSIZE_T ssize_t;
#define _SSIZE_T_DEFINED
#endif
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h> // For close
#include <errno.h> // For errno
#define CLOSE_SOCKET close
#define GET_LAST_ERROR() errno
#endif
// ----------------------------------------------------
#define PORT 12345
#define MAX_CLIENTS 5 // Maximum number of concurrent clients
#define BUFFER_SIZE 1024
int main() {
// --- Windows-specific Winsock initialization ---
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
fprintf(stderr, "WSAStartup failed with error: %d\n", GET_LAST_ERROR());
return 1;
}
#endif
// ----------------------------------------------------
int master_socket, new_socket, activity, i, valread;
int client_sockets[MAX_CLIENTS]; // Array to hold client socket descriptors
int max_sd; // Maximum file descriptor number
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE];
// Set of socket descriptors (for select())
fd_set readfds;
// Initialize all client_sockets to 0 (empty)
for (i = 0; i < MAX_CLIENTS; i++) {
client_sockets[i] = 0;
}
// 1. Create master socket (listening socket)
if ((master_socket = socket(AF_INET, SOCK_STREAM, 0)) ==
#ifdef _WIN32
INVALID_SOCKET
#else
0
#endif
) {
fprintf(stderr, "master socket failed with error: %d\n", GET_LAST_ERROR());
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
// Optional: Set master socket to allow multiple connections
// This helps in quickly restarting the server if the port is in TIME_WAIT state
int opt = 1;
if (setsockopt(master_socket, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt)) < 0) {
perror("setsockopt failed");
CLOSE_SOCKET(master_socket);
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
// Prepare the sockaddr_in structure for binding
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 2. Bind the master socket to the specified IP and port
if (bind(master_socket, (struct sockaddr *)&address, addrlen) ==
#ifdef _WIN32
SOCKET_ERROR
#else
-1
#endif
) {
fprintf(stderr, "bind failed with error: %d\n", GET_LAST_ERROR());
CLOSE_SOCKET(master_socket);
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
// 3. Listen for incoming connections (max 3 pending connections)
if (listen(master_socket, 3) < 0) {
fprintf(stderr, "listen failed with error: %d\n", GET_LAST_ERROR());
CLOSE_SOCKET(master_socket);
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
printf("Multi-client Chat Server listening on port %d...\n", PORT);
printf("Waiting for connections...\n");
// Main server loop
while (1) {
// Clear the socket set
FD_ZERO(&readfds);
// Add master socket to set
FD_SET(master_socket, &readfds);
max_sd = master_socket;
// Add child sockets to set
for (i = 0; i < MAX_CLIENTS; i++) {
int sd = client_sockets[i];
// If valid socket descriptor exists, add to read list
if (sd > 0) {
FD_SET(sd, &readfds);
}
// Find the highest file descriptor number, needed for select()
if (sd > max_sd) {
max_sd = sd;
}
}
// 4. Wait for an activity on one of the sockets
// Timeout is NULL, so it will wait indefinitely
activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
if (activity ==
#ifdef _WIN32
SOCKET_ERROR
#else
-1
#endif
) {
fprintf(stderr, "select error: %d\n", GET_LAST_ERROR());
// In a real application, you might try to recover or exit gracefully.
// For this example, we will just break.
break;
}
// If something happened on the master socket, then It is an incoming connection
if (FD_ISSET(master_socket, &readfds)) {
if ((new_socket = accept(master_socket, (struct sockaddr *)&address, (socklen_t*)&addrlen)) ==
#ifdef _WIN32
INVALID_SOCKET
#else
-1
#endif
) {
fprintf(stderr, "accept failed with error: %d\n", GET_LAST_ERROR());
// In a real application, you might try to recover or exit gracefully.
break;
}
printf("New connection, socket fd is %d, IP: %s, Port: %d\n",
new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// Add new socket to array of sockets
for (i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i] == 0) { // Find an empty spot
client_sockets[i] = new_socket;
printf("Adding to list of sockets as %d\n", i);
break;
}
}
if (i == MAX_CLIENTS) { // No space for new client
printf("Max clients reached. Connection refused.\n");
const char *refuse_msg = "Server full. Please try again later.\n";
send(new_socket, refuse_msg, strlen(refuse_msg), 0);
CLOSE_SOCKET(new_socket);
}
}
// Else, It is some I/O operation on an existing client socket
for (i = 0; i < MAX_CLIENTS; i++) {
int sd = client_sockets[i]; // Current client socket descriptor
if (sd == 0) continue; // Skip if socket slot is empty
if (FD_ISSET(sd, &readfds)) { // If this client socket has activity
// Clear the buffer before reading new data
memset(buffer, 0, BUFFER_SIZE);
// Read incoming message
valread = recv(sd, buffer, BUFFER_SIZE - 1, 0);
if (valread == 0) { // Client disconnected
char client_ip[INET_ADDRSTRLEN];
// Get client info before closing socket
getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
inet_ntop(AF_INET, &address.sin_addr, client_ip, INET_ADDRSTRLEN);
printf("Client disconnected, IP %s, Port %d\n", client_ip, ntohs(address.sin_port));
// Close the socket and mark slot as free
CLOSE_SOCKET(sd);
client_sockets[i] = 0;
} else if (valread ==
#ifdef _WIN32
SOCKET_ERROR
#else
-1
#endif
) {
fprintf(stderr, "recv failed on socket %d with error: %d\n", sd, GET_LAST_ERROR());
// Consider closing the socket if It is a critical error
CLOSE_SOCKET(sd);
client_sockets[i] = 0;
} else { // Message received
buffer[valread] = '\0'; // Null-terminate
char client_ip[INET_ADDRSTRLEN];
getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
inet_ntop(AF_INET, &address.sin_addr, client_ip, INET_ADDRSTRLEN);
printf("Message from client %s:%d (socket %d): %s\n",
client_ip, ntohs(address.sin_port), sd, buffer);
// Check for server-side exit command (if server admin types 'quit')
if (strcmp(buffer, "quit_server") == 0 || strcmp(buffer, "exit_server") == 0) {
printf("Received server shutdown command from client %d. Shutting down...\n", sd);
// Send a shutdown message to all clients before exiting
const char *shutdown_msg = "Server is shutting down. Goodbye!\n";
for (int j = 0; j < MAX_CLIENTS; j++) {
if (client_sockets[j] != 0 && client_sockets[j] != sd) { // Send to others
send(client_sockets[j], shutdown_msg, strlen(shutdown_msg), 0);
}
}
CLOSE_SOCKET(sd);
client_sockets[i] = 0;
goto cleanup_and_exit; // Jump to cleanup
}
// --- Broadcast the message to all other connected clients ---
for (int j = 0; j < MAX_CLIENTS; j++) {
int dest_sd = client_sockets[j];
// Send to valid sockets, but not to the sender itself
if (dest_sd != 0 && dest_sd != sd) {
char broadcast_msg[BUFFER_SIZE + 50]; // Increased buffer for prefix
SNPRINTF(broadcast_msg, sizeof(broadcast_msg), "Client %d: %s", sd, buffer);
if (send(dest_sd, broadcast_msg, strlen(broadcast_msg), 0) < 0) {
fprintf(stderr, "send to client %d failed with error: %d\n", dest_sd, GET_LAST_ERROR());
// Handle error (e.g., client might have just disconnected)
}
}
}
}
}
}
}
cleanup_and_exit:
// Close all open sockets before exiting
for (i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i] != 0) {
CLOSE_SOCKET(client_sockets[i]);
}
}
CLOSE_SOCKET(master_socket);
printf("Server shut down.\n");
// --- Windows-specific Winsock cleanup ---
#ifdef _WIN32
WSACleanup();
#endif
// -----------------------------------------
return 0;
}
multi_chat_client.c
(Similar to `chat_client.c` from Project 1)The client-side code remains largely the same as our `chat_client.c` from Project 1. It continuously sends user input and receives messages. For this project, the sequential `send()` then `recv()` loop is sufficient as the server handles multiplexing.
You can copy `chat_client.c` from Project 1 and rename it to `multi_chat_client.c`. Just ensure the `PORT` and `SERVER_IP` constants match the server. The client will receive broadcast messages as if they were direct server messages.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// --- Platform-specific includes and definitions ---
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#define CLOSE_SOCKET closesocket
#define GET_LAST_ERROR WSAGetLastError
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#define CLOSE_SOCKET close
#define GET_LAST_ERROR() errno
#endif
// ----------------------------------------------------
#define PORT 12345
#define SERVER_IP "127.0.0.1" // Loopback address for local testing
#define BUFFER_SIZE 1024
int main() {
// --- Windows-specific Winsock initialization ---
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
fprintf(stderr, "WSAStartup failed with error: %d\n", GET_LAST_ERROR());
return 1;
}
#endif
// ----------------------------------------------------
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
char message[BUFFER_SIZE] = {0}; // Buffer for client's outgoing message
// 1. Create socket file descriptor
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) ==
#ifdef _WIN32
INVALID_SOCKET
#else
0
#endif
) {
fprintf(stderr, "socket creation failed with error: %d\n", GET_LAST_ERROR());
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
// Prepare the server address structure
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// Convert IPv4 address from text to binary form
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
fprintf(stderr, "Invalid address/ Address not supported\n");
CLOSE_SOCKET(sock);
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
// 2. Connect to the server
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) ==
#ifdef _WIN32
SOCKET_ERROR
#else
-1
#endif
) {
fprintf(stderr, "connection failed with error: %d\n", GET_LAST_ERROR());
CLOSE_SOCKET(sock);
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
printf("Connected to server %s:%d\n", SERVER_IP, PORT);
printf("Type 'quit' or 'exit' to end the chat.\n");
// Main chat loop
while (1) {
// --- Send message to server (from client's console input) ---
printf("You: ");
if (fgets(message, BUFFER_SIZE, stdin) == NULL) {
fprintf(stderr, "Error reading input.\n");
break;
}
message[strcspn(message, "\n")] = 0; // Remove trailing newline
// Check for client's exit command
if (strcmp(message, "quit") == 0 || strcmp(message, "exit") == 0) {
printf("Client requested to end chat. Closing connection.\n");
send(sock, message, strlen(message), 0); // Send quit command to server
break;
}
// Send message
if (send(sock, message, strlen(message), 0) < 0) {
fprintf(stderr, "send failed with error: %d\n", GET_LAST_ERROR());
break;
}
// --- Receive message from server ---
int valread = recv(sock, buffer, BUFFER_SIZE - 1, 0); // -1 for null termination space
if (valread > 0) {
buffer[valread] = '\0'; // Null-terminate the received data
printf("%s\n", buffer); // Server sends "Client X: message"
} else if (valread == 0) {
printf("Server disconnected.\n");
break;
} else {
fprintf(stderr, "recv failed with error: %d\n", GET_LAST_ERROR());
break;
}
}
// Close socket
CLOSE_SOCKET(sock);
printf("Client socket closed.\n");
// --- Windows-specific Winsock cleanup ---
#ifdef _WIN32
WSACleanup();
#endif
// -----------------------------------------
return 0;
}
To run your multi-client chat application:
Compile both programs:
Linux/WSL:
gcc multi_chat_server.c -o multi_chat_server
gcc multi_chat_client.c -o multi_chat_client
Windows (MinGW-w64):
gcc multi_chat_server.c -o multi_chat_server.exe -lws2_32
gcc multi_chat_client.c -o multi_chat_client.exe -lws2_32
Open multiple terminal windows:
In one terminal, run the server:
# Linux/WSL
./multi_chat_server
# Windows
multi_chat_server.exe
You should see: `Multi-client Chat Server listening on port 12345... Waiting for connections...`
In two or more separate terminals, run the client:
# Linux/WSL
./multi_chat_client
# Windows
multi_chat_client.exe
Each client should connect and confirm: `Connected to server 127.0.0.1:12345`
Start Chatting!
Type messages in any client terminal. You will see your message sent, and then You will receive messages from other clients (prefixed with "Client X:"). The server terminal will log all incoming and outgoing messages.
To end a client session, type `quit` or `exit`. The server will detect the disconnection and remove the client. To stop the server, you can type `Ctrl+C` in its terminal (or send a special "quit_server" message from a client, as included in the server code for demonstration purposes).
Example Interaction (simplified):
Server Terminal:
Multi-client Chat Server listening on port 12345...
Waiting for connections...
New connection, socket fd is 4, IP: 127.0.0.1, Port: XXXXX
Adding to list of sockets as 0
New connection, socket fd is 5, IP: 127.0.0.1, Port: YYYYY
Adding to list of sockets as 1
Message from client 127.0.0.1:XXXXX (socket 4): Hello everyone!
Message from client 127.0.0.1:YYYYY (socket 5): Hi Client 4!
Client disconnected, IP 127.0.0.1, Port XXXXX
Server shut down.
Client 1 Terminal:
Connected to server 127.0.0.1:12345
Type 'quit' or 'exit' to end the chat.
You: Hello everyone!
Client 5: Hi Client 4!
You: quit
Client requested to end chat. Closing connection.
Client socket closed.
Client 2 Terminal:
Connected to server 127.0.0.1:12345
Type 'quit' or 'exit' to end the chat.
Client 4: Hello everyone!
You: Hi Client 4!
Client 4: Client requested to end chat. Closing connection.
Congratulations! You have successfully built a multi-client chat server using `select()`, demonstrating how a single server process can manage multiple connections efficiently. This is a foundational concept for building any scalable network application.
You have now mastered TCP, UDP, and server concurrency. In the final article of this series, we will bring all these concepts together to build a significant real-world application: a simple HTTP web server in C! Get ready to serve your own web pages directly from your custom C program.
How to move your Email accounts from one hosting provider to another without losing any mails?
How to resolve the issue of receiving same email message multiple times when using Outlook?
Self Referential Data Structure in C - create a singly linked list
Mosquito Demystified - interesting facts about mosquitoes
Elements of the C Language - Identifiers, Keywords, Data types and Data objects
How to pass Structure as a parameter to a function in C?
Rajeev Kumar is the primary author of How2Lab. He is a B.Tech. from IIT Kanpur with several years of experience in IT education and Software development. He has taught a wide spectrum of people including fresh young talents, students of premier engineering colleges & management institutes, and IT professionals.
Rajeev has founded Computer Solutions & Web Services Worldwide. He has hands-on experience of building variety of websites and business applications, that include - SaaS based erp & e-commerce systems, and cloud deployed operations management software for health-care, manufacturing and other industries.