This part explains how TCP/IP is organized into layers, why that layered design is so powerful, and how data is wrapped (encapsulated) as it travels from your application down to the physical wire. This is foundational knowledge for anyone writing socket programs or debugging network issues.
A networking protocol is a set of rules that define how information is formatted and transmitted across a network. Two machines that both follow the same protocol can exchange data correctly — regardless of their hardware or operating system.
Protocols are organized into layers. Each layer does a specific job and provides services to the layer above it, while using services from the layer below it. This is called a layered architecture or protocol stack.
The TCP/IP suite is organized into four layers. From top to bottom:
Your program: HTTP client, SSH, custom socket app
Socket types: SOCK_STREAM | SOCK_DGRAM | SOCK_RAW
TCP (reliable stream) | UDP (unreliable datagram)
IP — routes packets across networks
Network interface hardware (Ethernet, Wi-Fi card)
Cable, fiber optic, radio waves (Wi-Fi)
The code implementing all these layers together is called the protocol stack. On Linux, the kernel contains the TCP, UDP, and IP implementations. The data-link layer is implemented in device drivers and network interface hardware.
Why is it called TCP/IP? Because TCP is the most heavily used transport-layer protocol. When people say “TCP/IP”, they mean the entire family — TCP, UDP, IP, and all supporting protocols.
The TCP/IP suite includes several supporting protocols beyond TCP, UDP, and IP:
| Protocol | Full Name | Purpose | Used By |
|---|---|---|---|
| ARP | Address Resolution Protocol | Maps IP addresses to hardware (MAC) addresses | Kernel, automatically |
| ICMP | Internet Control Message Protocol | Conveys error and control information between hosts | ping, traceroute |
| IGMP | Internet Group Management Protocol | Manages IP multicast group membership | Multicast applications |
ping 8.8.8.8, the kernel sends an ICMP Echo Request packet and waits for an ICMP Echo Reply. If the host is alive and reachable, you get a reply. This operates below TCP/UDP — at the IP/ICMP level.Transparency means each layer hides its internal complexity from the layers above it. A higher layer only sees a clean interface — not the messy details underneath.
“I just want to send bytes to a remote address. I don’t care how.”
“I handle reliability, ordering, flow control. App doesn’t see this.”
“I handle routing between networks. TCP doesn’t see this.”
“I handle frames on the wire. IP doesn’t see this.”
- Your app works over Ethernet, Wi-Fi, fiber — without any changes to your code
- You can swap UDP for TCP with minimal code change
- The kernel handles packet loss, retransmit, routing — your code just reads/writes
- New physical technologies can be added at the data-link layer without rewriting TCP or your app
Encapsulation is how data passes down through protocol layers. Each layer treats the data from the layer above it as an opaque blob — it does not try to interpret it. Instead, it wraps the blob in its own packet format by adding a header (and sometimes a trailer), then passes the wrapped packet to the layer below.
e.g., “GET /index.html HTTP/1.1\r\n…”
On the receiving side, the opposite happens — each layer strips its own header and passes the inner data up to the layer above. This is called de-encapsulation.
IP Datagram → IP strips IP header → TCP Segment
TCP Segment → TCP strips TCP header → Application Data
Application Data → Your code reads it
When you create a socket in C, you specify a socket type that determines which transport protocol you use:
| Socket Type | Protocol | Layer | Characteristic |
|---|---|---|---|
SOCK_STREAM |
TCP | Transport | Reliable, ordered, byte stream. Connection-oriented. |
SOCK_DGRAM |
UDP | Transport | Unreliable, unordered datagrams. Connectionless. |
SOCK_RAW |
IP (raw) | Network | Bypass transport layer. Construct your own headers. Requires root. |
Example 1: SOCK_STREAM (TCP) socket creation
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(void)
{
/* AF_INET = IPv4 (network layer)
SOCK_STREAM = TCP (transport layer)
Protocol 0 = let kernel choose (chooses TCP for SOCK_STREAM) */
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
return 1;
}
printf("TCP socket created. fd = %d\n", sockfd);
/* At this point we have a socket that will use:
Application data → TCP header → IP header → Data-link frame */
close(sockfd);
return 0;
}
Example 2: SOCK_DGRAM (UDP) socket creation
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(void)
{
/* SOCK_DGRAM = UDP at transport layer
No connection, no guarantee of delivery or ordering */
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket");
return 1;
}
printf("UDP socket created. fd = %d\n", sockfd);
close(sockfd);
return 0;
}
Example 3: SOCK_RAW — bypass transport layer, access IP directly (requires root)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(void)
{
/* SOCK_RAW with IPPROTO_ICMP:
Lets us send/receive raw ICMP packets — like ping does.
We must construct the ICMP header ourselves.
Requires root privileges (or CAP_NET_RAW capability). */
int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sockfd == -1) {
perror("socket (need root for SOCK_RAW)");
return 1;
}
printf("Raw ICMP socket created. fd = %d\n", sockfd);
close(sockfd);
return 0;
}
Example 4: Use tcpdump to watch encapsulation in action
# In terminal 1 — capture packets on loopback interface
sudo tcpdump -i lo -vv -X port 8080
# In terminal 2 — send a quick TCP connection
nc 127.0.0.1 8080
# tcpdump output shows you:
# 1. The Ethernet frame header (data-link layer)
# 2. The IP datagram header (network layer: src IP, dst IP, TTL)
# 3. The TCP segment header (transport layer: src port, dst port, seq, ack)
# 4. The payload (application data)
# This is encapsulation visible in real time!
Example 5: Read IP header fields from a raw socket (shows encapsulation structure)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* struct iphdr */
#include <arpa/inet.h>
#include <string.h>
int main(void)
{
char buf[65536];
struct sockaddr_in saddr;
socklen_t saddr_len = sizeof(saddr);
/* Raw socket — capture all IP packets coming in */
int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
if (sockfd < 0) {
perror("socket (need root)");
return 1;
}
printf("Listening for TCP packets (IP header visible)...\n");
/* Receive one packet */
int n = recvfrom(sockfd, buf, sizeof(buf), 0,
(struct sockaddr *)&saddr, &saddr_len);
if (n < 0) { perror("recvfrom"); return 1; }
/* Cast beginning of buffer to IP header */
struct iphdr *iph = (struct iphdr *)buf;
char src[INET_ADDRSTRLEN], dst[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &iph->saddr, src, sizeof(src));
inet_ntop(AF_INET, &iph->daddr, dst, sizeof(dst));
printf("IP Header:\n");
printf(" Version : %d\n", iph->version);
printf(" IHL : %d bytes\n", iph->ihl * 4);
printf(" TTL : %d\n", iph->ttl);
printf(" Protocol : %d (6=TCP, 17=UDP, 1=ICMP)\n", iph->protocol);
printf(" Src IP : %s\n", src);
printf(" Dst IP : %s\n", dst);
printf(" Total Len : %d bytes (IP hdr + TCP hdr + data)\n",
ntohs(iph->tot_len));
close(sockfd);
return 0;
}
/* Compile: gcc -o raw_ip raw_ip.c
Run: sudo ./raw_ip */
ping sends ICMP Echo Request/Reply; traceroute uses ICMP Time Exceeded messages.