readLine() Utility FunctionSocket I/O
TLPI Ch 59
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().
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 storednโ 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, witherrnoset
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.
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;
}
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-1bytes 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
\nbefore 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.
#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.
On Linux, a read() call can be interrupted by a signal while it is blocking. When that happens:
read()returns-1errnois set toEINTR- 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.
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.
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.
Next: Part 2 โ Internet Socket Address Structures (IPv4, IPv6, sockaddr_storage)
