昨天(Day 15)我們完成了第一個應用層協定 DNS Client,整個網路協定路徑為:
Application
↓
DNS
↓
UDP
↓
IPv4
↓
Ethernet
今天(Day 16),我們正式踏入網際網路最核心、最龐大的傳輸層協定 —— TCP(Transmission Control Protocol,傳輸控制協定)!
接下來 Day16~Day21 會形成一個 TCP 小系列:
Day16:讀懂 TCP Header,完成 TCP 封包接收與解析
Day17:建立 TCP Socket Table 與 TCP State Machine
Day18:收到 SYN 後建立連線槽位,並回覆 SYN-ACK
Day19:收到最後 ACK,進入 ESTABLISHED
Day20:在 ESTABLISHED 狀態接收 Payload,並回傳 ACK
Day21:主動送出 Payload,完成最小雙向資料傳輸
這個小系列的目標是先做出「最小可觀察、可理解」的 TCP 教學模型,而不是一次補齊真實 Kernel TCP 的所有機制。
現代網際網路幾乎所有重要的應用都建立在 TCP 之上:
HTTP / HTTPS (Web 瀏覽)
SSH (遠端連線)
Git (程式碼版本控制)
MySQL / PostgreSQL (資料庫連線)
SMTP / IMAP (電子郵件傳輸)
今天我們在自製網路堆疊中成功打通了 TCP 封包的接收、解析與解構路徑:
Ethernet
↓
IPv4 (Protocol 6)
↓
TCP
struct tcp_hdr。tcp_receive(),讓 IPv4 Protocol 6 封包進入 TCP 解析流程。send_tcp_syn 測試工具,在 TAP 上送入並解析第一個 TCP SYN 封包。| 特性 | UDP (User Datagram Protocol) | TCP (Transmission Control Protocol) |
|---|---|---|
| 連線模式 | 無連線(Connectionless) | 連線導向(Connection-Oriented) |
| 生活比喻 | 普通平信(寄了就走,不理對方是否收到) | 雙向電話 / 掛號信(確保通話建立才講話) |
| 可靠性 | 不保證送達、不保證順序、不自動重傳 | 保證送達、保證順序正確、掉包自動重傳 |
| 傳輸單位 | 獨立封包(Message-based) | 連續位元組串流(Byte Stream) |
| 流量控制 | 無(發送端可隨意狂噴) | 滑動視窗(Sliding Window Flow Control) |
| 擁塞控制 | 無 | 具備慢啟動、擁塞避免機制 |
| 標頭開銷 | 固定 8 Bytes | 最小 20 Bytes(可擴充 Options) |
標準 TCP 標頭固定最小長度為 20 Bytes(若無額外 Option):
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port | (4 Bytes)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number | (4 Bytes)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number | (4 Bytes)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window | (4 Bytes)
| (4b) | (6b) |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer | (4 Bytes)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options and Padding (可選) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Source Port:客戶端作業系統動態配發的臨時埠(如 52144)。Destination Port:目的服務埠(如 HTTP 80、HTTPS 443、SSH 22)。ntohs() 轉換為本機位元組序。SEQ 標明了**「當前這包封包的第一個位元組,在整個資料流中是第幾個 Byte」**。接收端因此能精準拼裝還原原始資料,並發現遺失。ntohl()。ACK bit 為 1 時,此欄位才有效。ntohl()。5 × 4 = 20 Bytes),若帶有 Options 會大於 5。(tcp->data_offset >> 4)。Window = 0(Zero Window)迫使發送端暫停,防止接收端緩衝區溢位。tcp->checksum = 0,測試工具也不驗證 TCP Checksum;若要與真實 OS TCP Stack 穩定互通,後續必須實作完整 TCP Pseudo Header Checksum。URG 設為 1 時生效,標示緊急資料相對於 SEQ 的位移量(如傳送 Ctrl + C 中斷訊號)。TCP Flags 是單個 Byte 中的位元標誌,每個 bit 代表一個特定的控制訊號:
| Flag | 位元遮罩 (Hex) | 意義與說明 |
|---|---|---|
| SYN | 0x02 |
Synchronize:發起連線建立,同步初始序號(ISN)。 |
| ACK | 0x10 |
Acknowledgment:確認收到資料,ACK 欄位有效。 |
| FIN | 0x01 |
Finish:發送端已無資料要傳送,禮貌性關閉連線。 |
| RST | 0x04 |
Reset:強制重設/中斷連線(如連接未開啟的 Port 或連線崩潰)。 |
| PSH | 0x08 |
Push:請求接收端儘速將緩衝區資料推交應用程式。 |
| URG | 0x20 |
Urgent:緊急指標有效。 |
Flags: 0x02 [SYN]
Flags: 0x12 [SYN, ACK]
Flags: 0x10 [ACK]
include/tcp.h)#ifndef TCP_H
#define TCP_H
#include <stdint.h>
#include <stddef.h>
#ifndef IPPROTO_TCP
#define IPPROTO_TCP 6
#endif
/* TCP Flags 定義 */
#define TCP_FIN 0x01
#define TCP_SYN 0x02
#define TCP_RST 0x04
#define TCP_PSH 0x08
#define TCP_ACK 0x10
#define TCP_URG 0x20
/* TCP Header 結構 (RFC 793 - 固定最小 20 Bytes) */
struct tcp_hdr
{
uint16_t src_port;
uint16_t dst_port;
uint32_t seq;
uint32_t ack;
uint8_t data_offset;
uint8_t flags;
uint16_t window;
uint16_t checksum;
uint16_t urgent_ptr;
} __attribute__((packed));
void tcp_print_header(const struct tcp_hdr *tcp);
void tcp_receive(int fd, const uint8_t *buffer, size_t len);
#endif /* TCP_H */
src/tcp.c)#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "ethernet.h"
#include "ipv4.h"
#include "tcp.h"
void tcp_print_header(const struct tcp_hdr *tcp)
{
printf("\nTCP Packet\n");
printf("----------------------------------------\n");
printf("Source Port : %u\n", ntohs(tcp->src_port));
printf("Destination Port : %u\n", ntohs(tcp->dst_port));
printf("SEQ : %u\n", ntohl(tcp->seq));
printf("ACK : %u\n", ntohl(tcp->ack));
printf("Data Offset : %u (Header Length: %u bytes)\n",
(tcp->data_offset >> 4),
(tcp->data_offset >> 4) * 4);
printf("Flags : 0x%02x [ %s%s%s%s%s%s]\n",
tcp->flags,
(tcp->flags & TCP_SYN) ? "SYN " : "",
(tcp->flags & TCP_ACK) ? "ACK " : "",
(tcp->flags & TCP_FIN) ? "FIN " : "",
(tcp->flags & TCP_RST) ? "RST " : "",
(tcp->flags & TCP_PSH) ? "PSH " : "",
(tcp->flags & TCP_URG) ? "URG " : "");
printf(" SYN = %d, ACK = %d, FIN = %d, RST = %d\n",
(tcp->flags & TCP_SYN) ? 1 : 0,
(tcp->flags & TCP_ACK) ? 1 : 0,
(tcp->flags & TCP_FIN) ? 1 : 0,
(tcp->flags & TCP_RST) ? 1 : 0);
printf("Window : %u\n", ntohs(tcp->window));
printf("Checksum : 0x%04x\n", ntohs(tcp->checksum));
printf("Urgent Pointer : %u\n", ntohs(tcp->urgent_ptr));
printf("----------------------------------------\n");
}
void tcp_receive(int fd, const uint8_t *buffer, size_t len)
{
(void)fd;
// 取得 IPv4 標頭以計算動態 IP Header 長度
struct ipv4_hdr *ip = (struct ipv4_hdr *)(buffer + ETH_HEADER_LEN);
size_t ip_hdr_len = (ip->version_ihl & 0x0F) * 4;
// 確保封包長度足夠容納 Ethernet + IPv4 + TCP Header
if (len < ETH_HEADER_LEN + ip_hdr_len + sizeof(struct tcp_hdr)) {
printf("[TCP] Packet too short\n");
return;
}
struct tcp_hdr *tcp = (struct tcp_hdr *)(buffer + ETH_HEADER_LEN + ip_hdr_len);
tcp_print_header(tcp);
}
src/tap.c)與 ICMP、UDP 完全對齊的整潔架構:
// IPv4 Protocol Dispatcher
switch (ip->protocol) {
case IPPROTO_ICMP:
icmp_receive(fd, buffer, n);
break;
case IPPROTO_UDP:
udp_receive(fd, buffer, n);
break;
case IPPROTO_TCP:
tcp_receive(fd, buffer, n);
break;
default:
break;
}
test/send_tcp_syn.c)透過 Linux Raw Socket 注入一包標準的 TCP SYN 封包至 tap0:
Src Port: 52144
Dst Port: 80
SEQ: 1000
ACK: 0
Flags: TCP_SYN (0x02)
Window: 4096
./network) 輸出[ACCEPT]
Ethernet Frame
-------------------------
Destination : 02:00:00:00:00:01
Source : 52:54:00:12:34:56
EtherType : 0x0800
IPv4 Packet
------------------
Version : 4
Header Length: 20 bytes
TTL : 64
Protocol : 6
Checksum : 0x62e5 (OK)
Source IP : 10.0.0.1
Destination IP : 10.0.0.2
[IPv4] Local Delivery (for me)
TCP Packet
----------------------------------------
Source Port : 52144
Destination Port : 80
SEQ : 1000
ACK : 0
Data Offset : 5 (Header Length: 20 bytes)
Flags : 0x02 [ SYN ]
SYN = 1, ACK = 0, FIN = 0, RST = 0
Window : 4096
Checksum : 0x0000
Urgent Pointer : 0
----------------------------------------
Frame length: 54 bytes
0x0800 (IPv4)。10.0.0.2,Protocol 6 (TCP),Checksum 驗證無誤,判定 Local Delivery。tcp_receive() 接收並由 tcp_print_header() 完整印出!所有欄位皆精確對應我們在測試工具中設定的數值。 Ethernet (L2)
│
┌─────────────┴─────────────┐
│ │
ARP (0x0806) IPv4 (0x0800)
│ │
ARP Cache Table ┌────────────┼────────────┐
│ │ │
ICMP (1) UDP (17) TCP (6) <-- 【Day 16】
│ │ │
Echo Reply DNS / App Header Parser
├── Ports
├── Sequence (SEQ)
├── Acknowledge (ACK)
├── Flags (SYN/ACK...)
└── Window Size
今天我們看懂了 TCP 封包的「骨架」。
明天,我們將賦予 TCP 真正的「靈魂」:
80、8080),以及目前的連線狀態。CLOSED
LISTEN
SYN_SENT
SYN_RECEIVED
ESTABLISHED
SYN 時,如何變更狀態並準備回覆 SYN + ACK!