Beyond read() and write()
read() and write() work on sockets, but they don’t give you any socket-specific control. The recv() and send() system calls do exactly the same I/O, but they accept an extra flags argument that lets you control socket-specific behaviours — peeking at data without consuming it, non-blocking mode on a single call, waiting for a full buffer, and more.
Function Signatures
#include <sys/socket.h>
/* Receive data from a socket */
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
/* Send data on a socket */
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
/* Returns number of bytes transferred on success, -1 on error */
When flags is 0, recv() behaves identically to read() and send() behaves identically to write(). The power is in the flags.
recv() Flags
MSG_PEEK copies data from the receive buffer into your buffer, but does NOT remove it from the kernel’s receive buffer. The next read() or recv() call will see the same data again. This is useful for protocol framing — you can look ahead to see what type of message is coming before deciding how to read it.
#include <stdio.h>
#include <sys/socket.h>
#include <string.h>
/*
* Peek at the first byte to decide message type, then do a real read.
*/
void handle_message(int sockfd)
{
char type_byte;
ssize_t nr;
/* Peek at first byte — does NOT remove it from buffer */
nr = recv(sockfd, &type_byte, 1, MSG_PEEK);
if (nr <= 0) return;
if (type_byte == 0x01) {
/* Message type 1: read full 10-byte header */
char header[10];
recv(sockfd, header, sizeof(header), 0); /* Consumes the byte now */
printf("Type 1 message, header read\n");
} else if (type_byte == 0x02) {
/* Message type 2: different size */
char data[256];
recv(sockfd, data, sizeof(data), 0);
printf("Type 2 message, data read\n");
}
}
Important: MSG_PEEK is a point-in-time view. Between the peek and the actual read, more data may arrive or the process may be preempted. Do not assume the peeked data is the only data in the buffer when you do the real read.
Normally recv() returns as soon as any data is available. MSG_WAITALL tells the kernel to block the call until either all len bytes are received, the connection is closed (EOF), or a signal arrives. This is useful when you know exactly how many bytes a message contains.
#include <sys/socket.h>
#include <stdio.h>
/* Read a fixed-size protocol header in one call */
typedef struct {
uint32_t magic;
uint32_t length;
uint32_t type;
} proto_header_t;
int read_header(int sockfd, proto_header_t *hdr)
{
ssize_t nr;
/*
* MSG_WAITALL: block until exactly sizeof(*hdr) bytes are received.
* Without this flag, we might get a partial header.
*/
nr = recv(sockfd, hdr, sizeof(*hdr), MSG_WAITALL);
if (nr == 0) {
printf("Connection closed by peer\n");
return -1;
}
if (nr != (ssize_t)sizeof(*hdr)) {
printf("Partial header or error: got %zd bytes\n", nr);
return -1;
}
return 0;
}
Note: MSG_WAITALL may still return fewer bytes if a signal is delivered or if EOF occurs. Always check the return value. For TCP, MSG_WAITALL is a useful shortcut, but readn() is safer because it also handles EINTR by retrying.
Normally if no data is available, recv() blocks. MSG_DONTWAIT makes just this one call non-blocking — if no data is available, it returns -1 with errno == EAGAIN (or EWOULDBLOCK) immediately. This avoids having to set the socket to non-blocking mode with fcntl() just for one operation.
#include <sys/socket.h>
#include <errno.h>
#include <stdio.h>
void try_recv_nowait(int sockfd)
{
char buf[256];
ssize_t nr;
/*
* Try to receive without blocking.
* If no data available, return immediately instead of sleeping.
*/
nr = recv(sockfd, buf, sizeof(buf), MSG_DONTWAIT);
if (nr > 0) {
printf("Got %zd bytes immediately\n", nr);
} else if (nr == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
printf("No data available right now — do other work\n");
} else if (nr == 0) {
printf("Connection closed\n");
} else {
perror("recv");
}
}
This is especially useful in event loops where you want to drain a socket without blocking the loop, but you do not want to permanently switch the socket to non-blocking mode.
TCP has a rarely-used “urgent data” mechanism that lets you send a single byte out-of-band — it can “jump ahead” of normal data in the stream. Traditionally used by protocols like Telnet for interrupt signals.
#include <sys/socket.h>
#include <stdio.h>
/* Send one byte of urgent/OOB data */
void send_urgent(int sockfd, char urgent_byte)
{
/*
* MSG_OOB: marks this byte as urgent.
* It is sent via TCP's urgent pointer mechanism.
* Only 1 byte can be in-flight as OOB at a time.
*/
if (send(sockfd, &urgent_byte, 1, MSG_OOB) == -1) {
perror("send OOB");
}
printf("Sent urgent byte: 0x%02x\n", (unsigned char)urgent_byte);
}
/* Receive OOB data — use SIGURG or select()/poll() with exception fd set */
void recv_urgent(int sockfd)
{
char byte;
ssize_t nr;
nr = recv(sockfd, &byte, 1, MSG_OOB);
if (nr == 1) {
printf("Received urgent byte: 0x%02x\n", (unsigned char)byte);
}
}
Practical note: OOB data is rarely used in modern protocols. Most protocols that need signalling use a separate control channel or embed control messages in-band. Do not use OOB unless you are implementing a legacy protocol that requires it.
send() Flags
Normally, if you call write() or send() on a socket where the peer has closed the connection, the kernel delivers a SIGPIPE signal to your process. If you have not installed a handler for SIGPIPE, the default action terminates your process — silently and unexpectedly. MSG_NOSIGNAL suppresses this signal and instead makes send() return -1 with errno == EPIPE, which you can handle gracefully.
#include <sys/socket.h>
#include <errno.h>
#include <stdio.h>
ssize_t safe_send(int sockfd, const void *buf, size_t len)
{
ssize_t nw;
/*
* MSG_NOSIGNAL: if peer has closed, return EPIPE instead of
* sending SIGPIPE signal. Prevents unexpected process termination.
*/
nw = send(sockfd, buf, len, MSG_NOSIGNAL);
if (nw == -1) {
if (errno == EPIPE) {
printf("Peer closed connection (EPIPE)\n");
} else {
perror("send");
}
}
return nw;
}
An alternative approach is to set SIGPIPE to SIG_IGN globally at startup, but MSG_NOSIGNAL is cleaner because it is per-call rather than process-wide.
MSG_MORE is a Linux-specific flag that hints to the kernel that more data will be sent shortly. The kernel may delay sending the current data, hoping to combine it with the next send into a single TCP segment. This is similar in effect to the TCP_CORK socket option.
#include <sys/socket.h>
void send_http_response(int sockfd)
{
const char *headers = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n";
const char *body = "Hello";
/*
* MSG_MORE: tell kernel to hold this send and combine with next.
* Reduces number of TCP segments.
*/
send(sockfd, headers, strlen(headers), MSG_MORE);
/* Final send — no MSG_MORE, so kernel flushes combined data */
send(sockfd, body, strlen(body), 0);
}
Quick Reference: Flag Summary
| Flag | Used With | What It Does |
|---|---|---|
| MSG_PEEK | recv() | Copy data but don’t consume it from receive buffer |
| MSG_WAITALL | recv() | Block until full requested length received (or EOF/signal) |
| MSG_DONTWAIT | recv() / send() | Non-blocking for this single call (EAGAIN if would block) |
| MSG_OOB | recv() / send() | Send/receive TCP urgent (out-of-band) data |
| MSG_NOSIGNAL | send() | Don’t raise SIGPIPE on broken connection — return EPIPE instead |
| MSG_MORE | send() (Linux) | Hint that more data follows — delay sending for coalescing |
Interview Questions
Answer: A normal recv() copies data from the kernel receive buffer into the application buffer and removes it from the kernel buffer — the data is consumed. MSG_PEEK also copies data to the application buffer but leaves the data in the kernel buffer. The next recv() or read() will see the same data again.
Answer: If a client closes its connection and the server then tries to write to that socket, the kernel sends SIGPIPE to the server process. The default handler for SIGPIPE is process termination, which kills the entire server just because one client disconnected. Using send() with MSG_NOSIGNAL suppresses the signal and instead returns -1 with errno == EPIPE, which the server can handle gracefully.
Answer: MSG_WAITALL may return fewer bytes than requested if: (1) EOF is received (peer closed connection), (2) a signal interrupts the call, or (3) a network error occurs. It does not guarantee delivery in the way that readn() does, because readn() explicitly retries on EINTR.
Answer: fcntl(fd, F_SETFL, O_NONBLOCK) makes the socket permanently non-blocking — every subsequent I/O call on that descriptor will be non-blocking. MSG_DONTWAIT applies only to the single recv() or send() call it is passed to. Other calls on the same socket remain blocking. MSG_DONTWAIT is useful when you want occasional non-blocking behaviour without changing the socket’s permanent mode.
Answer: TCP’s out-of-band (urgent) data mechanism allows a single byte to be sent with the “urgent” flag, which can be delivered ahead of normal data. It is used in legacy protocols like Telnet to send interrupt or abort signals even when the normal data stream is congested. In modern applications it is rarely used, because proper protocol design with a separate control channel or in-band signalling is cleaner and more reliable.
