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?
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.
to write
free right now
(not 4096!)
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.
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”).
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.
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
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;
}
returns 1024 ✓
bytes LOST!
1024 bytes ✓
2048 bytes ✓
1024 bytes ✓
Interview Questions
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.
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.
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.
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.
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).
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.
