The Problem: Expensive File-to-Socket Transfer
Imagine writing a simple HTTP file server. The naive approach is: open the file, read() chunks into a user-space buffer, write() from that buffer to the socket. This works, but it involves multiple data copies and multiple context switches between kernel and user space.
sendfile() eliminates the user-space detour entirely. The kernel copies data directly from the file’s page cache to the socket’s send buffer — no trip through user space. This is called zero-copy I/O.
Traditional Approach vs sendfile()
(DMA copy — kernel mode)
(CPU copy — user mode)
(CPU copy — kernel mode)
(DMA copy — kernel mode)
(DMA copy — kernel mode)
(kernel-to-kernel copy)
(DMA copy — kernel mode)
The sendfile() API
#define _GNU_SOURCE
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
/*
* out_fd — destination: must be a socket (on Linux)
* in_fd — source: must be a regular file or block device
* offset — pointer to file offset to start reading from.
* If NULL, uses in_fd's current file offset and advances it.
* If non-NULL, uses *offset and updates it; in_fd's position unchanged.
* count — number of bytes to transfer
*
* Returns: bytes transferred on success, -1 on error.
* A return value less than count is possible (partial transfer).
*/
Basic Example: Sending a File over TCP
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080
/*
* Send the file at 'filepath' to the connected socket 'connfd'.
* Uses sendfile() for zero-copy efficiency.
*/
int send_file_to_socket(int connfd, const char *filepath)
{
int filefd;
struct stat st;
off_t offset;
ssize_t sent;
size_t remaining;
/* Open file for reading */
filefd = open(filepath, O_RDONLY);
if (filefd == -1) {
perror("open"); return -1;
}
/* Get file size */
if (fstat(filefd, &st) == -1) {
perror("fstat"); close(filefd); return -1;
}
printf("Sending file: %s (%ld bytes)\n", filepath, (long)st.st_size);
offset = 0;
remaining = (size_t)st.st_size;
/*
* sendfile() may not transfer all bytes in one call
* (similar to write() partial writes).
* Loop until all bytes are sent.
*/
while (remaining > 0) {
sent = sendfile(connfd, filefd, &offset, remaining);
if (sent == -1) {
perror("sendfile");
close(filefd);
return -1;
}
remaining -= (size_t)sent;
printf("Sent %zd bytes, %zu remaining\n", sent, remaining);
}
printf("File sent completely\n");
close(filefd);
return 0;
}
int main(void)
{
int listenfd, connfd;
struct sockaddr_in servaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) { perror("socket"); exit(1); }
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
perror("bind"); exit(1);
}
listen(listenfd, 5);
printf("File server listening on port %d\n", PORT);
connfd = accept(listenfd, NULL, NULL);
if (connfd == -1) { perror("accept"); exit(1); }
printf("Client connected\n");
/* Send a specific file — change to a real file on your system */
send_file_to_socket(connfd, "/etc/passwd");
close(connfd);
close(listenfd);
return 0;
}
Test with netcat:
gcc -o fileserver fileserver.c
./fileserver &
# Receive the file:
nc 127.0.0.1 8080 > received_file.txt
diff /etc/passwd received_file.txt # should be identical
Realistic Example: Minimal HTTP File Server
This shows how sendfile() fits into a real HTTP server responding to GET requests.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080
#define DOCROOT "/var/www/html"
void handle_client(int connfd)
{
char req[2048];
char method[16], path[256], version[16];
char filepath[512];
char header[512];
int filefd;
struct stat st;
off_t offset;
size_t remaining;
ssize_t n;
/* Read HTTP request (simplified — just first line) */
n = recv(connfd, req, sizeof(req) - 1, 0);
if (n <= 0) return;
req[n] = '\0';
sscanf(req, "%15s %255s %15s", method, path, version);
printf("Request: %s %s\n", method, path);
/* Build file path */
snprintf(filepath, sizeof(filepath), "%s%s",
DOCROOT, strcmp(path, "/") == 0 ? "/index.html" : path);
filefd = open(filepath, O_RDONLY);
if (filefd == -1) {
/* 404 response */
const char *r404 =
"HTTP/1.1 404 Not Found\r\n"
"Content-Length: 9\r\n\r\nNot Found";
send(connfd, r404, strlen(r404), MSG_NOSIGNAL);
return;
}
fstat(filefd, &st);
/* Send HTTP 200 header */
snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Content-Length: %ld\r\n"
"Connection: close\r\n\r\n",
(long)st.st_size);
send(connfd, header, strlen(header), MSG_NOSIGNAL);
/* Send file body using zero-copy sendfile() */
offset = 0;
remaining = (size_t)st.st_size;
while (remaining > 0) {
ssize_t sent = sendfile(connfd, filefd, &offset, remaining);
if (sent <= 0) break;
remaining -= (size_t)sent;
}
close(filefd);
}
int main(void)
{
int listenfd, connfd;
struct sockaddr_in servaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, 10);
printf("HTTP server at http://127.0.0.1:%d/\n", PORT);
for (;;) {
connfd = accept(listenfd, NULL, NULL);
if (connfd == -1) { perror("accept"); continue; }
handle_client(connfd);
close(connfd);
}
}
Using the offset Parameter
The offset parameter is powerful. When non-NULL, sendfile() reads from that position in the file, and updates the value pointed to by offset after the transfer. The file’s own seek position (lseek position) is NOT changed. This is ideal for serving byte-range requests (HTTP Range: headers).
/*
* Serve a byte range from a file (for HTTP Range: requests).
* Example: Range: bytes=1000-2999 (2000 bytes starting at offset 1000)
*/
int serve_range(int connfd, int filefd, off_t start, size_t length)
{
off_t offset = start; /* sendfile updates this after each call */
size_t remaining = length;
while (remaining > 0) {
ssize_t sent = sendfile(connfd, filefd, &offset, remaining);
if (sent == -1) { perror("sendfile"); return -1; }
if (sent == 0) break; /* shouldn't happen but guard anyway */
remaining -= (size_t)sent;
}
printf("Served %zu bytes starting at offset %lld\n",
length, (long long)start);
/*
* filefd's lseek position is UNCHANGED by sendfile()
* when offset is non-NULL. We can seek it independently.
*/
return 0;
}
