Skip to Content
Course content

216: Handling Multiple Connections with select()

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're a waiter in a busy diner. You have six different tables, and each one is undecided about their order. If you stood at Table 1 and refused to move until they told you what they wanted, the other five tables would starve, and your manager would probably fire you. That's essentially what happens when you call recv() on a single socket in a blocking manner—your entire program just freezes, waiting for one specific person to speak.

Instead, a professional waiter does something different: they scan the room. They glance at Table 1, then Table 2, then Table 3. They aren't taking orders yet; they're just looking for a "signal"—a raised hand or a closed menu. The moment someone signals, the waiter rushes over to that specific table, handles the request, and then immediately goes back to scanning the room. In C, select() is that scan of the room.

The Watchlist and the fd_set

To make this work, we can't just tell the OS "watch everything." We have to give it a specific list of file descriptors we care about. This is where fd_set comes in. Think of it as a checklist. You start with a blank sheet (FD_ZERO), and then you check off the sockets you're interested in (FD_SET).

Here is the tricky part that trips up almost everyone: select() is destructive. When you pass your checklist to the function, the OS scribbles all over it. It removes every socket that isn't ready and leaves only the ones that actually have data waiting for you. Because of this, you have to rebuild your checklist from scratch every single time you loop. I can't tell you how many hours I've wasted debugging "ghost" connections because I forgot to re-initialize my fd_set inside the loop.

Managing the Event Loop

When you call select(), the program pauses. But it doesn't pause on one socket; it pauses on the entire group. It wakes up the moment any one of those sockets becomes "ready" (meaning there's data in the buffer to read) or if a timeout occurs.

Once it wakes up, you don't know which socket triggered the event. You have to loop through your list and use FD_ISSET to check which "boxes" are still checked on your list. It's a bit tedious, but it's the only way to handle multiple clients without spawning a thousand threads and crashing your system.

Putting the Watcher to Work

Let's look at how this actually looks in code. We'll set up a server that can handle multiple clients simultaneously without using threads.

#include <stdio.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <unistd.h&gt>

#define MAX_CLIENTS 10
#define PORT 8080

int main() {
    int master_socket, client_sockets[MAX_CLIENTS] = {0};
    fd_set readfds;
    struct sockaddr_in address;
    int addrlen = sizeof(address);

    // ... (Assume standard socket(), bind(), and listen() setup here) ...
    master_socket = setup_server_socket(PORT); 

    while(1) {
        FD_ZERO(&readfds);
        // We add the master socket to watch for NEW connections
        FD_SET(master_socket, &readfds);
        
        // We also add every connected client to watch for DATA
        for (int i = 0; i < MAX_CLIENTS; i++) {
            if (client_sockets[i] > 0) FD_SET(client_sockets[i], &readfds);
        }

        // This is where the "scanning" happens. 
        // It blocks until something happens or the timeout (NULL) hits.
        int activity = select(MAX_CLIENTS + 1, &readfds, NULL, NULL, NULL);

        if (activity < 0) {
            perror("select error");
            break;
        }

        // Is it a new connection on the master socket?
        if (FD_ISSET(master_socket, &readfds)) {
            int new_socket = accept(master_socket, (struct sockaddr *)&address, (socklen_t*)&addrlen);
            // Add new_socket to our client_sockets array...
            printf("New connection established!\n");
        }

        // Or is it data from an existing client?
        for (int i = 0; i < MAX_CLIENTS; i++) {
            int sd = client_sockets[i];
            if (sd > 0 && FD_ISSET(sd, &readfds)) {
                char buffer[1024] = {0};
                int valread = read(sd, buffer, 1024);
                if (valread == 0) {
                    // Client disconnected
                    close(sd);
                    client_sockets[i] = 0;
                } else {
                    printf("Client %d says: %s\n", i, buffer);
                }
            }
        }
    }
    return 0;
}

Notice how the master_socket is treated differently than the client_sockets. The master socket is like the front door of the restaurant—when it's "ready," it means someone is walking in. The client sockets are like the tables—when they're "ready," it means someone is trying to order.




📋 Practical Task

Build a Multi-Source Log Monitor

Your task is to create a program that monitors two different sources of input simultaneously using select(): stdin (standard input) and a TCP server socket.

The program should behave as follows:

  • If you type something into the console and hit Enter, the program should print: "Local admin command received: [your text]".
  • If a remote client connects and sends a message, the program should print: "Remote client message received: [client text]".
  • The program must not block on read() or recv(); it must use select() to determine which source is ready.
  • If a remote client disconnects, the program should detect this and clean up the socket without crashing.

Constraint: You cannot use threads. Everything must happen in a single-threaded event loop using fd_set.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.