Partial I/O on Stream Sockets Why read() and write() are not enough on sockets

 

Partial I/O on Stream Sockets
Chapter 61 · Part 1 of 7 — Why read() and write() are not enough on sockets

The Problem: Partial Transfers

When you call read() or write() on a regular file, you generally get back exactly as many bytes as you asked for (unless you hit end-of-file). Sockets behave differently. On a stream socket, read() and write() may transfer fewer bytes than requested, even without any error. This is called a partial transfer.

Understanding why this happens and how to handle it correctly is fundamental to writing robust network programs.

Why Does Partial I/O Happen?

Kernel Buffers Are Finite

Every socket has two kernel buffers — a send buffer and a receive buffer. When you call write(), the kernel copies data from your user-space buffer into its send buffer. If the send buffer is currently full (because the network is slow or the receiver’s buffer is full due to TCP flow control), the kernel can only accept part of your data.

Write Path — Partial Write Scenario
Your Buffer
4096 bytes
to write
Kernel Send Buffer
Only 1500 bytes
free right now
write() returns
1500
(not 4096!)
write() is NOT an error — you must call write() again for the remaining bytes

Similarly for reads: read() returns as soon as any data is available in the receive buffer. If only 100 bytes have arrived so far but you asked for 1000, you get 100 bytes back. This is not an error — more data may arrive later.

Signals Can Interrupt System Calls

If a signal arrives while read() or write() is blocked waiting for data, the system call is interrupted and returns -1 with errno == EINTR. Even if some bytes were already transferred, the call tells you nothing about them — you must track state yourself and retry.

This is another reason why raw read()/write() loops need extra care on sockets.

The Solution: readn() and writen()

The standard pattern in production network code is to wrap read() and write() in loops that keep going until the full requested amount is transferred. These wrapper functions are conventionally called readn() and writen() (the “n” stands for “exactly n bytes”).

writen() — Write Exactly n Bytes

The idea is simple: loop calling write(), advancing the buffer pointer by however many bytes were written each time, until all bytes are sent.

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

/*
 * writen - Write exactly 'n' bytes to file descriptor 'fd'.
 *
 * Returns:
 *   n   on complete success
 *  -1   on error (errno is set)
 *   0   if fd was closed before all bytes written (treated as error)
 */
ssize_t writen(int fd, const void *buf, size_t n)
{
    size_t  total_written = 0;
    ssize_t nw;
    const char *p = (const char *)buf;

    while (total_written < n) {
        nw = write(fd, p + total_written, n - total_written);

        if (nw <= 0) {
            if (nw == -1 && errno == EINTR) {
                /* Interrupted by signal — just retry */
                continue;
            }
            /* Real error or unexpected EOF */
            return -1;
        }

        total_written += nw;
    }

    return (ssize_t)total_written; /* Always equals n on success */
}

Key points about this implementation:

We track total_written and advance the pointer p + total_written each iteration, so we never re-send already-sent bytes. When errno == EINTR, we simply retry rather than returning an error, because the write was interrupted before doing any real work. Any other negative return is a genuine error.

readn() — Read Exactly n Bytes

Reading is slightly trickier because read() returning 0 means end-of-file (the peer closed the connection), which is a normal event we must handle.

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

/*
 * readn - Read exactly 'n' bytes from file descriptor 'fd'.
 *
 * Returns:
 *   n           on complete success
 *   0 to n-1   if EOF reached before all bytes read
 *  -1           on error (errno is set)
 */
ssize_t readn(int fd, void *buf, size_t n)
{
    size_t  total_read = 0;
    ssize_t nr;
    char   *p = (char *)buf;

    while (total_read < n) {
        nr = read(fd, p + total_read, n - total_read);

        if (nr == 0) {
            /* EOF — peer closed connection */
            break;
        }

        if (nr == -1) {
            if (errno == EINTR) {
                /* Signal interrupted — retry */
                continue;
            }
            /* Real error */
            return -1;
        }

        total_read += nr;
    }

    return (ssize_t)total_read; /* May be less than n if EOF hit */
}

The caller should check whether the return value equals the requested n. If it is less, EOF was reached prematurely — the connection was closed by the peer before all expected data arrived.

Complete Working Example

Echo Server and Client Using readn/writen

Here is a complete minimal example. The server echoes back whatever the client sends. The client sends a fixed-size message and reads back the same fixed size using readn()/writen() so partial transfers are handled automatically.

/* server.c — Simple echo server using readn/writen */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>

#define PORT    8080
#define BUFSIZE 1024

/* writen and readn as defined above — included here for completeness */
ssize_t writen(int fd, const void *buf, size_t n);
ssize_t readn(int fd, void *buf, size_t n);

int main(void)
{
    int                lfd, cfd;
    struct sockaddr_in addr;
    char               buf[BUFSIZE];
    ssize_t            nr;

    lfd = socket(AF_INET, SOCK_STREAM, 0);

    memset(&addr, 0, sizeof(addr));
    addr.sin_family      = AF_INET;
    addr.sin_port        = htons(PORT);
    addr.sin_addr.s_addr = INADDR_ANY;

    bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
    listen(lfd, 5);

    printf("Server listening on port %d\n", PORT);

    for (;;) {
        cfd = accept(lfd, NULL, NULL);
        if (cfd == -1) continue;

        /* Read exactly BUFSIZE bytes */
        nr = readn(cfd, buf, BUFSIZE);
        if (nr > 0) {
            printf("Received %zd bytes\n", nr);
            /* Echo back exactly what we received */
            writen(cfd, buf, nr);
        }

        close(cfd);
    }

    close(lfd);
    return 0;
}
/* client.c — Send a message and read back the echo */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT    8080
#define BUFSIZE 1024

ssize_t writen(int fd, const void *buf, size_t n);
ssize_t readn(int fd, void *buf, size_t n);

int main(void)
{
    int                 sfd;
    struct sockaddr_in  addr;
    char                sbuf[BUFSIZE];
    char                rbuf[BUFSIZE];
    ssize_t             nr;

    sfd = socket(AF_INET, SOCK_STREAM, 0);

    memset(&addr, 0, sizeof(addr));
    addr.sin_family      = AF_INET;
    addr.sin_port        = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);

    connect(sfd, (struct sockaddr *)&addr, sizeof(addr));

    /* Fill buffer with test data */
    memset(sbuf, 'A', BUFSIZE);

    /* Write exactly BUFSIZE bytes — handles partial writes internally */
    writen(sfd, sbuf, BUFSIZE);
    printf("Sent %d bytes\n", BUFSIZE);

    /* Read exactly BUFSIZE bytes back */
    nr = readn(sfd, rbuf, BUFSIZE);
    printf("Echo received: %zd bytes\n", nr);

    close(sfd);
    return 0;
}

Visualising the Difference
What Happens Without writen() (Naive Approach)
write(fd, buf, 4096)
returns 1024 ✓
✗ STOP
Remaining 3072
bytes LOST!

What Happens WITH writen() (Correct Approach)
write() call 1
1024 bytes ✓
write() call 2
2048 bytes ✓
write() call 3
1024 bytes ✓
→ Done!

Interview Questions

Q1. Why can read() return fewer bytes than requested on a socket?

Answer: Because stream sockets deliver data as a byte stream, not as fixed-size chunks. read() returns as soon as any data is available in the kernel receive buffer. If only part of the data sent by the peer has arrived so far, you get what is there. This is normal behaviour, not an error.

Q2. What does write() returning a value less than n mean on a socket?

Answer: It means the kernel’s send buffer was partially full and could only accept that many bytes right now. It is not an error. You must call write() again starting from where you left off to send the remaining data. The writen() helper does this automatically in a loop.

Q3. What should readn() do when it receives EINTR?

Answer: It should simply retry the read() call. EINTR means a signal interrupted the system call before any data was transferred. It is not a real error. Retrying transparently is the correct behaviour in most cases.

Q4. What does readn() returning 0 mean vs returning a value less than n?

Answer: Returning 0 from the underlying read() means EOF — the peer closed the connection. In readn(), if EOF is hit before all n bytes are read, the function returns the number of bytes read so far (which is less than n). The caller should treat this as an incomplete message and handle it as a protocol error if a full n bytes were expected.

Q5. Does partial I/O happen on datagram sockets?

Answer: No. Datagram sockets (UDP) preserve message boundaries. Each read()/recvfrom() returns exactly one datagram. If your buffer is smaller than the datagram, the excess is silently discarded. The partial-transfer problem is specific to stream sockets (TCP).

Q6. Why is it not safe to just loop on write() without tracking the offset?

Answer: If you call write(fd, buf, n) again without advancing the pointer, you will resend the already-sent bytes from the start of the buffer. The receiver gets duplicate data. You must track total_written and pass buf + total_written to each subsequent write() call so you always start from where the last call left off.

Leave a Reply

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