Sockets: Internet Domains readLine() Utility Function

 

Chapter 59 โ€“ Sockets: Internet Domains
Part 1 of 3 ย |ย  The readLine() Utility Function
๐Ÿ“ก Topic
Socket I/O
๐Ÿ“š Source
TLPI Ch 59
๐ŸŽฏ Level
Intermediate

Why Do We Need readLine()?

When building networked applications, servers and clients often communicate using line-based text protocols โ€” each command or response ends with a newline character \n. HTTP/1.x, SMTP, FTP, and many custom protocols all work this way.

The standard read() system call does not understand lines. It just reads raw bytes. So if a client sends "HELLO\n", a single read() might return only "HEL" or might bundle it with the next message. We need a helper that reads exactly one line at a time. That helper is readLine().

Key Terms in This Section

readLine() read() syscall EINTR line-based protocol null terminator EOF handling buffer overflow newline detection

1. Function Signature and What It Does

readLine() is a helper function (not part of the standard C library or POSIX). It is defined in the TLPI source as:

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

ssize_t readLine(int fd, void *buffer, size_t n);

Parameters:

  • fd โ€” file descriptor (typically a connected socket)
  • buffer โ€” caller-supplied buffer where the line will be stored
  • n โ€” total size of the buffer in bytes

Return value:

  • Number of bytes placed in buffer (including the newline if present)
  • 0 โ€” if EOF was reached and no bytes were read
  • -1 โ€” on error, with errno set

2. How readLine() Works โ€” Step by Step

The function reads the file descriptor one byte at a time in a loop. Each byte goes into a local variable ch. Here is the logic of each iteration:

Condition from read() What it means Action taken
numRead == -1 and errno == EINTR Signal interrupted the read Loop again โ€” restart read()
numRead == -1 (other errno) Real I/O error Return -1 immediately
numRead == 0 and totRead == 0 EOF, no bytes read yet Return 0
numRead == 0 and totRead > 0 EOF after some bytes Break loop, add null terminator
numRead == 1 and buffer not full Normal byte read Append byte to buffer
ch == '\n' End of line reached Break loop (newline included in result)
Buffer full (totRead >= n-1) Line longer than buffer Discard byte, keep reading until newline

After the loop exits for any reason, the function appends a null byte '\0' so the caller gets a proper C string.

3. Full Implementation with Inline Comments
ssize_t
readLine(int fd, void *buffer, size_t n)
{
    ssize_t numRead;   /* bytes fetched by last read() call */
    size_t  totRead;   /* running total of bytes stored     */
    char   *buf;
    char    ch;

    /* Basic sanity checks */
    if (n <= 0 || buffer == NULL) {
        errno = EINVAL;
        return -1;
    }

    buf = buffer;   /* cast void* to char* for pointer arithmetic */
    totRead = 0;

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

        if (numRead == -1) {
            if (errno == EINTR)
                continue;   /* interrupted by signal, retry */
            else
                return -1;  /* real error */

        } else if (numRead == 0) {   /* EOF */
            if (totRead == 0)
                return 0;   /* no bytes at all, clean EOF */
            else
                break;      /* partial line at EOF, return what we have */

        } else {   /* numRead == 1 */
            if (totRead < n - 1) {   /* still room in buffer */
                totRead++;
                *buf++ = ch;
            }
            /* Note: if buffer is full, we still read until '\n'
               to consume the rest of the line from the socket,
               but we don't store the extra bytes.              */

            if (ch == '\n')
                break;      /* end of line found, stop */
        }
    }

    *buf = '\0';     /* null-terminate the result */
    return totRead;
}

4. Buffer Overflow Handling โ€” The Key Design Decision

This is the most important thing to understand about readLine(). When a line from the network is longer than the buffer you provided:

  • Only the first n-1 bytes are stored.
  • The function keeps reading (and discarding) bytes until it sees the \n.
  • The caller can detect overflow by checking: does the returned buffer end with \n before the \0?

Approach What happens with a long line Risk
Stop reading at buffer limit (leave rest in socket buffer) Next call reads the tail of the previous line Protocol desynchronization โ€” server thinks it is processing line N+1 when it is still on line N
readLine() approach: discard excess, consume full line Excess bytes thrown away Data loss, but protocol stays in sync

For well-designed protocols this is acceptable because a compliant peer will never send lines longer than the agreed maximum.

5. Practical Usage Example โ€” Simple Echo Client
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include "read_line.h"   /* provides readLine() */

#define BUF_SIZE  256

/* Reads one line from socket and prints it */
void handle_response(int sockfd)
{
    char buf[BUF_SIZE];
    ssize_t n;

    n = readLine(sockfd, buf, BUF_SIZE);

    if (n == -1) {
        perror("readLine");
        return;
    }

    if (n == 0) {
        printf("Server closed connection\n");
        return;
    }

    /* Check if line was truncated (no \n before \0) */
    if (buf[n - 1] != '\n')
        printf("[TRUNCATED] ");

    printf("Server said: %s", buf);
}

Notice how the caller checks buf[n-1] != '\n' to detect truncation. This is the idiom the TLPI book recommends.

6. Understanding EINTR โ€” Interrupted System Calls

On Linux, a read() call can be interrupted by a signal while it is blocking. When that happens:

  • read() returns -1
  • errno is set to EINTR
  • No data was actually read

The correct action is to retry the system call. This is exactly what readLine() does with the continue statement inside the if (errno == EINTR) branch.

/* The EINTR handling pattern โ€” used everywhere in Unix I/O */
for (;;) {
    numRead = read(fd, &ch, 1);
    if (numRead == -1) {
        if (errno == EINTR)
            continue;   /* signal interrupted us โ€” try again */
        else
            return -1;  /* real error โ€” give up */
    }
    /* ... rest of logic ... */
}

On newer Linux kernels you can also use the SA_RESTART flag when installing signal handlers, which makes most system calls automatically restart on EINTR. But writing explicit retry loops is safer and more portable.

7. The read_line.h Header

To use readLine() in your project, create a header:

/* read_line.h */
#ifndef READ_LINE_H
#define READ_LINE_H

#include <sys/types.h>

ssize_t readLine(int fd, void *buffer, size_t n);

#endif

Include this header in any source file that calls readLine(), and compile read_line.c alongside your program.

๐ŸŽฏ Interview Questions โ€” readLine() and Socket I/O

Q1. Why does readLine() read one byte at a time instead of using fgets()?

fgets() uses stdio buffering, which is not safe with sockets because the internal buffer may read ahead and consume bytes that belong to the next message. Reading one byte at a time gives exact control and avoids buffering issues. The performance cost is acceptable for interactive/protocol use cases where lines are short.

Q2. What does readLine() return when the peer closes the connection after sending some data without a newline?

It returns the bytes read so far (without a trailing newline). The loop breaks at the numRead == 0 / totRead > 0 branch, adds a null terminator, and returns totRead.

Q3. What happens if you call readLine() with n = 1?

The function will always store 0 data bytes (since it only stores when totRead < n - 1, i.e., 0 < 0 is false), but it still reads and discards bytes until newline. It returns 0 and the buffer contains only a null terminator. Practically useless โ€” callers should pass a buffer large enough to hold expected lines.

Q4. Why is EINTR handled inside readLine() rather than by the caller?

Because readLine() is a multi-byte accumulator. If EINTR is returned to the caller, the caller has no way to know how many bytes were already accumulated inside the function. By handling EINTR internally, the function maintains its internal state and presents a clean interface.

Q5. How does readLine() protect against buffer overflow?

The condition totRead < n - 1 ensures that at most n-1 bytes are written (leaving room for the null terminator at position n-1). Excess bytes are read from the socket and discarded, keeping the protocol in sync.

Q6. What is the difference between readLine() returning 0 and returning -1?

Return 0 means a clean EOF with no bytes read โ€” the peer closed the connection before sending anything. Return -1 means a real I/O error occurred (disk error, broken pipe, etc.) and errno describes the problem.

Continue Learning

Next: Part 2 โ€” Internet Socket Address Structures (IPv4, IPv6, sockaddr_storage)

Part 2 โ†’ EmbeddedPathashala.com

Leave a Reply

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