Beyond read() and write()
You already know that you can use read() and write() on sockets. But sockets also have two dedicated system calls — recv() and send() — that give you extra control through a flags argument.
These flags let you do things like: make a single call non-blocking, peek at data without consuming it, wait until a full buffer is filled, or prevent a SIGPIPE crash when the remote side closes unexpectedly. You cannot do any of these with plain read()/write().
The recv() and send() API
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buffer, size_t length, int flags);
/* Returns: number of bytes received, 0 on EOF, -1 on error */
ssize_t send(int sockfd, const void *buffer, size_t length, int flags);
/* Returns: number of bytes sent, -1 on error */
The first three arguments are identical to read() and write(). The only addition is the flags argument. When you do not need any flags, passing 0 makes recv() behave exactly like read(), and send() behave exactly like write().
Flags for recv()
Normally, if no data is available in the socket receive buffer, recv() blocks and waits. MSG_DONTWAIT changes that behavior for just this one call: if no data is ready, it returns immediately with an error instead of blocking.
When there is no data and MSG_DONTWAIT is set, recv() returns -1 and sets errno to EAGAIN (or EWOULDBLOCK, which is the same value on Linux).
How is this different from O_NONBLOCK? Setting O_NONBLOCK via fcntl() makes the socket permanently non-blocking — every subsequent call behaves this way. MSG_DONTWAIT is per-call — it only affects that single recv(). This is handy when you want to do an occasional non-blocking check without changing the socket’s global mode.
#include <sys/socket.h>
#include <errno.h>
#include <stdio.h>
void try_nonblocking_recv(int sockfd)
{
char buf[256];
ssize_t n;
n = recv(sockfd, buf, sizeof(buf), MSG_DONTWAIT);
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
/* No data available right now — that is OK */
printf("No data ready yet, come back later\n");
} else {
/* A real error occurred */
perror("recv");
}
} else if (n == 0) {
printf("Connection closed by peer\n");
} else {
printf("Got %zd bytes\n", n);
/* process buf[0..n-1] */
}
}
Normally, when you call recv(), the bytes are removed from the socket receive buffer. With MSG_PEEK, you get a copy of the bytes but they stay in the buffer. The next call to recv() (with or without MSG_PEEK) will see the same data again.
When is this useful? Imagine you need to look at the first few bytes of a message to decide what protocol it is (e.g., HTTP vs. some custom binary protocol), but you do not want to consume those bytes before passing them to the right handler. MSG_PEEK lets you inspect without destroying.
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
void peek_and_decide(int sockfd)
{
char header[4];
ssize_t n;
/* Peek at the first 4 bytes to determine message type */
n = recv(sockfd, header, sizeof(header), MSG_PEEK);
if (n <= 0) {
perror("recv peek");
return;
}
if (memcmp(header, "HTTP", 4) == 0) {
printf("HTTP request detected\n");
handle_http(sockfd); /* will re-read including these 4 bytes */
} else {
printf("Custom protocol detected\n");
handle_custom(sockfd); /* will re-read including these 4 bytes */
}
}
/* Both handlers can now do a normal recv() and see the full message
including the 4 bytes that were peeked */
Normally, recv() returns as soon as any data is available — even if it is just 1 byte when you asked for 100. This is correct behavior for a stream socket because data can arrive in fragments. But often you know exactly how many bytes you expect (for example, a fixed-size header or struct), and you want to block until all of them arrive.
MSG_WAITALL tells the kernel: “do not return until you have filled my entire buffer.” The call will keep blocking and accumulating data until length bytes are received.
Exceptions — MSG_WAITALL may still return fewer bytes if:
- A signal interrupts the call
- The peer closes the connection (you get whatever arrived before the close)
- An out-of-band data byte is encountered
- The datagram received is smaller than
length - A socket error occurs
Always check the return value even with MSG_WAITALL.
#include <sys/socket.h>
#include <stdio.h>
#include <stdint.h>
/* A fixed-size message header for a custom protocol */
struct msg_header {
uint32_t msg_type;
uint32_t payload_len;
};
int recv_full_header(int sockfd, struct msg_header *hdr)
{
ssize_t n;
/*
* MSG_WAITALL: block until ALL sizeof(*hdr) bytes have arrived.
* Without this flag, we might get a partial header, which is hard
* to handle correctly.
*/
n = recv(sockfd, hdr, sizeof(*hdr), MSG_WAITALL);
if (n == 0) {
printf("Connection closed by peer\n");
return -1;
}
if (n == -1) {
perror("recv");
return -1;
}
if (n < (ssize_t)sizeof(*hdr)) {
/* Fewer bytes than expected — connection was interrupted */
fprintf(stderr, "Partial header: got %zd of %zu bytes\n",
n, sizeof(*hdr));
return -1;
}
return 0; /* success */
}
readn() loop (a loop that keeps calling read() until n bytes are collected). The difference is that readn() can restart itself after a signal interruption, while MSG_WAITALL cannot.TCP supports a concept called out-of-band (OOB) data — also called urgent data. This is a way to send a special high-priority byte that jumps ahead of the normal data stream. A classic use was the telnet protocol sending interrupt commands without waiting for normal data to be consumed.
To receive OOB data, you pass MSG_OOB to recv(). This is an advanced and rarely-used feature in modern applications. The full details are in Section 61.13.1 of TLPI.
char oob_byte;
ssize_t n = recv(sockfd, &oob_byte, 1, MSG_OOB);
if (n == 1)
printf("Received OOB byte: 0x%02x\n", (unsigned char)oob_byte);
recv() Flags — Quick Reference
| Flag | What it does | Returns EAGAIN when? |
|---|---|---|
| MSG_DONTWAIT | Non-blocking for this call only | No data in receive buffer |
| MSG_PEEK | Copy bytes but leave them in buffer | N/A — behaves like normal recv |
| MSG_WAITALL | Block until length bytes received | N/A — blocks until done or error |
| MSG_OOB | Receive out-of-band data | No OOB data pending |
Flags for send()
Just like for recv(), MSG_DONTWAIT on send() makes that single send call non-blocking. If the socket’s send buffer is full (the kernel cannot accept more data right now because the network is backed up), instead of blocking until space is available, send() returns -1 with errno set to EAGAIN.
#include <sys/socket.h>
#include <errno.h>
#include <string.h>
int try_send(int sockfd, const char *data, size_t len)
{
ssize_t n = send(sockfd, data, len, MSG_DONTWAIT);
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
/* Send buffer full — try again later */
printf("Send buffer full, will retry\n");
return 0; /* caller should retry */
}
perror("send");
return -1;
}
printf("Sent %zd bytes\n", n);
return n;
}
When you send small chunks of data using multiple send() calls, TCP may send each chunk as a separate small packet. This is wasteful — it is more efficient to collect several small pieces and send them together in one larger packet.
TCP_CORK is a socket option that does this globally — it accumulates data until you un-cork the socket. MSG_MORE achieves the same effect but per-call, without permanently changing the socket option.
When you set MSG_MORE on a send() call, the kernel holds the data in the send buffer waiting for more. When you make a send() call without MSG_MORE, all the buffered data is flushed as one packet.
Since Linux 2.6, MSG_MORE on UDP datagram sockets also works — successive sends with MSG_MORE accumulate into a single UDP datagram, transmitted on the next call without the flag.
MSG_MORE has no effect on UNIX domain sockets.
#include <sys/socket.h>
void send_http_response(int sockfd)
{
const char *header = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\n";
const char *body = "Hello, World!";
/*
* Send the header with MSG_MORE — tell kernel: "more data is coming,
* hold this in the buffer for now"
*/
send(sockfd, header, strlen(header), MSG_MORE);
/*
* Send the body WITHOUT MSG_MORE — this flushes the buffer.
* Both header and body will be sent together in (ideally) one packet.
*/
send(sockfd, body, strlen(body), 0);
}
TCP_CORK is a persistent socket option you toggle with setsockopt(). MSG_MORE is per-call — lighter and more convenient when you just want to batch a few sends without managing a socket option.When you try to send() data on a connected stream socket but the peer has already closed their end, two things normally happen:
- The kernel delivers a SIGPIPE signal to your process
- The
send()call fails witherrno = EPIPE
The default action for SIGPIPE is to terminate the process. This catches many programmers off guard — their server crashes silently because a client disconnected.
You have two options to handle this:
Option 1 — Global: Ignore SIGPIPE for the whole process with signal(SIGPIPE, SIG_IGN). Then every failed send() just returns -1 / EPIPE which you can handle.
Option 2 — Per call: Use MSG_NOSIGNAL on each send(). No SIGPIPE is generated for that call — you just get -1 / EPIPE as an error return.
#include <sys/socket.h>
#include <errno.h>
#include <stdio.h>
int safe_send(int sockfd, const void *buf, size_t len)
{
ssize_t n;
/*
* MSG_NOSIGNAL: if the peer has closed, do NOT send SIGPIPE.
* Just return -1 with errno = EPIPE so we can handle it gracefully.
*/
n = send(sockfd, buf, len, MSG_NOSIGNAL);
if (n == -1) {
if (errno == EPIPE) {
/* Peer closed their end — clean up gracefully */
printf("Client disconnected\n");
return -1;
}
perror("send");
return -1;
}
return (int)n;
}
signal(SIGPIPE, SIG_IGN) at startup or use MSG_NOSIGNAL on every send.MSG_OOB in send() is the sending counterpart to MSG_OOB in recv(). It sends data as TCP urgent/OOB data. The remote side can receive it with recv(sockfd, buf, 1, MSG_OOB). This is a niche feature, mainly used by legacy protocols like telnet.
char urgent = 0xFF;
send(sockfd, &urgent, 1, MSG_OOB);
send() Flags — Quick Reference
| Flag | What it does | Standard? |
|---|---|---|
| MSG_DONTWAIT | Non-blocking for this call only. Returns EAGAIN if send buffer full. | Some UNIX systems |
| MSG_MORE | Buffer this data; send together with next call that omits MSG_MORE (TCP/UDP corking per call) | Linux-specific |
| MSG_NOSIGNAL | Don’t send SIGPIPE if peer closed. Return EPIPE instead. | Linux-specific |
| MSG_OOB | Send out-of-band urgent data on a stream socket | SUSv3 |
Combining Flags
Flags are a bitmask — you can OR multiple flags together in a single call:
/* Non-blocking peek: look at data without consuming it,
and don't block if nothing is there */
ssize_t n = recv(sockfd, buf, sizeof(buf), MSG_PEEK | MSG_DONTWAIT);
if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
printf("No data available right now\n");
} else if (n > 0) {
printf("Peeked at %zd bytes (still in buffer)\n", n);
}
Complete Practical Example — Robust Message Reader
This example shows a function that reads a fixed-size header using MSG_WAITALL, then reads the payload. It uses MSG_DONTWAIT to check if data is available before committing to a blocking read.
#include <sys/socket.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
/* Our custom message format */
struct msg_header {
uint32_t type;
uint32_t length; /* length of payload following the header */
};
/*
* Reads a complete message from sockfd.
* Returns allocated payload buffer (caller must free()), or NULL on error/EOF.
* *payload_len is set to the number of payload bytes.
*/
char *read_message(int sockfd, uint32_t *payload_len)
{
struct msg_header hdr;
char *payload;
ssize_t n;
/* Step 1: Quick non-blocking peek to see if any data is waiting */
n = recv(sockfd, &hdr, sizeof(hdr), MSG_PEEK | MSG_DONTWAIT);
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
/* Nothing ready yet */
return NULL;
}
perror("recv peek");
return NULL;
}
if (n == 0) {
printf("Connection closed\n");
return NULL;
}
/* Step 2: Blocking read of full header using MSG_WAITALL */
n = recv(sockfd, &hdr, sizeof(hdr), MSG_WAITALL);
if (n <= 0) {
if (n == 0) printf("Connection closed\n");
else perror("recv header");
return NULL;
}
hdr.type = ntohl(hdr.type);
hdr.length = ntohl(hdr.length);
if (hdr.length == 0) {
*payload_len = 0;
return strdup(""); /* empty payload */
}
/* Step 3: Allocate buffer and read full payload */
payload = malloc(hdr.length + 1);
if (!payload) {
perror("malloc");
return NULL;
}
n = recv(sockfd, payload, hdr.length, MSG_WAITALL);
if (n <= 0) {
free(payload);
return NULL;
}
payload[hdr.length] = '\0';
*payload_len = hdr.length;
printf("Received message type=%u, len=%u\n", hdr.type, hdr.length);
return payload;
}
Interview Questions & Answers
O_NONBLOCK is a persistent flag set on the file descriptor — every subsequent I/O call on that socket will be non-blocking until you turn it off. MSG_DONTWAIT is per-call — it only makes that one recv() or send() non-blocking; the socket’s global blocking mode is unchanged. Use MSG_DONTWAIT when you want an occasional non-blocking check without permanently altering the socket’s behavior.MSG_PEEK causes recv() to copy bytes from the socket receive buffer into the user buffer, but does NOT remove them from the buffer. A subsequent recv() will see the same bytes again. A practical use case is protocol multiplexing: peek at the first few bytes of an incoming message to determine which protocol or message type it belongs to, then dispatch to the appropriate handler which will re-read the full message normally.MSG_WAITALL makes recv() block until the requested number of bytes have been received. It may still return fewer bytes if: a signal interrupts the call, the peer closes the connection, an OOB data byte is encountered, the datagram is smaller than requested, or a socket error occurs. Always check the return value even when using MSG_WAITALL.send() on a stream socket whose peer has already closed their end, the kernel generates a SIGPIPE signal. The default action for SIGPIPE is to terminate the process, which is usually not what a server wants. MSG_NOSIGNAL suppresses SIGPIPE generation for that specific send() call — instead, the call returns -1 with errno = EPIPE, which the application can handle gracefully. The alternative is to call signal(SIGPIPE, SIG_IGN) globally.MSG_MORE and TCP_CORK enable TCP corking — buffering outgoing data to be sent as a single larger packet rather than multiple small packets. TCP_CORK is a persistent socket option toggled with setsockopt() and affects all subsequent sends. MSG_MORE is per-call: data sent with MSG_MORE is held in the kernel buffer, and sent together when a send() call is made without that flag. MSG_MORE is more convenient for short batching without managing socket options. Since Linux 2.6, MSG_MORE also works on UDP sockets.0 as the flags argument makes recv() behave identically to read(), and send() behave identically to write(). This is the standard way to use these calls when you do not need any special flag behavior.Free Embedded & Linux Tutorials
EmbeddedPathashala — Learn Linux System Programming, BLE, and Embedded Systems for free.
