Linux Sockets: Internet Domains TCP Stream Socket Client-Server

 

Linux Sockets: Internet Domains
Chapter 59 Part 6 — File 3 of 4  |  TCP Stream Socket Client-Server
Pattern
Iterative Server
Protocol
TCP / Stream
Task
Sequence Numbers

The Application: Sequence Number Server

This is a classic Linux programming example that ties everything together: getaddrinfo(), socket creation, binding, listening, accepting, and reading/writing. The application is a sequence number server — clients connect and ask for a block of unique sequence numbers, and the server hands them out without duplication, even across many clients.

The same task was done earlier in the book using FIFOs (Chapter 44). Here we do it over TCP sockets, which allows clients on different machines to connect.

To handle portability (client and server may have different integer representations), all integers are sent as newline-terminated ASCII strings.

Key Terms
TCP server iterative server getaddrinfo AI_PASSIVE SO_REUSEADDR accept() wildcard address SIGPIPE readLine seqNum

59.11   Design Overview

What the Server Does

The server maintains a single integer: the current sequence number. When a client connects, the server:

  1. Accepts the TCP connection.
  2. Reads how many sequence numbers the client wants (sent as a newline-terminated string).
  3. Sends the client the starting number (as a newline-terminated string).
  4. Advances the counter by that amount.
  5. Closes the client connection and waits for the next one.

This is an iterative server — it serves one client at a time. A concurrent server (Chapter 60) would fork a child or use threads for each client.

Why ASCII Strings Instead of Raw Integers?

Integers stored in binary have different byte orders on different machines (big-endian vs little-endian). If the server and client are on different hardware, raw integer bytes would be misinterpreted. Sending ASCII (“42\n”) is universally portable without needing htonl/ntohl conversions.

Server Flow

Startup
getaddrinfo(AI_PASSIVE)
create + bind socket
SO_REUSEADDR
listen()
Loop (for each client)
accept() → get client addr
print client IP:port via getnameinfo()
Per-Client
readLine() → reqLen
send seqNum as string
seqNum += reqLen
close(connFd)
back to accept()

Common Header File — is_seqnum.h

Both the server and client share a header that includes necessary system headers and defines the TCP port and integer string length.

/* is_seqnum.h — shared by server and client */
#include <netinet/in.h>
#include <sys/socket.h>
#include <signal.h>
#include "read_line.h"   /* declaration of readLine() */
#include "tlpi_hdr.h"    /* standard TLPI error-handling header */

#define PORT_NUM "50000" /* TCP port number the server listens on */
#define INT_LEN  30      /* largest integer as a string, including '\n' */

Using a string "50000" for the port number (not an integer) is deliberate — getaddrinfo() accepts a service name or decimal port number as a string. This makes the code directly usable without conversion.

Server Steps — Detailed Walkthrough

Step q — Initialise Sequence Number

The server starts the sequence counter at 1 by default, or at a value given on the command line. This lets you restart the server and continue from where it left off.

int seqNum = (argc > 1) ? atoi(argv[1]) : 1;

Step r — Ignore SIGPIPE

If the server writes to a socket after the client has closed its end, the kernel sends SIGPIPE to the process. By default this kills the process. We use signal(SIGPIPE, SIG_IGN) so that write() fails with EPIPE instead, which the server can check and handle gracefully.

if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
    errExit("signal");

Step e — getaddrinfo with AI_PASSIVE

AI_PASSIVE means “I want to bind this socket, not connect”. The resulting socket address has the wildcard IP address (0.0.0.0 for IPv4, :: for IPv6), which means the server accepts connections on any of the host’s network interfaces.

struct addrinfo hints;
struct addrinfo *result, *rp;

memset(&hints, 0, sizeof(hints));
hints.ai_canonname = NULL;
hints.ai_addr      = NULL;
hints.ai_next      = NULL;
hints.ai_socktype  = SOCK_STREAM;    /* TCP */
hints.ai_family    = AF_UNSPEC;      /* IPv4 or IPv6 */
hints.ai_flags     = AI_PASSIVE;     /* wildcard address */

if (getaddrinfo(NULL, PORT_NUM, &hints, &result) != 0)
    errExit("getaddrinfo");

Step t/u — Loop Through Addresses Until One Works

getaddrinfo() may return multiple address structures (e.g., one for IPv4 and one for IPv6). The server loops through them and stops at the first one it can successfully create and bind a socket to.

int sfd = -1;
for (rp = result; rp != NULL; rp = rp->ai_next) {
    sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
    if (sfd == -1)
        continue;   /* try next */

    if (bind(sfd, rp->ai_addr, rp->ai_addrlen) == 0)
        break;      /* success */

    close(sfd);     /* bind failed — close and try next */
}

if (rp == NULL)
    fatal("Could not bind socket to any address");

freeaddrinfo(result);

Step y — SO_REUSEADDR

Without this option, if the server crashes and restarts within a few minutes, bind() fails because the port is still in TIME_WAIT state. SO_REUSEADDR allows the bind to succeed immediately. Every TCP server should set this.

int optval = 1;
if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1)
    errExit("setsockopt");

Step i — listen()

if (listen(sfd, SOMAXCONN) == -1)
    errExit("listen");

Step o — Main Accept Loop

for (;;) {
    struct sockaddr_storage claddr;
    socklen_t addrlen = sizeof(claddr);
    char addrStr[IS_ADDR_STR_LEN];
    char host[NI_MAXHOST];
    char service[NI_MAXSERV];

    /* Step a: accept a new connection */
    int cfd = accept(sfd, (struct sockaddr *)&claddr, &addrlen);
    if (cfd == -1) {
        errMsg("accept");
        continue;
    }

    /* Step s: print the client's address */
    if (getnameinfo((struct sockaddr *)&claddr, addrlen,
                    host, NI_MAXHOST,
                    service, NI_MAXSERV,
                    NI_NUMERICHOST | NI_NUMERICSERV) == 0)
        printf("Connection from (%s, %s)\n", host, service);
    else
        printf("Connection from unknown\n");

    /* Step d: read how many numbers the client wants */
    char reqLenStr[INT_LEN];
    if (readLine(cfd, reqLenStr, INT_LEN) <= 0) {
        close(cfd);
        continue;
    }
    int reqLen = atoi(reqLenStr);
    if (reqLen <= 0) {
        close(cfd);
        continue;
    }

    /* Step g: send the current seqNum as a string */
    char seqNumStr[INT_LEN];
    snprintf(seqNumStr, sizeof(seqNumStr), "%d\n", seqNum);
    if (write(cfd, seqNumStr, strlen(seqNumStr)) != (ssize_t)strlen(seqNumStr))
        fprintf(stderr, "Error on write\n");

    /* Step h: advance the counter */
    seqNum += reqLen;

    close(cfd);
}

Complete Minimal Self-Contained Server

Below is a stripped-down but complete TCP sequence number server that you can compile and run directly (without the TLPI helper headers):

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <netdb.h>

#define PORT   "50000"
#define BUF     64

static int seqNum = 1;

/* Read a newline-terminated line from fd into buf (max len bytes) */
static ssize_t read_line(int fd, char *buf, size_t len)
{
    ssize_t total = 0;
    char c;
    while (total < (ssize_t)(len - 1)) {
        ssize_t n = read(fd, &c, 1);
        if (n <= 0) break;
        buf[total++] = c;
        if (c == '\n') break;
    }
    buf[total] = '\0';
    return total;
}

int main(int argc, char *argv[])
{
    if (argc > 1) seqNum = atoi(argv[1]);

    signal(SIGPIPE, SIG_IGN);   /* handle broken pipe gracefully */

    /* --- getaddrinfo: wildcard address, TCP, passive --- */
    struct addrinfo hints = {0};
    hints.ai_family   = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags    = AI_PASSIVE;

    struct addrinfo *res;
    int gai = getaddrinfo(NULL, PORT, &hints, &res);
    if (gai != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai));
        return 1;
    }

    /* --- loop through addresses, bind first one that works --- */
    int sfd = -1;
    struct addrinfo *rp;
    for (rp = res; rp; rp = rp->ai_next) {
        sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
        if (sfd == -1) continue;

        int opt = 1;
        setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

        if (bind(sfd, rp->ai_addr, rp->ai_addrlen) == 0) break;
        close(sfd);
        sfd = -1;
    }
    freeaddrinfo(res);

    if (sfd == -1) { fprintf(stderr, "Could not bind\n"); return 1; }

    if (listen(sfd, 50) == -1) { perror("listen"); return 1; }
    printf("Server listening on port " PORT "\n");

    /* --- main accept loop (iterative) --- */
    for (;;) {
        struct sockaddr_storage caddr;
        socklen_t clen = sizeof(caddr);
        int cfd = accept(sfd, (struct sockaddr *)&caddr, &clen);
        if (cfd == -1) { perror("accept"); continue; }

        /* print client info */
        char host[NI_MAXHOST], serv[NI_MAXSERV];
        if (getnameinfo((struct sockaddr *)&caddr, clen,
                        host, sizeof(host), serv, sizeof(serv),
                        NI_NUMERICHOST | NI_NUMERICSERV) == 0)
            printf("Client: %s:%s\n", host, serv);

        /* read request: how many sequence numbers? */
        char buf[BUF];
        if (read_line(cfd, buf, sizeof(buf)) <= 0) { close(cfd); continue; }
        int reqLen = atoi(buf);

        /* send current seqNum */
        char reply[BUF];
        int n = snprintf(reply, sizeof(reply), "%d\n", seqNum);
        write(cfd, reply, n);

        seqNum += reqLen;
        close(cfd);
    }
}
/* Compile: gcc -o seqnum_server seqnum_server.c */
/* Run:     ./seqnum_server [start_number]        */

Simple Client to Test

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>

#define BUF 64

int main(int argc, char *argv[])
{
    if (argc < 3) {
        fprintf(stderr, "Usage: %s host count\n", argv[0]);
        return 1;
    }

    const char *host  = argv[1];
    const char *count = argv[2];   /* how many sequence numbers we want */

    struct addrinfo hints = {0};
    hints.ai_family   = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    struct addrinfo *res;
    int gai = getaddrinfo(host, "50000", &hints, &res);
    if (gai != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai));
        return 1;
    }

    int sfd = -1;
    struct addrinfo *rp;
    for (rp = res; rp; rp = rp->ai_next) {
        sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
        if (sfd == -1) continue;
        if (connect(sfd, rp->ai_addr, rp->ai_addrlen) == 0) break;
        close(sfd);
        sfd = -1;
    }
    freeaddrinfo(res);

    if (sfd == -1) { fprintf(stderr, "Could not connect\n"); return 1; }

    /* Send request: "3\n" asks for 3 sequence numbers */
    char msg[BUF];
    int len = snprintf(msg, sizeof(msg), "%s\n", count);
    write(sfd, msg, len);

    /* Read reply: starting sequence number */
    char reply[BUF];
    ssize_t n = read(sfd, reply, sizeof(reply) - 1);
    if (n > 0) {
        reply[n] = '\0';
        int start = atoi(reply);
        printf("Got sequence numbers: %d to %d\n",
               start, start + atoi(count) - 1);
    }

    close(sfd);
    return 0;
}
/* Compile: gcc -o seqnum_client seqnum_client.c          */
/* Run:     ./seqnum_client localhost 3                    */
/* Output:  Got sequence numbers: 1 to 3                  */
/* Run again: Got sequence numbers: 4 to 6                */

Key Design Decisions Summary
Decision Why
Use ASCII strings for integers Avoids byte-order problems between different hardware architectures
AI_PASSIVE flag Makes the server bind to the wildcard address — accepts connections on all interfaces
SO_REUSEADDR socket option Allows the server to restart and rebind immediately after crash, without waiting for TIME_WAIT to expire
Ignore SIGPIPE Prevents the server from dying if a client closes its end mid-write; write() returns EPIPE instead
Loop through getaddrinfo results The system may return both IPv4 and IPv6 addresses; trying each in order ensures the server binds to whatever the OS prefers
NI_NUMERICHOST | NI_NUMERICSERV in logging Avoids slow DNS reverse lookups for every new connection; numeric is faster and unambiguous

Interview Questions — TCP Client-Server

Q1. What is an iterative server and how does it differ from a concurrent server?

An iterative server handles one client completely before accepting the next. It is simple to write and fine for fast requests. A concurrent server uses fork(), threads, or select()/epoll() to handle multiple clients simultaneously. If the sequence number server had slow clients (e.g., sending over a high-latency link), other clients would be blocked waiting. A concurrent design solves this at the cost of added complexity.

Q2. Why pass NULL as the first argument to getaddrinfo() in the server?

The first argument is the hostname to resolve. In a server, you do not want to bind to a specific IP — you want the wildcard address so the server accepts connections on all network interfaces. Passing NULL with the AI_PASSIVE flag achieves this. The wildcard is 0.0.0.0 for IPv4 and :: for IPv6.

Q3. Why is SO_REUSEADDR important for TCP servers?

When a TCP connection closes, it enters a TIME_WAIT state for up to two minutes. During this time, the kernel refuses to let another process bind the same port. If the server crashes and is restarted, bind() would fail with EADDRINUSE. SO_REUSEADDR allows binding even if there are TIME_WAIT connections on the port, making server restarts seamless during development and production.

Q4. What is SIGPIPE and why does the server ignore it?

When a process writes to a socket (or pipe) whose reading end has been closed, the kernel sends SIGPIPE. The default action for SIGPIPE is to terminate the process. For a server, this would be catastrophic — one misbehaving client would kill the entire server. By setting signal(SIGPIPE, SIG_IGN), the signal is ignored and the write() call fails with EPIPE instead, which the server code can detect and handle.

Q5. Why does the server loop through multiple addrinfo results from getaddrinfo()?

On a dual-stack machine (both IPv4 and IPv6), getaddrinfo() may return two address structures. Socket creation or binding could fail for one family but succeed for another. The loop tries each in turn and stops at the first success. This makes the code portable without hard-coding which address family to use.

Q6. The server sends integers as ASCII strings. What is the alternative and why is it messier?

The alternative is to send raw binary integers. But a 32-bit integer has value 0x00000001 on a big-endian machine and 0x01000000 on a little-endian machine. You would need htonl() on the sender side and ntohl() on the receiver side. Using ASCII strings sidesteps this entirely — “1\n” is the same bytes on every machine.

Next: Comprehensive Interview Questions

File 4 brings together all interview questions from this chapter part in one reference sheet.

File 4 → ← File 2

Leave a Reply

Your email address will not be published. Required fields are marked *