Why recv() and send() Exist
You can use read() and write() on sockets, and they work perfectly fine for basic I/O. But sockets have capabilities that file descriptors don’t — like peeking at incoming data without consuming it, or suppressing the SIGPIPE signal on a broken connection.
recv() and send() are the socket-specific equivalents of read() and write(). They take an extra flags argument that unlocks these socket-specific behaviours.
The API
#include <sys/socket.h>
/*
* Receive data from a socket.
* Returns: bytes received, 0 on EOF, -1 on error.
*/
ssize_t recv(int sockfd, void *buffer, size_t length, int flags);
/*
* Send data on a socket.
* Returns: bytes sent, -1 on error.
*/
ssize_t send(int sockfd, const void *buffer, size_t length, int flags);
When flags is 0, recv() behaves identically to read(), and send() behaves identically to write().
recv() Flags
send() Flags
Example 1 — MSG_PEEK: Peeking at Incoming Data
MSG_PEEK lets you look at what’s in the receive buffer without consuming it. The data remains available for the next recv() call. This is useful for protocol parsers that need to inspect the first few bytes to decide how to read the rest.
#include <stdio.h>
#include <sys/socket.h>
#include <string.h>
/*
* Peek at the first 4 bytes to detect protocol version
* without consuming the data.
*/
void handle_connection(int connfd)
{
unsigned char header[4];
ssize_t n;
/*
* MSG_PEEK: reads up to 4 bytes but leaves them in buffer.
* The next recv() will see the same bytes again.
*/
n = recv(connfd, header, sizeof(header), MSG_PEEK);
if (n <= 0) {
perror("recv peek"); return;
}
printf("Peeked header bytes: %02x %02x %02x %02x\n",
header[0], header[1], header[2], header[3]);
/* Decide protocol based on peeked magic bytes */
if (header[0] == 0x16 && header[1] == 0x03) {
printf("Detected TLS handshake — routing to TLS handler\n");
/* handle_tls(connfd); */
} else {
printf("Detected plain text — routing to HTTP handler\n");
/* handle_http(connfd); */
}
/*
* Now do the real read — data is still in buffer
* because we used MSG_PEEK above.
*/
char buf[256];
n = recv(connfd, buf, sizeof(buf), 0);
printf("Consumed %zd bytes from buffer\n", n);
}
Example 2 — MSG_WAITALL: Wait for All Bytes
MSG_WAITALL asks the kernel to block until the full requested number of bytes is available. It’s like a built-in readn() for a single call. However, it can still return fewer bytes on signal delivery, EOF, or error — so you should still check the return value.
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#define HEADER_SIZE 8 /* fixed-size protocol header */
typedef struct {
uint32_t magic;
uint32_t payload_len;
} ProtoHeader;
int read_message(int sockfd)
{
ProtoHeader hdr;
char *payload;
ssize_t n;
/*
* Read exactly 8 bytes for the header.
* MSG_WAITALL blocks until all 8 bytes arrive.
* Still check n == HEADER_SIZE in case of early EOF/signal.
*/
n = recv(sockfd, &hdr, sizeof(hdr), MSG_WAITALL);
if (n != sizeof(hdr)) {
fprintf(stderr, "Short header read: got %zd, expected %zu\n",
n, sizeof(hdr));
return -1;
}
uint32_t plen = ntohl(hdr.payload_len);
if (plen == 0 || plen > 1024*1024) {
fprintf(stderr, "Invalid payload length: %u\n", plen);
return -1;
}
payload = malloc(plen);
if (!payload) { perror("malloc"); return -1; }
/* Now read the variable-length payload */
n = recv(sockfd, payload, plen, MSG_WAITALL);
if ((size_t)n != plen) {
fprintf(stderr, "Short payload read: got %zd, expected %u\n", n, plen);
free(payload);
return -1;
}
printf("Received message: magic=0x%08x, payload=%u bytes\n",
ntohl(hdr.magic), plen);
free(payload);
return 0;
}
Example 3 — MSG_DONTWAIT: Per-Call Non-Blocking
Instead of setting O_NONBLOCK on the socket (which affects all operations), MSG_DONTWAIT makes just this one recv() call non-blocking. This is useful in event loops where you want to drain a socket without blocking.
#include <stdio.h>
#include <errno.h>
#include <sys/socket.h>
/*
* Drain all currently available data from sockfd without blocking.
* Returns total bytes read, or -1 on error.
*/
ssize_t drain_socket(int sockfd)
{
char buf[4096];
ssize_t n;
ssize_t total = 0;
for (;;) {
n = recv(sockfd, buf, sizeof(buf), MSG_DONTWAIT);
if (n > 0) {
total += n;
/* process buf[0..n-1] */
printf("Drained %zd bytes\n", n);
continue;
}
if (n == 0) {
printf("Peer closed connection\n");
break;
}
/* n == -1 */
if (errno == EAGAIN || errno == EWOULDBLOCK) {
/* No more data right now — come back later */
printf("No more data available, total drained: %zd\n", total);
break;
}
perror("recv");
return -1;
}
return total;
}
Example 4 — MSG_NOSIGNAL: Avoiding SIGPIPE
By default, writing to a socket whose peer has closed the connection sends SIGPIPE to your process. The default action for SIGPIPE is to terminate the process. In a server, this is dangerous — one client disconnecting could kill the whole server.
MSG_NOSIGNAL suppresses SIGPIPE and returns EPIPE as errno instead, which you can handle cleanly.
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
/*
* Safe send that won't kill the process via SIGPIPE
* even if the peer has closed the connection.
*/
ssize_t safe_send(int sockfd, const void *buf, size_t len)
{
ssize_t n;
/*
* MSG_NOSIGNAL: if peer closed connection, return -1
* with errno == EPIPE instead of raising SIGPIPE.
*/
n = send(sockfd, buf, len, MSG_NOSIGNAL);
if (n == -1) {
if (errno == EPIPE) {
/* Peer closed — connection is gone */
printf("Connection broken: peer closed socket\n");
} else {
perror("send");
}
}
return n;
}
/*
* Alternative: install a SIG_IGN handler for SIGPIPE globally.
* Then use regular write() and check errno for EPIPE.
*/
#include <signal.h>
void ignore_sigpipe(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = SIG_IGN;
sigaction(SIGPIPE, &sa, NULL);
/* Now write() returns -1 with errno=EPIPE instead of raising signal */
}
recvmsg() and sendmsg() — Advanced I/O
For the most advanced use cases — scatter/gather I/O, sending ancillary (control) data, passing file descriptors over UNIX sockets — Linux provides recvmsg() and sendmsg().
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
/*
* The msghdr structure allows:
* - Scatter I/O: receive into multiple non-contiguous buffers (iovec)
* - Ancillary data: fd passing, credentials, timestamps
* - Peer address (for datagram sockets)
*/
struct msghdr {
void *msg_name; /* peer address (UDP), NULL for TCP */
socklen_t msg_namelen; /* address length */
struct iovec *msg_iov; /* scatter/gather I/O buffers */
size_t msg_iovlen; /* number of iovec elements */
void *msg_control; /* ancillary data buffer */
size_t msg_controllen; /* ancillary data buffer size */
int msg_flags; /* flags on received message (output) */
};
Example: Passing a file descriptor over a UNIX socket
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <string.h>
/*
* Send a file descriptor 'fd_to_send' to a peer over unix socket 'sockfd'.
* This is used by privilege-separation daemons and systemd activation.
*/
int send_fd(int sockfd, int fd_to_send)
{
struct msghdr msg;
struct iovec iov;
char iobuf[1]; /* must send at least 1 data byte */
char cmsgbuf[CMSG_SPACE(sizeof(int))];
struct cmsghdr *cmsg;
memset(&msg, 0, sizeof(msg));
/* We must send at least 1 byte of regular data */
iobuf[0] = '\0';
iov.iov_base = iobuf;
iov.iov_len = 1;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
/* Ancillary data: the file descriptor */
msg.msg_control = cmsgbuf;
msg.msg_controllen = sizeof(cmsgbuf);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS; /* "rights" = file descriptors */
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(cmsg), &fd_to_send, sizeof(int));
if (sendmsg(sockfd, &msg, 0) == -1) {
perror("sendmsg");
return -1;
}
printf("Sent fd %d to peer\n", fd_to_send);
return 0;
}
