getsockopt() and setsockopt()

 

Chapter 61 · Part 7 of 7
getsockopt() and setsockopt()
Fine-tuning socket behaviour with kernel-level options

Why Socket Options Exist

Every socket has a set of properties that control its behaviour — buffer sizes, keep-alive behaviour, address reuse, linger on close, and more. These are called socket options, and you can read and modify them using getsockopt() and setsockopt().

Options are organised into levels: SOL_SOCKET (generic socket options), IPPROTO_TCP (TCP-specific), IPPROTO_IP (IP layer). Some options change the kernel’s handling of the socket; others just affect your application’s behaviour.

The API

#include <sys/socket.h>

/*
 * Set a socket option.
 *
 * sockfd  — the socket to configure
 * level   — option level (SOL_SOCKET, IPPROTO_TCP, IPPROTO_IP, ...)
 * optname — the option to set (SO_REUSEADDR, SO_KEEPALIVE, ...)
 * optval  — pointer to the option value
 * optlen  — size of optval in bytes
 *
 * Returns 0 on success, -1 on error.
 */
int setsockopt(int sockfd, int level, int optname,
               const void *optval, socklen_t optlen);

/*
 * Get a socket option's current value.
 *
 * optval  — buffer to receive the value
 * optlen  — in: size of buffer; out: actual bytes written
 *
 * Returns 0 on success, -1 on error.
 */
int getsockopt(int sockfd, int level, int optname,
               void *optval, socklen_t *optlen);

Option Levels

SOL_SOCKET
Generic socket options. Work on all socket types. Examples: SO_REUSEADDR, SO_KEEPALIVE, SO_SNDBUF, SO_RCVBUF, SO_LINGER, SO_ERROR.
IPPROTO_TCP
TCP-specific options. Only for SOCK_STREAM sockets over IP. Examples: TCP_NODELAY, TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT, TCP_CORK.
IPPROTO_IP
IP layer options. Examples: IP_TTL (time-to-live), IP_MULTICAST_TTL, IP_ADD_MEMBERSHIP (multicast join).

SO_REUSEADDR — Allow Port Reuse

Already discussed in Part 5, but essential enough to cover in detail here. This option allows a new socket to bind to a port that has sockets in TIME_WAIT from a previous run.

#include <sys/socket.h>
#include <stdio.h>

/*
 * Set SO_REUSEADDR — ALWAYS do this on server sockets before bind().
 */
int set_reuseaddr(int sockfd)
{
    int optval = 1;

    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
                   &optval, sizeof(optval)) == -1) {
        perror("setsockopt SO_REUSEADDR");
        return -1;
    }
    printf("SO_REUSEADDR enabled\n");
    return 0;
}

/* Verify it's set */
void check_reuseaddr(int sockfd)
{
    int      optval;
    socklen_t optlen = sizeof(optval);

    if (getsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
                   &optval, &optlen) == 0) {
        printf("SO_REUSEADDR is %s\n", optval ? "enabled" : "disabled");
    }
}
SO_REUSEPORT is a different option (Linux 3.9+). It allows multiple sockets to bind to the exact same address and port. The kernel distributes incoming connections across all bound sockets — useful for multi-threaded/multi-process servers to avoid the accept() bottleneck.

SO_KEEPALIVE — Detect Dead Connections

By default, TCP connections can remain ESTABLISHED indefinitely even if the peer machine has crashed or been disconnected. SO_KEEPALIVE enables the kernel to periodically probe the peer to check if it’s still alive.

If keep-alive probes fail, the connection is declared dead and the application’s next read/write gets an error.

#include <sys/socket.h>
#include <netinet/tcp.h>

int setup_keepalive(int sockfd)
{
    int optval;
    int rc;

    /* 1. Enable keep-alive on the socket */
    optval = 1;
    rc = setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE,
                    &optval, sizeof(optval));
    if (rc == -1) { perror("SO_KEEPALIVE"); return -1; }

    /*
     * 2. TCP_KEEPIDLE: seconds of inactivity before first probe.
     *    Default on Linux is 7200 seconds (2 hours!) — too long.
     *    Set to 60 seconds.
     */
    optval = 60;
    rc = setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE,
                    &optval, sizeof(optval));
    if (rc == -1) { perror("TCP_KEEPIDLE"); return -1; }

    /*
     * 3. TCP_KEEPINTVL: seconds between subsequent probes.
     *    Set to 10 seconds.
     */
    optval = 10;
    rc = setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL,
                    &optval, sizeof(optval));
    if (rc == -1) { perror("TCP_KEEPINTVL"); return -1; }

    /*
     * 4. TCP_KEEPCNT: number of probes before declaring connection dead.
     *    After 3 failed probes (3 × 10s = 30s after first probe),
     *    the connection is aborted.
     */
    optval = 3;
    rc = setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT,
                    &optval, sizeof(optval));
    if (rc == -1) { perror("TCP_KEEPCNT"); return -1; }

    printf("Keep-alive: enabled (idle=60s, interval=10s, count=3)\n");
    printf("Dead connection detected after: 60 + (3 × 10) = 90 seconds\n");
    return 0;
}

SO_LINGER — Control close() Behaviour

By default, close() returns immediately and the kernel sends any buffered data in the background. SO_LINGER changes this behaviour.

#include <sys/socket.h>

struct linger {
    int l_onoff;   /* 0 = disable linger, non-zero = enable */
    int l_linger;  /* seconds to linger (when l_onoff != 0) */
};

void demo_linger(int sockfd)
{
    struct linger lg;
    socklen_t     optlen = sizeof(lg);

    /* Check current linger setting */
    getsockopt(sockfd, SOL_SOCKET, SO_LINGER, &lg, &optlen);
    printf("Linger: onoff=%d, linger=%d\n", lg.l_onoff, lg.l_linger);

    /* --- Option 1: Disable linger (default behaviour) --- */
    lg.l_onoff  = 0;
    lg.l_linger = 0;
    setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg));
    /* close() returns immediately; kernel drains buffer in background */

    /* --- Option 2: Enable linger with timeout --- */
    lg.l_onoff  = 1;
    lg.l_linger = 5;  /* wait up to 5 seconds */
    setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg));
    /*
     * close() blocks until:
     *   (a) all data in send buffer is ACK'd by peer, OR
     *   (b) the linger timeout (5s) expires.
     * If timeout expires, remaining data is discarded and RST is sent.
     */

    /* --- Option 3: Linger with zero timeout (immediate RST) --- */
    lg.l_onoff  = 1;
    lg.l_linger = 0;
    setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg));
    /*
     * close() immediately sends RST, discarding any unsent data.
     * The connection is abruptly terminated — no graceful FIN exchange.
     * Useful for intentionally resetting a misbehaving connection.
     */
}
When is blocking linger useful? In applications that need to guarantee the peer received your last message before the process exits — like a payment confirmation message. Without linger, close() returns immediately but the kernel might still be sending that data when the process exits.

SO_SNDBUF and SO_RCVBUF — Buffer Sizes

The kernel has send and receive buffers for each socket. The default sizes are determined by system settings (typically 212KB on Linux). Increasing these can improve throughput on high-bandwidth, high-latency connections (the “bandwidth-delay product” problem).

#include <sys/socket.h>
#include <stdio.h>

void tune_buffers(int sockfd)
{
    int      optval;
    socklen_t optlen;

    /* --- Check current buffer sizes --- */
    optlen = sizeof(optval);
    getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &optval, &optlen);
    printf("Current send buffer: %d bytes\n", optval);

    getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &optval, &optlen);
    printf("Current recv buffer: %d bytes\n", optval);

    /* --- Increase send buffer to 256KB --- */
    optval = 256 * 1024;
    if (setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF,
                   &optval, sizeof(optval)) == -1) {
        perror("SO_SNDBUF");
    }

    /* --- Increase receive buffer to 256KB --- */
    optval = 256 * 1024;
    if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF,
                   &optval, sizeof(optval)) == -1) {
        perror("SO_RCVBUF");
    }

    /* Note: Linux doubles the value you set (for internal overhead).
     * If you set 256KB, getsockopt may return 512KB.
     * The kernel also enforces a maximum (rmem_max / wmem_max). */

    /* Re-read to verify */
    optlen = sizeof(optval);
    getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &optval, &optlen);
    printf("New send buffer: %d bytes\n", optval);
}

System-wide buffer limits:

# Check current maximum buffer sizes:
cat /proc/sys/net/core/rmem_max   # max receive buffer
cat /proc/sys/net/core/wmem_max   # max send buffer

# Temporarily increase (takes effect immediately):
echo 8388608 | sudo tee /proc/sys/net/core/rmem_max
echo 8388608 | sudo tee /proc/sys/net/core/wmem_max

# Permanently (survives reboot) — edit /etc/sysctl.conf:
# net.core.rmem_max = 8388608
# net.core.wmem_max = 8388608
# Then: sudo sysctl -p

TCP_NODELAY — Disable Nagle Algorithm

Covered in Part 5 in context, here is the full option usage with read-back verification:

#include <netinet/tcp.h>
#include <stdio.h>

int set_tcp_nodelay(int sockfd, int enable)
{
    int flag = enable ? 1 : 0;

    if (setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY,
                   &flag, sizeof(flag)) == -1) {
        perror("TCP_NODELAY");
        return -1;
    }

    /* Verify */
    int      result;
    socklen_t optlen = sizeof(result);
    getsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &result, &optlen);
    printf("TCP_NODELAY is %s\n", result ? "enabled (Nagle OFF)" : "disabled (Nagle ON)");
    return 0;
}

SO_ERROR — Read and Clear Pending Error

Asynchronous errors (like a TCP connection being reset by the peer while you weren’t reading) are stored in the socket’s error queue. SO_ERROR retrieves and clears this pending error. This is particularly important with non-blocking sockets and after using select()/poll() to wait for writability (which is how non-blocking connect works).

#include <sys/socket.h>
#include <string.h>
#include <stdio.h>

int check_socket_error(int sockfd)
{
    int       soerr;
    socklen_t optlen = sizeof(soerr);

    /*
     * getsockopt SO_ERROR:
     *  - Returns the pending error code (like errno values)
     *  - Clears the error after reading
     *  - Returns 0 if no error is pending
     */
    if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &soerr, &optlen) == -1) {
        perror("getsockopt SO_ERROR");
        return -1;
    }

    if (soerr != 0) {
        fprintf(stderr, "Socket has pending error: %s\n", strerror(soerr));
        return soerr;
    }

    printf("No pending socket error\n");
    return 0;
}

/*
 * Typical use: after non-blocking connect(), use select() to wait
 * for writability, then check SO_ERROR to know if connect succeeded.
 */
int nonblocking_connect_result(int sockfd)
{
    int soerr = check_socket_error(sockfd);

    if (soerr == 0) {
        printf("connect() succeeded\n");
        return 0;
    } else if (soerr == ECONNREFUSED) {
        fprintf(stderr, "connect() refused — no server at that port\n");
        return -1;
    } else if (soerr == ETIMEDOUT) {
        fprintf(stderr, "connect() timed out — host unreachable?\n");
        return -1;
    } else {
        fprintf(stderr, "connect() failed: %s\n", strerror(soerr));
        return -1;
    }
}

SO_RCVTIMEO and SO_SNDTIMEO — I/O Timeouts

These options set a timeout for receive and send operations. If a recv() or send() blocks longer than the timeout, it returns -1 with errno set to EAGAIN or EWOULDBLOCK.

#include <sys/socket.h>
#include <sys/time.h>

int set_io_timeout(int sockfd, int seconds, int microseconds)
{
    struct timeval tv;
    tv.tv_sec  = seconds;
    tv.tv_usec = microseconds;

    /* Set receive timeout */
    if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO,
                   &tv, sizeof(tv)) == -1) {
        perror("SO_RCVTIMEO"); return -1;
    }

    /* Set send timeout */
    if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO,
                   &tv, sizeof(tv)) == -1) {
        perror("SO_SNDTIMEO"); return -1;
    }

    printf("I/O timeout set to %d.%06d seconds\n", seconds, microseconds);
    return 0;
}

/* Example: read with 5-second timeout */
void read_with_timeout(int sockfd)
{
    char    buf[256];
    ssize_t n;

    set_io_timeout(sockfd, 5, 0);  /* 5 second timeout */

    n = recv(sockfd, buf, sizeof(buf), 0);

    if (n == -1) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            printf("recv() timed out — no data within 5 seconds\n");
        } else {
            perror("recv");
        }
    } else if (n == 0) {
        printf("Peer closed connection\n");
    } else {
        printf("Received %zd bytes\n", n);
    }
}

Quick Reference: Common Socket Options

Option
Level
Type
Purpose
SO_REUSEADDR
SOL_SOCKET
int (bool)
Allow binding to port with TIME_WAIT sockets
SO_REUSEPORT
SOL_SOCKET
int (bool)
Allow multiple sockets on same addr:port
SO_KEEPALIVE
SOL_SOCKET
int (bool)
Enable periodic keep-alive probes
SO_LINGER
SOL_SOCKET
struct linger
Control close() blocking/immediate RST behaviour
SO_SNDBUF
SOL_SOCKET
int
Set socket send buffer size in bytes
SO_RCVBUF
SOL_SOCKET
int
Set socket receive buffer size in bytes
SO_RCVTIMEO
SOL_SOCKET
struct timeval
Timeout for recv() operations
SO_SNDTIMEO
SOL_SOCKET
struct timeval
Timeout for send() operations
SO_ERROR
SOL_SOCKET
int (read-only)
Read and clear pending async error
TCP_NODELAY
IPPROTO_TCP
int (bool)
Disable Nagle algorithm (send immediately)
TCP_CORK
IPPROTO_TCP
int (bool)
Buffer all sends until uncorked (batch HTTP headers+body)
TCP_KEEPIDLE
IPPROTO_TCP
int (seconds)
Idle time before keep-alive probes begin
TCP_KEEPINTVL
IPPROTO_TCP
int (seconds)
Interval between keep-alive probes
TCP_KEEPCNT
IPPROTO_TCP
int
Number of failed probes before connection is aborted

Interview Questions & Answers

Q1. What is the difference between setsockopt() and fcntl() for socket configuration?
fcntl() controls file descriptor flags (O_NONBLOCK, O_CLOEXEC, FD_CLOEXEC) that apply to all file descriptor types. setsockopt() controls socket-specific options (buffer sizes, keep-alive, Nagle, etc.) that are properties of the underlying socket, not the file descriptor. They are complementary — you often use both.
Q2. Why does Linux double the buffer size you request with SO_SNDBUF?
Linux doubles the requested buffer size to account for internal kernel overhead (sk_buff structures, alignment, etc.). The extra space is used by the kernel, not accessible to your application. If you request 256KB, the kernel may allocate 512KB, but your effective buffer is still ~256KB. getsockopt() returns the doubled value.
Q3. What happens when close() is called with SO_LINGER enabled and l_linger=0?
An RST (reset) is immediately sent to the peer. All unacknowledged data in the send buffer is discarded. The connection is abruptly terminated — no graceful FIN/ACK sequence. This is used intentionally when you want to force-close a misbehaving connection, but should not be used in normal operation as it can cause data loss.
Q4. Why is TCP_KEEPIDLE’s default of 7200 seconds (2 hours) problematic?
A crashed or unreachable peer won’t be detected for up to 2 hours. During that time, the connection stays in ESTABLISHED state, consuming resources (fd, memory, port). For servers handling many long-lived connections (IoT, mobile apps), this leads to resource leaks. Always tune TCP_KEEPIDLE down to minutes, not hours.
Q5. What is TCP_CORK and how does it differ from TCP_NODELAY?
TCP_CORK explicitly holds all writes until you uncork (set TCP_CORK=0), then flushes everything at once in the minimum number of segments. TCP_NODELAY disables Nagle, letting each write go out immediately. They are opposite strategies: CORK forces batching under application control; NODELAY forces immediate sending. Common pattern: CORK before writing HTTP header+body, UNCORK to flush.
Q6. What is SO_REUSEPORT and how is it different from SO_REUSEADDR?
SO_REUSEADDR allows reusing a port that has TIME_WAIT sockets. SO_REUSEPORT (Linux 3.9+) allows multiple independent sockets to bind to the exact same addr:port simultaneously. The kernel load-balances incoming connections across all SO_REUSEPORT sockets. This lets you run multiple server worker processes each with their own listening socket — eliminating the single accept() bottleneck.
Q7. How do you use SO_ERROR to check the result of a non-blocking connect()?
After connect() returns EINPROGRESS, wait for the socket to become writable using select()/poll(). When writable, call getsockopt(SOL_SOCKET, SO_ERROR, …). If the value is 0, the connection succeeded. If non-zero, it contains the errno code of the connect failure (ECONNREFUSED, ETIMEDOUT, etc.). This is the standard pattern for non-blocking connect with timeout.

Chapter 61 Series Complete!

You’ve covered all 7 parts of Sockets: Advanced Topics

← Back to Chapter Index Visit EmbeddedPathashala

Leave a Reply

Your email address will not be published. Required fields are marked *