TCP Flow Control Sockets: Fundamentals of TCP/IP Networks

 

TCP Flow Control
Chapter 58 | Sockets: Fundamentals of TCP/IP Networks โ€” Part 4 of 5
๐Ÿ“š TLPI Ch.58
๐Ÿ’ฐ Sliding Window
๐Ÿšซ Buffer Full

What is Flow Control?

Imagine filling a glass of water from a jug. If you pour too fast, water spills. TCP’s flow control is the same idea: it prevents a fast sender from sending data faster than the receiver can process it.

The receiver has a finite receive buffer. If the sender pours data into the network faster than the application reads it from the buffer, the buffer overflows and data is lost. Flow control uses a sliding window mechanism to tell the sender exactly how much data it can send at any given time.

Key Terms in This Section

Flow Control Receive Buffer Sliding Window Window Size Advertised Window Zero Window SO_RCVBUF SO_SNDBUF

๐Ÿ“ The Receive Buffer

The receiving TCP maintains a receive buffer for incoming data. Data arrives from the network, is placed in this buffer, and stays there until the application calls read() to consume it.

Receive Buffer State

Buffer has data waiting to be read by application:
Data (received, not yet read)
Free space (window)
Used = 60%
Available window = 40%

After app reads data (buffer freed up):
All Free โ€” Window fully open. Sender can send more.

Buffer full โ€” app is not reading (slow app):
FULL โ€” Zero Window! Sender must STOP.

During connection establishment, each side tells the other the size of its receive buffer. This is the initial advertised window size. As data is received and consumed, the available window changes dynamically.

๐Ÿ’ฐ The Sliding Window Algorithm

The sliding window allows the sender to have multiple segments in flight (unacknowledged) at the same time, up to a limit called the window size. This improves throughput compared to stopping and waiting for each ACK.

With each ACK, the receiver tells the sender: “I have N bytes of free space.” The sender can have at most N bytes of unacknowledged data in the network at any moment.

Sliding Window โ€” Sender’s View

Sent & ACKed
Sent, NOT ACKed yet (in-flight)
Can send (window open)
Cannot send yet (beyond window)

โ†โ€”โ€” Sender Window (max unACKed bytes = window size) โ€”โ€”โ†’

As ACKs arrive, the left edge advances โ†’ window slides right

After receiving ACK for “Sent, NOT ACKed” portion:
Sent & ACKed (window slid right)
Can send more now!
Cannot send yet

The window “slides” to the right as the receiver ACKs data. This is why it’s called a sliding window. The window size in each ACK tells the sender how much it can still send before it must stop and wait for more ACKs.

๐Ÿšซ Zero Window โ€” When the Receiver Says “Stop”

When the receiver’s buffer is completely full, it sends an ACK with window=0. This is called a Zero Window. The sender must completely stop transmitting.

The sender then sends periodic Window Probe segments (tiny 1-byte segments) to check if the window has reopened. Once the application reads data and frees buffer space, the receiver sends a Window Update โ€” an ACK with a non-zero window โ€” and the sender resumes.

Zero Window Scenario
Sender
โžก Data segment (window was open)
โฌ… ACK, win=0 (buffer full!)
โธ Sender PAUSES transmission
โžก Window Probe (1 byte) every ~T seconds
โฌ… ACK, win=8192 (app read data!)
โžก Sender RESUMES data transmission
Receiver

๐Ÿ’ป Code Example: Setting Socket Buffer Sizes

You can control the receive and send buffer sizes using SO_RCVBUF and SO_SNDBUF socket options. The OS may adjust these values (typically doubling them internally for overhead), so read back the value after setting.

/* socket_buffers.c - controlling TCP send/receive buffer sizes */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define DESIRED_RCVBUF  (256 * 1024)   /* 256 KB receive buffer */
#define DESIRED_SNDBUF  (128 * 1024)   /* 128 KB send buffer    */

int main(void)
{
    int sockfd;
    int rcvbuf, sndbuf;
    socklen_t optlen;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) { perror("socket"); exit(EXIT_FAILURE); }

    /* --- Read default buffer sizes --- */
    optlen = sizeof(rcvbuf);
    getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &optlen);
    printf("Default recv buffer: %d bytes\n", rcvbuf);

    optlen = sizeof(sndbuf);
    getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, &optlen);
    printf("Default send buffer: %d bytes\n", sndbuf);

    /* --- Set new buffer sizes --- */
    rcvbuf = DESIRED_RCVBUF;
    if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)) == -1) {
        perror("setsockopt SO_RCVBUF");
    }

    sndbuf = DESIRED_SNDBUF;
    if (setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)) == -1) {
        perror("setsockopt SO_SNDBUF");
    }

    /* --- Read back actual values (OS may double them) --- */
    optlen = sizeof(rcvbuf);
    getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &optlen);
    printf("Actual recv buffer:  %d bytes (OS may have adjusted)\n", rcvbuf);

    optlen = sizeof(sndbuf);
    getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, &optlen);
    printf("Actual send buffer:  %d bytes (OS may have adjusted)\n", sndbuf);

    /*
     * Important: SO_RCVBUF must be set BEFORE calling listen() on a server socket
     * or BEFORE calling connect() on a client socket.
     * Setting it after connect() may not affect the window advertised to the peer.
     *
     * Linux kernel limits: check /proc/sys/net/core/rmem_max
     *   cat /proc/sys/net/core/rmem_max   (max allowed by non-root processes)
     *   cat /proc/sys/net/core/rmem_default (default)
     */
    printf("\n/proc hints:\n");
    system("cat /proc/sys/net/core/rmem_default 2>/dev/null || echo 'not available'");
    system("cat /proc/sys/net/core/rmem_max 2>/dev/null || echo 'not available'");

    close(sockfd);
    return 0;
}
/* Checking window size with TCP_INFO after connection */
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <unistd.h>

void print_window_info(int sockfd)
{
    struct tcp_info ti;
    socklen_t len = sizeof(ti);

    if (getsockopt(sockfd, IPPROTO_TCP, TCP_INFO, &ti, &len) == 0) {
        /*
         * tcpi_rcv_space:   bytes available in receive buffer
         * tcpi_snd_wnd:     peer's advertised receive window (what we can send)
         * tcpi_rcv_wnd:     our own advertised receive window
         */
        printf("Peer's advertised window (send budget): %u bytes\n", ti.tcpi_snd_wnd);
        printf("Our advertised recv window:             %u bytes\n", ti.tcpi_rcv_wnd);
        printf("Receive buffer space:                   %u bytes\n", ti.tcpi_rcv_space);
    }
}

System-wide buffer tuning (requires root):

# View current limits
cat /proc/sys/net/core/rmem_default   # default receive buffer
cat /proc/sys/net/core/rmem_max       # max receive buffer
cat /proc/sys/net/core/wmem_default   # default send buffer
cat /proc/sys/net/core/wmem_max       # max send buffer

# Enable auto-tuning (Linux kernel adjusts buffers based on conditions)
echo 1 > /proc/sys/net/ipv4/tcp_moderate_rcvbuf

# Set TCP-specific buffer limits
# Format: min default max (in bytes)
echo "4096 87380 16777216" > /proc/sys/net/ipv4/tcp_rmem
echo "4096 65536 16777216" > /proc/sys/net/ipv4/tcp_wmem

๐Ÿ…พ Interview Questions โ€” TCP Flow Control
Q1. What is TCP flow control and why is it necessary?

Flow control prevents a fast sender from overwhelming a slow receiver. Without it, the sender could flood the receiver’s buffer, causing data loss. TCP uses the advertised window (in ACKs) to tell the sender how much free space the receive buffer has.

Q2. What is the difference between flow control and congestion control?

Flow control is between sender and receiver โ€” it protects the receiver’s buffer. Congestion control is between sender and the network โ€” it protects intermediate routers from being overwhelmed. Flow control uses the receiver’s advertised window; congestion control uses the congestion window (cwnd) maintained by the sender based on network feedback.

Q3. What is a sliding window in TCP?

A sliding window allows the sender to send multiple segments without waiting for an ACK for each one. The window defines the maximum number of unacknowledged bytes. As ACKs arrive, the window slides right, allowing more data to be sent. This avoids the “stop and wait” inefficiency and keeps the link utilized.

Q4. What is a zero window and how is it resolved?

A zero window occurs when the receiver’s buffer is full โ€” it advertises window=0, telling the sender to stop. The sender pauses and periodically sends 1-byte window probe segments to check if the window has reopened. Once the application reads data (freeing buffer space), the receiver sends a window update with a non-zero window.

Q5. How can you change the receive buffer size in Linux? What is the effect?

Use setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)). A larger receive buffer increases the advertised window, allowing the sender to have more data in-flight, which improves throughput on high-latency links (BDP = bandwidth ร— RTT). The OS may cap the value at /proc/sys/net/core/rmem_max. Set it before listen() or connect().

Q6. What is the Bandwidth-Delay Product (BDP) and why does it matter for buffer sizing?

BDP = bandwidth ร— RTT. It represents the amount of data “in flight” in the network at any moment when the pipe is full. For example, 1 Gbps ร— 100ms RTT = 12.5 MB. The receive buffer should be at least as large as the BDP so that the sender is never forced to wait for ACKs. Under-sized buffers on high-BDP paths cause severe throughput degradation.

Next: TCP Congestion Control
Slow-start, congestion avoidance, and how TCP protects the network itself

Part 5 โ†’ โ† Part 3

Leave a Reply

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