TCP Protocol
Intermediate
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.
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.).
| 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). |
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.
Each TCP endpoint maintains two buffers in the kernel:
| Client-Side Kernel | Server-Side Kernel | |||||
|
←→ IP packets flow both ways |
|
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.
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.
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 |
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.
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.
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.
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.
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.
| 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.
