Recap

In Day 1, we built the simplest possible TCP server in C:
a blocking echo server that handled one client at a time.

That worked fine for basic tests, but it had a major limitation —
a second client had to wait until the first one disconnected.

Today, we fix that by introducing fork().


The Idea: fork() per Client

Unix gives us a neat primitive: fork().
It clones the current process into a child,
which can run independently of the parent.

That means the parent process can keep listening for new clients,
while each child handles one client connection.

This “one child per client” model is the traditional
way to scale simple servers before more advanced mechanisms
(select, poll, epoll) came along.


The Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
/*
 * server_forked — Day 2: multi-client forking echo server
 *
 * Run:    ./server_forked [port]    (default 8080)
 * Test:   nc 127.0.0.1 8080
 *
 * Signals: SIGINT/SIGTERM trigger clean shutdown.
 * Limits:  
 *
 * WHY: Minimal baseline to compare against select/epoll in later labs.
 */

#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

// Use this to experiment and log various things to help learning. Feel free to
// add more logs as you see fit.
#define LOG(fmt, ...) fprintf(stderr, "[blocking] " fmt "\n", ##__VA_ARGS__)

static volatile sig_atomic_t g_stop = 0;

// WHY: Use sigaction *without* SA_RESTART so blocking syscalls return EINTR.
//      This lets the main loop notice g_stop promptly on Ctrl-C/TERM.
static void on_stop(int _) { 
    (void)_; g_stop = 1; 
}

static void install_signals(void) {
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = on_stop;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;                // <-- no SA_RESTART
    sigaction(SIGINT,  &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);  // allow `kill -TERM` too
}

static void on_sigchld(int _) { (void)_; while (waitpid(-1, NULL, WNOHANG) > 0) {} }

static void handle_client(int cfd) {
    char buf[4096];
    for (;;) {
        ssize_t n = read(cfd, buf, sizeof(buf));
        // log bytes echoed if you want this for debugging/learning
        // fprintf(stderr, "[child %d] read() -> %zd\n", getpid(), n);
        if (n == 0) {
            fprintf(stderr, "Client disconnected (fd=%d)\n", cfd);
            break;
        }
        if (n < 0) {
             if (errno == EINTR) continue; 
             perror("read"); 
             fprintf(stderr, "Client disconnected abruptly (fd=%d)\n", cfd);
             break; 
            }
        ssize_t off = 0;
        while (off < n) {
            ssize_t m = write(cfd, buf + off, n - off);
            if (m < 0) { if (errno == EINTR) continue; perror("write"); goto out; }
            off += m;
        }
    }
out:
    close(cfd);
    return;
}


int main(int argc, char **argv) {
    // For debugging, log when the server binary was built.
    fprintf(stderr, "server_forked built %s %s (pid=%d)\n",
        __DATE__, __TIME__, getpid()); 

    int port = (argc > 1) ? atoi(argv[1]) : 8080; // default port

    // setvbuf(stderr, NULL, _IONBF, 0); // Make stderr unbuffered for logging

    install_signals(); // setup Ctrl-C/TERM handler

    // --- install SIGCHLD handler to reap children ---
    struct sigaction sc;
    memset(&sc, 0, sizeof(sc));
    sc.sa_handler = on_sigchld;
    sigemptyset(&sc.sa_mask);
    sc.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    sigaction(SIGCHLD, &sc, NULL);

    // opens a TCP endpoint and returns its file descriptor, 
    // lfd=listen file descriptor
    int lfd = socket(AF_INET, SOCK_STREAM, 0);  
    if (lfd < 0) { perror("socket"); return 1; } // create socket

    // NOTE: Enables quick rebind after restarts (TIME_WAIT); not multi-bind magic.
    int yes = 1;
    if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) {
        perror("setsockopt"); return 1;
    }

    // bind to the given port on any/all local IPs
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(port);

    // bind the socket to the address/port
    if (bind(lfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { 
        perror("bind"); return 1; 
    }

    // start listening for incoming connections (max 128 queued clients)
    if (listen(lfd, 128) < 0) { 
        perror("listen"); return 1; 
    }

    fprintf(stderr, "[forked] listening on :%d (Ctrl-C to stop)\n", port);

    // main loop: accept, echo, close, repeat
    while (!g_stop) {
        // accept a new client
        struct sockaddr_in cli; socklen_t clilen = sizeof(cli);

        // accept() blocks until a new client connects. cfd=client file descriptor
        int cfd = accept(lfd, (struct sockaddr*)&cli, &clilen); 

        // WHY: accept() returns EINTR if interrupted by signal (e.g. Ctrl-C).
        if (cfd < 0) {
            if (errno == EINTR) {           // interrupted by signal
                if (g_stop) break;          // exit cleanly
                continue;                   // else, retry
            }
            perror("accept");
            continue;
        }

        // log the new connection
        char ip[64]; inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip));
        fprintf(stderr, "client connected %s:%d\n", ip, ntohs(cli.sin_port));

        // echo loop: read until EOF, echo back what we received
         pid_t pid = fork();
        if (pid < 0) { perror("fork"); close(cfd); continue; }
        if (pid == 0) { // child
            close(lfd);
            handle_client(cfd);
            _exit(0);
        } else { // parent
            close(cfd);
        }
    }

    close(lfd);
    fprintf(stderr, "bye\n");
    return 0;
}

How It Works

  • Parent

    • Creates the listening socket.
    • Accepts new connections.
    • Forks a child process for each client.
    • Closes its copy of the client socket. (Immediately after the fork we have two references to the same connection, we don’t want that!)
    • Keeps listening for new clients.
  • Child

    • Closes the its reference to the listening socket (There are also two references to this socket, and only the parent should accept).
    • Runs the echo loop on the client socket.
    • Closes the client socket and exits when done.
  • Signals

    • SIGINT/SIGTERM let us stop the server gracefully.
    • SIGCHLD ensures finished children are reaped,
      so we don’t accumulate zombies.

Testing

Open two terminals:

Server:

1
./server_forked 8080

Clients:

1
nc 127.0.0.1 8080

Open as many nc clients as you like —
each gets its own forked process and runs independently.


Why This Matters

This pattern — fork per connection
was common in early Unix daemons (e.g. inetd, sshd).
It’s simple and isolates clients, but it doesn’t scale well
if you have thousands of connections (process overhead).

That’s why in future labs we’ll look at select(), poll(), and epoll()
for multiplexing many clients in one process.


Next

  • Day 3: multiplexing with select()
  • Day 4: non-blocking sockets
  • Day 5: epoll for high scalability