What is TCP?
TCP stands for Transmission Control Protocol. It is one of the two main transport protocols used on the Internet (the other being UDP). TCP provides a reliable, ordered, and error-checked delivery of data between two programs communicating over a network.
When you open a socket in Linux using SOCK_STREAM, you are using TCP under the hood. Think of TCP like a phone call โ before you talk, you dial and connect. Once connected, everything you say arrives in order, and the other side can hear it all clearly.
A TCP connection is always between two endpoints. Each endpoint is maintained by the TCP layer on that machine. One side is the sending TCP and the other is the receiving TCP.
TCP is bidirectional โ both sides can send and receive at the same time. But when we describe a specific data flow, we call one end the sender and the other the receiver for that direction.
The TCP layer maintains state information at each endpoint โ things like how much data was sent, what was acknowledged, what the sequence numbers are, and the size of the receive buffer. This is why TCP is called a stateful protocol.
Before any data can be sent, TCP must establish a connection. This happens through a process called the three-way handshake.
During this handshake, both sides also exchange options โ for example, the maximum segment size (MSS) they support, whether they support window scaling, etc.
The handshake ensures both sides are ready to communicate and have agreed on initial parameters. Only after the third step (ACK) is the connection considered established.
TCP does not send your data as one big blob. It breaks data into smaller chunks called segments. Each segment:
- Contains a checksum โ a mathematical value used to detect transmission errors
- Is transmitted inside a single IP datagram
- Has a header with control information (sequence number, ACK number, flags, window size)
The checksum covers both the header and data. When the receiver gets the segment, it recomputes the checksum. If the values don’t match, the segment is silently discarded (TCP then handles retransmission separately).
In Linux, you create a TCP socket using socket() with SOCK_STREAM. Below is a minimal TCP client that connects to a server โ demonstrating the connection establishment phase in real code.
/* tcp_client.c - minimal TCP connection example */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
#define MSG "Hello from TCP client\n"
int main(void)
{
int sockfd;
struct sockaddr_in srv_addr;
ssize_t bytes_sent;
/*
* Step 1: Create a TCP socket (SOCK_STREAM = TCP)
* AF_INET = IPv4
* SOCK_STREAM = reliable, ordered, connection-based (TCP)
* 0 = let OS choose protocol (TCP for SOCK_STREAM)
*/
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
/* Step 2: Fill in server address */
memset(&srv_addr, 0, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(SERVER_PORT); /* host to network byte order */
srv_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
/*
* Step 3: connect() triggers the THREE-WAY HANDSHAKE
* - Kernel sends SYN to server
* - Waits for SYN-ACK
* - Sends final ACK
* After connect() returns, TCP connection is ESTABLISHED
*/
if (connect(sockfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) == -1) {
perror("connect");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("TCP connection established to %s:%d\n", SERVER_IP, SERVER_PORT);
/* Step 4: Send data โ TCP segments this into one or more segments */
bytes_sent = write(sockfd, MSG, strlen(MSG));
if (bytes_sent == -1) {
perror("write");
} else {
printf("Sent %zd bytes\n", bytes_sent);
}
/* Step 5: Close โ triggers FIN handshake (connection teardown) */
close(sockfd);
return 0;
}
/* tcp_server.c - minimal TCP server to pair with above client */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080
#define BACKLOG 5
#define BUFSIZE 256
int main(void)
{
int listenfd, connfd;
struct sockaddr_in srv_addr, cli_addr;
socklen_t cli_len = sizeof(cli_addr);
char buf[BUFSIZE];
ssize_t n;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) { perror("socket"); exit(EXIT_FAILURE); }
memset(&srv_addr, 0, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(PORT);
srv_addr.sin_addr.s_addr = INADDR_ANY; /* accept on all interfaces */
/* bind: attach socket to port */
if (bind(listenfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) == -1) {
perror("bind"); exit(EXIT_FAILURE);
}
/* listen: mark socket as passive; ready to accept connections */
if (listen(listenfd, BACKLOG) == -1) {
perror("listen"); exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
/*
* accept() completes the three-way handshake from the server side.
* It returns a NEW socket (connfd) for this specific connection.
* listenfd keeps accepting new clients.
*/
connfd = accept(listenfd, (struct sockaddr *)&cli_addr, &cli_len);
if (connfd == -1) { perror("accept"); exit(EXIT_FAILURE); }
printf("Client connected!\n");
n = read(connfd, buf, BUFSIZE - 1);
if (n > 0) {
buf[n] = '\0';
printf("Received: %s", buf);
}
close(connfd);
close(listenfd);
return 0;
}
Compile and run:
# Terminal 1: Start server
gcc tcp_server.c -o tcp_server
./tcp_server
# Terminal 2: Run client
gcc tcp_client.c -o tcp_client
./tcp_client
The three-way handshake (SYN โ SYN-ACK โ ACK) establishes a TCP connection. It is needed to: (1) ensure both sides are ready, (2) synchronize initial sequence numbers, and (3) exchange connection parameters like MSS and window size. Without it, data could be sent to a side that is not ready.
A TCP segment is the unit of data at the TCP layer โ a chunk of data with a TCP header. A TCP stream is the logical byte-stream abstraction that the application sees. The stream is split into segments for transmission; the receiver reassembles them back into the original byte order before delivering to the application.
SOCK_STREAM requests a reliable, connection-oriented, byte-stream socket. On most systems this maps to TCP over IPv4/IPv6. It guarantees ordered delivery, no duplication, and error detection.
Checksums allow end-to-end detection of data corruption during transmission (bit errors in links, RAM errors in routers). If a segment’s checksum does not match, TCP discards it silently. TCP then handles recovery through retransmission.
Common TCP options exchanged during the handshake include: Maximum Segment Size (MSS), Window Scale factor (for large windows), SACK (Selective Acknowledgement) permitted, and Timestamps option for RTT measurement and PAWS (Protection Against Wrapped Sequence numbers).
listenfd is the passive socket โ it only receives connection requests. connfd is a new socket created by accept() specifically for the newly accepted client connection. The server uses connfd to exchange data with that client while listenfd continues accepting new connections.
