Sockets – Internet Domains The readLine() Utility – Reading Text Lines from Sockets

 

Chapter 59: Sockets – Internet Domains
Part 4 of 4  |  The readLine() Utility – Reading Text Lines from Sockets
readLine()
Line-by-line read
SOCK_STREAM
Byte-stream socket
Text Protocol
Newline terminated

Why Do We Need a readLine() Function?

In Part 3 we saw that a clean way to exchange data across heterogeneous systems is to use newline-delimited text. But reading exactly one line from a stream socket is trickier than it sounds.

You cannot use fgets() with a raw socket file descriptor directly in a portable way. You also cannot call read(fd, buf, 100) and assume it gives you exactly one line — stream sockets deliver a continuous byte stream, and a single read() may return part of a line, one full line, or multiple lines at once.

The solution is a helper function — readLine() — that reads one byte at a time until it finds a newline character.

Key Terms

readLine() Byte Stream Newline Delimiter Partial Read Null Terminator read() one byte EOF handling Buffer overflow protection Text Protocol ssize_t

The Byte-Stream Problem: Why read() Is Not Enough

A TCP socket is a byte stream. There are no message boundaries built in. If the sender calls write(fd, "hello\nworld\n", 12), the receiver’s read(fd, buf, 100) might get:

Possible Results of a Single read() Call
Case 1
Gets "hello\n"
(6 bytes — exactly one line)
Case 2
Gets "hel"
(only 3 bytes — partial line)
Case 3
Gets "hello\nworld\n"
(both lines at once)
Case 4
Gets "hello\nwor"
(one full + partial)
You cannot predict which case will happen — TCP controls when data arrives

The readLine() function solves this by reading one byte at a time, accumulating bytes into a buffer until it sees a '\n' or hits the end of file. This guarantees you always get exactly one complete line per call.

The readLine() Function – Interface and Implementation

The function signature (from TLPI’s read_line.h):

#include "read_line.h"

/*
 * readLine() - Read a newline-terminated line from file descriptor fd.
 *
 * Parameters:
 *   fd     - file descriptor to read from (socket, pipe, file, etc.)
 *   buffer - caller-provided buffer to store the result
 *   n      - size of the buffer in bytes
 *
 * Returns:
 *   > 0  : number of bytes placed in buffer (NOT counting the null terminator)
 *     0  : end of file (no bytes read)
 *    -1  : error (errno is set)
 *
 * Notes:
 *   - The returned string is always null-terminated.
 *   - At most (n-1) bytes of actual data are returned.
 *   - The newline character '\n' IS included in the buffer if found.
 */
ssize_t readLine(int fd, void *buffer, size_t n);

Full implementation:

#include <unistd.h>
#include <errno.h>

ssize_t readLine(int fd, void *buffer, size_t n) {
    ssize_t num_read;   /* bytes returned by read() each call */
    size_t  total_read; /* total bytes accumulated so far */
    char   *buf;
    char    ch;

    if (n <= 0 || buffer == NULL) {
        errno = EINVAL;
        return -1;
    }

    buf = (char *)buffer;
    total_read = 0;

    for (;;) {
        /* Read exactly one byte */
        num_read = read(fd, &ch, 1);

        if (num_read == -1) {
            if (errno == EINTR) {
                /* Interrupted by signal — retry */
                continue;
            } else {
                return -1;  /* Some other error */
            }
        } else if (num_read == 0) {
            /* End of file */
            if (total_read == 0) {
                return 0;   /* No data read at all */
            } else {
                break;      /* EOF after reading some data — return what we have */
            }
        } else {
            /* Got one byte — store it if buffer has space */
            if (total_read < n - 1) {
                total_read++;
                *buf++ = ch;
            }

            /* Stop after the newline */
            if (ch == '\n') {
                break;
            }
        }
    }

    /* Always null-terminate */
    *buf = '\0';
    return total_read;
}

readLine() execution flow when reading “hello\n” from a socket
Loop iteration read() returns ch total_read buffer state
1 1 ‘h’ 1 “h\0”
2 1 ‘e’ 2 “he\0”
3 1 ‘l’ 3 “hel\0”
4 1 ‘l’ 4 “hell\0”
5 1 ‘o’ 5 “hello\0”
6 1 ‘\n’ 6 “hello\n\0” ← BREAK, return 6

Complete Example: Echo Server Using readLine()

A minimal TCP echo server that reads complete text lines from clients and echoes them back. This demonstrates practical use of readLine() in a real server loop.

/* ===== echo_server.c ===== */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT     7000
#define BUF_SIZE 256

/* Inline readLine() implementation */
ssize_t readLine(int fd, void *buffer, size_t n) {
    ssize_t num_read;
    size_t  total_read = 0;
    char   *buf = (char *)buffer;
    char    ch;

    if (n <= 0 || buffer == NULL) return -1;

    for (;;) {
        num_read = read(fd, &ch, 1);
        if (num_read == -1) return -1;
        if (num_read == 0) {
            if (total_read == 0) return 0;
            else break;
        }
        if (total_read < n - 1) {
            total_read++;
            *buf++ = ch;
        }
        if (ch == '\n') break;
    }
    *buf = '\0';
    return (ssize_t)total_read;
}

/* Handle one connected client */
void handle_client(int cfd) {
    char buf[BUF_SIZE];
    ssize_t n;

    printf("Client connected.\n");

    /* Keep reading lines until client closes connection */
    while ((n = readLine(cfd, buf, BUF_SIZE)) > 0) {
        printf("Received (%zd bytes): %s", n, buf);

        /* Echo the line back */
        if (write(cfd, buf, n) != n) {
            perror("write");
            break;
        }
    }

    if (n == 0) {
        printf("Client disconnected.\n");
    } else if (n == -1) {
        perror("readLine");
    }
}

int main() {
    int sfd, cfd;
    struct sockaddr_in srv_addr;

    /* Create socket */
    sfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sfd == -1) { perror("socket"); exit(EXIT_FAILURE); }

    /* Allow reuse of port immediately after server restart */
    int opt = 1;
    setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    /* Bind */
    memset(&srv_addr, 0, sizeof(srv_addr));
    srv_addr.sin_family      = AF_INET;
    srv_addr.sin_port        = htons(PORT);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (bind(sfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) == -1) {
        perror("bind"); exit(EXIT_FAILURE);
    }

    if (listen(sfd, 5) == -1) { perror("listen"); exit(EXIT_FAILURE); }

    printf("Echo server listening on port %d\n", PORT);
    printf("Test with: telnet 127.0.0.1 %d\n\n", PORT);

    /* Simple single-client loop */
    for (;;) {
        cfd = accept(sfd, NULL, NULL);
        if (cfd == -1) { perror("accept"); continue; }
        handle_client(cfd);
        close(cfd);
    }

    close(sfd);
    return 0;
}

Compile and test:

# Compile
gcc -o echo_server echo_server.c

# Run server in one terminal
./echo_server

# Test in another terminal using telnet
telnet 127.0.0.1 7000
# Type lines and watch them echo back
# Press Ctrl+] then quit to exit telnet

# Or test with nc (netcat)
echo "Hello from client" | nc 127.0.0.1 7000

Handling the Trailing Newline in readLine() Output

readLine() keeps the '\n' in the buffer (just like fgets()). If you want to process the line without the newline, strip it:

#include <string.h>

void strip_newline(char *buf) {
    size_t len = strlen(buf);
    if (len > 0 && buf[len - 1] == '\n') {
        buf[len - 1] = '\0';
    }
}

/* Usage example */
char buf[256];
ssize_t n = readLine(fd, buf, sizeof(buf));
if (n > 0) {
    strip_newline(buf);
    printf("Line without newline: [%s]\n", buf);
    int value = atoi(buf);   /* Parse if it's a number */
}

Some protocols use \r\n (carriage return + newline) as the line terminator, for example HTTP/1.1 and SMTP. You may need to strip both:

void strip_crlf(char *buf) {
    size_t len = strlen(buf);
    /* Strip \n */
    if (len > 0 && buf[len - 1] == '\n') { buf[--len] = '\0'; }
    /* Strip \r */
    if (len > 0 && buf[len - 1] == '\r') { buf[--len] = '\0'; }
}

Performance Note: Buffered vs Unbuffered readLine()

The simple readLine() shown above calls read() once per byte. This is correct but slow for large volumes of data — each single-byte read() is a system call with overhead.

Unbuffered: 1 syscall per byte Buffered: 1 syscall per chunk
read(fd,&ch,1) → ‘h’
read(fd,&ch,1) → ‘e’
read(fd,&ch,1) → ‘l’
read(fd,&ch,1) → ‘l’
read(fd,&ch,1) → ‘o’
read(fd,&ch,1) → ‘\n’
= 6 system calls
read(fd, chunk, 4096) → “hello\nworld\nfoo\n…”

Scan chunk in memory for ‘\n’
Return line from buffer
Next call uses remaining buffer data
= 1 system call per 4096 bytes

A simple buffered version using an internal static buffer:

#include <unistd.h>
#include <string.h>

#define INNER_BUF_SIZE 4096

/*
 * Buffered readLine — reads a chunk at a time internally,
 * serves bytes from that chunk on successive calls.
 * NOTE: This version is NOT reentrant (uses static buffer).
 * For multi-threaded code, use thread-local storage or pass a context struct.
 */
ssize_t readLine_buffered(int fd, char *out_buf, size_t n) {
    static char  inner[INNER_BUF_SIZE];
    static int   buf_pos = 0;
    static int   buf_len = 0;

    size_t out_pos = 0;

    while (out_pos < n - 1) {
        /* Refill inner buffer if empty */
        if (buf_pos >= buf_len) {
            buf_len = (int)read(fd, inner, INNER_BUF_SIZE);
            if (buf_len <= 0) {
                if (out_pos == 0) return buf_len;  /* 0=EOF, -1=error */
                break;
            }
            buf_pos = 0;
        }

        char ch = inner[buf_pos++];
        out_buf[out_pos++] = ch;
        if (ch == '\n') break;
    }

    out_buf[out_pos] = '\0';
    return (ssize_t)out_pos;
}

For most learning exercises and interview questions, the simple single-byte version is fine. In production code handling many concurrent connections (e.g., using epoll), you would use a per-connection read buffer inside a connection context structure.

Chapter 59 Summary – All Parts
Part Topic Key Takeaways
Part 1 Internet Domain Sockets AF_INET/AF_INET6, TCP=reliable stream, UDP=unreliable datagram, sockaddr_in, getaddrinfo()
Part 2 Network Byte Order Big vs little endian, network order = big endian, always use htons/htonl/ntohs/ntohl
Part 3 Data Representation Struct padding issues, marshalling standards (XDR, ASN.1), text encoding with newlines
Part 4 readLine() Function Read 1 byte at a time until ‘\n’, null-terminate result, handle EOF and EINTR

Interview Questions – Part 4

Q1. Why can’t you use a single read() call to reliably get one line from a TCP socket?

TCP is a byte stream with no message boundaries. A single read() call may return fewer bytes than sent (partial read), exactly one line, or multiple lines together — depending on network conditions and kernel buffering. You cannot predict which will happen, so you must loop until you find the newline delimiter.

Q2. What does readLine() return to indicate end of file?

It returns 0 when EOF is reached with no bytes read. If some bytes were read before EOF, it returns the count of those bytes (the partial data is still useful). It returns -1 on a genuine read error.

Q3. Why does readLine() handle EINTR separately inside the loop?

A read() call can be interrupted by a signal before any bytes are transferred, in which case it returns -1 with errno == EINTR. This is not a real error — it just means the system call needs to be retried. Without this check, the function would wrongly report an error whenever a signal arrives during a blocking read.

Q4. Does readLine() include the newline character ‘\n’ in the returned buffer?

Yes, the newline is included in the buffer (same behaviour as fgets()). The returned count includes the newline byte. If you need to process the line without it, you must strip it manually — for example, by checking if the last byte before the null terminator is '\n' and replacing it with '\0'.

Q5. What is the maximum number of data bytes readLine() will place in a buffer of size n?

At most n - 1 bytes of actual data. One byte is always reserved for the null terminator. So if you pass n = 256, the buffer will contain at most 255 data bytes plus a null terminator at position 255.

Q6. What is the performance disadvantage of the simple single-byte readLine() implementation?

Each byte requires a separate read() system call. System calls have overhead (context switch to kernel and back). For a 100-byte line, this means 100 system calls. A buffered version reads data in large chunks (e.g., 4096 bytes) into an internal buffer and scans that buffer in user space, dramatically reducing system call count.

Q7. How would you test a text-protocol server without writing a client program?

Use telnet host port from the command line. Telnet connects to any TCP port and lets you type text lines that are sent to the server. The server’s responses are printed to your terminal. This works for any text protocol like HTTP, SMTP, POP3, or a custom newline-delimited protocol.

Q8. Is the static-buffer version of readLine_buffered() safe to use in multi-threaded programs?

No. The static buffer is shared across all calls, so concurrent threads reading from different file descriptors would corrupt each other’s data. For multi-threaded servers, the buffer must be part of a per-connection context structure allocated on the heap or stack, and passed into the function as a parameter.

Chapter 59 Complete!

You have covered Internet Domain Sockets, Network Byte Order, Data Representation, and the readLine() utility.

← Back to Part 1 EmbeddedPathashala Home

Leave a Reply

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