Part 5: TCP Transmission Control Protocol — Reliable, Connection-Oriented, Byte-Stream Communication

 

Chapter 58 – Part 5: TCP
Transmission Control Protocol — Reliable, Connection-Oriented, Byte-Stream Communication
Topic
TCP Protocol
Level
Intermediate
Part
5 of 5

What is TCP?

TCP (Transmission Control Protocol) provides a reliable, connection-oriented, bidirectional, byte-stream communication channel between two endpoints. Where UDP is like sending a postcard (no guarantee it arrives), TCP is like making a phone call — a connection is established first, and both sides can speak and listen simultaneously, with guaranteed delivery.

TCP is used for applications where every byte matters and order must be preserved: web browsing (HTTP/HTTPS), file transfer (FTP), email (SMTP), SSH, and most other internet applications that cannot tolerate data loss.

Key Terms:

TCP SOCK_STREAM Connection-Oriented Reliable Byte-Stream 3-Way Handshake TCP Endpoint Send Buffer Receive Buffer Sequence Number ACK Flow Control Congestion Control FIN listen() accept()

1. TCP Endpoint — What the Kernel Maintains

The kernel maintains a TCP endpoint for each end of a TCP connection. This endpoint contains all the state needed to manage the connection. When we say “a TCP socket,” what we really mean is the combination of the application’s file descriptor (sockfd) and the underlying TCP endpoint maintained by the kernel.

Application A (Client) Kernel (Client Side) Network Kernel (Server Side) Application B (Server)
sockfd TCP Endpoint
📤 Send Buffer
📥 Receive Buffer
🔢 Sequence Numbers
📋 State (ESTABLISHED)
🔌 IP:Port pair
←→
IP Packets
TCP Endpoint
📤 Send Buffer
📥 Receive Buffer
🔢 Sequence Numbers
📋 State (ESTABLISHED)
🔌 IP:Port pair
sockfd

The TCP endpoint’s state information includes send and receive buffers, sequence numbers, acknowledgment numbers, window sizes for flow control, and the current connection state (SYN_SENT, ESTABLISHED, FIN_WAIT, etc.).

2. TCP’s Key Features
Feature How TCP Achieves It
Reliable Delivery Every segment is acknowledged. If no ACK received within a timeout, TCP automatically retransmits the segment. Data is never lost silently.
Ordered Delivery Each byte has a sequence number. Even if segments arrive out of order (packets can take different routes), TCP reorders them before delivering to the application.
Byte-Stream Model TCP treats data as a continuous stream of bytes, not discrete messages. There are no message boundaries — the receiver might get data in different chunk sizes than the sender sent.
Flow Control The receiver advertises its available buffer space (receive window). The sender limits its transmission rate to avoid overwhelming the receiver’s buffer.
Congestion Control TCP monitors for packet loss and reduces its sending rate when the network is congested — prevents a single connection from overwhelming network routers.
Bidirectional Both endpoints can send and receive simultaneously (full-duplex). Each direction has its own send and receive buffers and sequence number space.
Duplicate Detection Using sequence numbers, TCP detects and discards duplicate segments (a segment might be duplicated if retransmitted after a delayed ACK).

3. TCP Connection Establishment — 3-Way Handshake

Before any data is exchanged, TCP establishes a connection using a three-way handshake. This ensures both sides are ready to communicate and synchronizes sequence numbers:

Client Message Server
CLOSED
→ SYN_SENT
calls connect()
SYN (seq=x) ──────────────→
Step 1: “I want to connect, my seq starts at x”
LISTEN
→ SYN_RCVD
waiting on accept()
ESTABLISHED
connect() returns
← SYN-ACK (seq=y, ack=x+1)
Step 2: “OK, my seq starts at y, I got your x”
ESTABLISHED
accept() returns
Can now send data ACK (ack=y+1) ────────────→
Step 3: “Got your y, connection established”
Can now send data

The three steps: SYN (client initiates), SYN-ACK (server acknowledges and sends its own SYN), ACK (client acknowledges server’s SYN). After this, both sides are in the ESTABLISHED state and can exchange data.

4. TCP Send and Receive Buffers

Each TCP endpoint maintains two buffers in the kernel:

Client-Side Kernel Server-Side Kernel
📤 Send Buffer
App writes here via write()/send()
TCP reads from here to send segments
Data stays until ACKed by peer
📥 Receive Buffer
TCP puts incoming data here
App reads via read()/recv()
Size advertised as receive window
←→
IP packets
flow both
ways
📤 Send Buffer
App writes here via write()/send()
TCP reads from here to send segments
Data stays until ACKed by peer
📥 Receive Buffer
TCP puts incoming data here
App reads via read()/recv()
Size advertised as receive window

Why the send buffer holds data until ACKed: If the peer does not acknowledge a segment, TCP needs to retransmit it. Once an ACK is received for data in the send buffer, that data can be freed. This is the foundation of TCP’s reliable delivery guarantee.

5. TCP Connection Termination — 4-Way FIN

Since TCP is full-duplex, each direction of data flow must be closed independently. Closing a TCP connection uses a 4-way exchange of FIN and ACK segments:

Client Message (Active Closer → Passive Closer) Server
→ FIN_WAIT_1
calls close()
FIN ──────────→
“I’m done sending”
→ CLOSE_WAIT
→ FIN_WAIT_2 ←────────── ACK
“Got your FIN”
Still can send data
→ TIME_WAIT ←────────── FIN
“I’m done too”
→ LAST_ACK
→ CLOSED (after 2MSL wait) ACK ──────────→
“Got your FIN”
→ CLOSED

The TIME_WAIT state on the active closer lasts for 2 × MSL (Maximum Segment Lifetime, typically 60 seconds each = 2 minutes total). This ensures delayed packets from the old connection don’t confuse a new connection on the same port pair.

6. Coding Example – TCP Echo Server & Client

TCP Echo Server

/* tcp_echo_server.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT    8080
#define BUFSIZE 1024

int main() {
    int listen_fd, conn_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buf[BUFSIZE];
    ssize_t n;

    /*
     * Step 1: Create a TCP (SOCK_STREAM) socket.
     * The kernel allocates send and receive buffers
     * for this endpoint.
     */
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) { perror("socket"); return 1; }

    /* Allow immediate reuse of port after server restarts */
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    /* Step 2: Bind to IP and port */
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family      = AF_INET;
    server_addr.sin_port        = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (bind(listen_fd, (struct sockaddr *)&server_addr,
             sizeof(server_addr)) == -1) {
        perror("bind"); return 1;
    }

    /*
     * Step 3: Mark socket as passive (listening).
     * backlog=5: up to 5 pending connections can queue
     * waiting for accept() before the kernel starts
     * refusing new connection requests.
     */
    if (listen(listen_fd, 5) == -1) { perror("listen"); return 1; }
    printf("TCP echo server listening on port %d\n", PORT);

    for (;;) {
        /*
         * Step 4: accept() blocks until a client completes
         * the 3-way handshake. Returns a NEW socket descriptor
         * for this specific connection. listen_fd remains open
         * for future connections.
         */
        conn_fd = accept(listen_fd,
                         (struct sockaddr *)&client_addr,
                         &client_len);
        if (conn_fd == -1) { perror("accept"); continue; }

        printf("Client connected: %s:%d\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port));

        /*
         * Step 5: Read data from the connection and echo it back.
         * TCP is a byte stream — recv() may return fewer bytes
         * than the sender sent in one call. Keep reading until
         * the client closes the connection (recv returns 0).
         */
        while ((n = recv(conn_fd, buf, BUFSIZE, 0)) > 0) {
            /* Echo all received bytes back */
            send(conn_fd, buf, n, 0);
        }

        if (n == 0)
            printf("Client disconnected\n");
        else
            perror("recv");

        close(conn_fd);  /* Close this connection's socket */
    }

    close(listen_fd);
    return 0;
}

TCP Echo Client

/* tcp_echo_client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define SERVER_IP  "127.0.0.1"
#define PORT       8080
#define BUFSIZE    1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char send_buf[] = "Hello from TCP client!";
    char recv_buf[BUFSIZE];
    ssize_t n;

    /* Step 1: Create TCP socket */
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) { perror("socket"); return 1; }

    /* Step 2: Fill in server address */
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port   = htons(PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

    /*
     * Step 3: connect() triggers the 3-way handshake.
     * Blocks until ESTABLISHED or error.
     * OS assigns an ephemeral source port automatically.
     */
    if (connect(sockfd, (struct sockaddr *)&server_addr,
                sizeof(server_addr)) == -1) {
        perror("connect"); return 1;
    }
    printf("Connected to server\n");

    /* Step 4: Send data */
    n = send(sockfd, send_buf, strlen(send_buf), 0);
    printf("Sent %zd bytes: %s\n", n, send_buf);

    /*
     * Step 5: Receive echo.
     * TCP byte-stream: recv() might not return all bytes at once.
     * For a robust client, loop until expected bytes received.
     */
    n = recv(sockfd, recv_buf, BUFSIZE - 1, 0);
    if (n > 0) {
        recv_buf[n] = '\0';
        printf("Echo received: %s\n", recv_buf);
    }

    /*
     * Step 6: close() sends FIN to server, starting
     * the 4-way connection termination sequence.
     */
    close(sockfd);
    return 0;
}

TCP vs UDP — Function Usage Summary

Operation TCP (SOCK_STREAM) UDP (SOCK_DGRAM)
Create socket socket(AF_INET, SOCK_STREAM, 0) socket(AF_INET, SOCK_DGRAM, 0)
Server bind bind() — mandatory bind() — mandatory
Server listen listen() — mandatory Not used
Server accept accept() — new fd per client Not used
Client connect connect() — triggers 3-way handshake Optional (sets default peer)
Send data send() / write() sendto() (or send() if connected)
Receive data recv() / read() recvfrom() (or recv() if connected)
Close close() — triggers 4-way FIN close() — just deallocates socket

Interview Questions – TCP
Q1. What does it mean that TCP is “connection-oriented”?Connection-oriented means that before any data is exchanged, both parties must agree to communicate via a 3-way handshake (SYN → SYN-ACK → ACK). This establishes shared state (sequence numbers, buffer sizes) in the kernel. The connection is explicitly terminated when done. This is different from UDP, which is connectionless and sends data without any prior setup.

Q2. What is a TCP endpoint and what information does the kernel maintain in it?A TCP endpoint is the kernel’s data structure for one end of a TCP connection. It contains: a send buffer (holds data the app has sent but not yet ACKed by the peer), a receive buffer (holds data received from the peer but not yet read by the app), sequence and acknowledgment numbers, window sizes for flow control, and the current connection state (ESTABLISHED, FIN_WAIT, etc.). Each connected socket corresponds to one TCP endpoint.

Q3. What is the TCP 3-way handshake and why are exactly 3 steps needed?The 3-way handshake (SYN → SYN-ACK → ACK) establishes a TCP connection. Three steps are needed because each side must send its initial sequence number and have it acknowledged: Step 1 (SYN): client sends its sequence number. Step 2 (SYN-ACK): server acknowledges client’s sequence number AND sends its own. Step 3 (ACK): client acknowledges server’s sequence number. With only 2 steps, the server’s sequence number would never be acknowledged, making the connection incomplete.

Q4. What is the byte-stream nature of TCP and what does it mean for an application?TCP treats data as a continuous stream of bytes — not as discrete messages. If the sender calls write() three times with 100 bytes each, the receiver might read it as one 300-byte chunk, or six 50-byte chunks, or any other combination. Unlike UDP (which preserves datagram boundaries), TCP provides no message boundaries. Applications that need message framing must implement it themselves — e.g., prefixing each message with its length, or using a delimiter.

Q5. Why does accept() return a new socket descriptor and what is listen_fd used for after?accept() returns a new socket descriptor specific to the accepted client connection. The original socket (from socket()+listen()) continues to listen for new incoming connections — it is the “welcoming socket.” The new fd from accept() is used for communicating with that specific client. This design allows a server to handle many clients: one listening fd + one connected fd per client.

Q6. What is the listen() backlog parameter?The backlog parameter to listen() specifies the maximum number of connections that can be queued (have completed the 3-way handshake but not yet been accepted by the server with accept()). If the queue is full when a new client tries to connect, the kernel may drop or refuse the new SYN. A common value is 5–128. Set it higher for busy servers. SOMAXCONN is the system maximum.

Q7. What is the TIME_WAIT state in TCP and why does it exist?TIME_WAIT is a state the active closer (the side that sent the first FIN) enters after the 4-way termination exchange. It lasts for 2 × MSL (Maximum Segment Lifetime — typically 2 minutes). It exists for two reasons: (1) to ensure the final ACK reaches the passive closer (if the ACK is lost, the passive closer retransmits its FIN, and the active closer needs to be alive to re-send the ACK); (2) to allow old duplicate segments from the just-closed connection to expire before a new connection uses the same port pair.

Q8. What is flow control in TCP and how does it work?Flow control prevents the sender from overwhelming the receiver’s buffer. The receiver advertises its available buffer space in the receive window field of every ACK. The sender limits how much unacknowledged data it can have “in flight” to this window size. If the receiver’s buffer fills up (the app is reading slowly), it advertises a zero window, causing the sender to pause until the receiver drains its buffer and advertises a nonzero window again.

Q9. What is the SO_REUSEADDR socket option and why is it important for TCP servers?SO_REUSEADDR allows a socket to bind to a port that is in the TIME_WAIT state. Without it, when you restart a TCP server quickly after stopping it, bind() fails with EADDRINUSE because the old socket is still in TIME_WAIT for up to 2 minutes. Setting SO_REUSEADDR (via setsockopt()) before bind() bypasses this restriction, allowing immediate port reuse during development and in production restarts.

TCP vs UDP — Complete Comparison
Property TCP UDP
Connection Connection-oriented (3-way handshake) Connectionless (no handshake)
Reliability Guaranteed delivery with retransmission Best-effort, no retransmission
Order In-order delivery guaranteed No ordering guarantee
Data model Byte stream (no boundaries) Discrete datagrams (boundaries preserved)
Speed/Overhead Higher overhead (headers, handshakes, ACKs) Very low overhead (8-byte header)
Flow control Yes (receive window) No
Congestion control Yes (slow start, AIMD) No
Best for HTTP, SSH, FTP, email, databases DNS, VoIP, games, live streaming

Chapter 58 Complete!

You have covered all the TCP/IP fundamentals: IPv4, IPv6, Port Numbers, UDP, and TCP.

← Back to Part 1: IPv4 EmbeddedPathashala Home

Leave a Reply

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