在 Day 19 中,我們成功完成了 TCP 三向交握(Three-Way Handshake),連線正式進入了 TCP_ESTABLISHED 狀態。
今天(Day 20),我們邁出了通往真實世界網路應用的最關鍵一步:開始接收應用層的真正資料(Payload),精準解析長度,並回傳確認收到的純 ACK 封包!
Client Server
│ │
│ 1. 三向交握 (SYN -> SYN-ACK -> ACK) │
│<================================================>│ (連線建立:ESTABLISHED)
│ │
│ 2. HTTP Request (SEQ=1001, ACK=5001) │
│ GET / HTTP/1.1\r\nHost: 10.0.0.2\r\n\r\n │
├─────────────────────────────────────────────────>│ (解析 Data Offset,算出 Payload 長度)
│ │ (印出 [TCP DATA] 內容)
│ │ (計算 ACK 號碼 = 1001 + 34 = 1035)
│ │
│ 3. ACK (SEQ=5001, ACK=1035) │
│<─────────────────────────────────────────────────┤ (Server 回傳純 ACK,確認全部收妥!)
│ │
total_length 計算 Payload 長度。fwrite() 安全印出 Payload。TCP_ESTABLISHED 狀態處理帶資料的 TCP 封包。conn->ack。send_tcp_data 驗證 HTTP GET 請求與 ACK 回覆。在 UDP 中,標頭永遠固定為 8 Bytes。
但在 TCP 中,RFC 793 為了保留未來的擴展彈性,在固定的 20 Bytes 之後預留了 Options(選項欄位):
| Option 名稱 | 主要目的 | 常見出現時機 |
|---|---|---|
| MSS (Maximum Segment Size) | 協商最大封包大小,避免中間路由器 IP 分片 | SYN 握手階段 |
| Window Scale | 將 16-bit 接收視窗突破 64 KB 限制,支援現代 G 級頻寬 | SYN 握手階段 |
| SACK (Selective ACK) | 允許接收端回報局部收到的區塊,掉一包只重傳一包 | 資料傳輸階段 |
| Timestamps | 精準計算延遲 RTT,防止極速網路下的序號回繞(PAWS) | 幾乎每個資料封包 |
因此,TCP 標頭長度可在 20 ~ 60 Bytes 之間動態浮動,必須透過 data_offset 精準定位資料本體。
在 TCP Header 的第 12 個 Byte 結構如下:
+-+-+-+-+-+-+-+-+
|Data Offset|Res|
+-+-+-+-+-+-+-+-+
7 6 5 4 3 2 1 0
>> 4):* 4):5:$5 \times 4 = 20\text{ Bytes}$(無 Options)8:$8 \times 4 = 32\text{ Bytes}$(包含 12 Bytes Options)
TCP 是位元組流(Byte Stream)協定,序號代表的是「第幾個 Byte」:
SEQ = 1001 的 HTTP GET 請求。"GET / HTTP/1.1\r\nHost: 10.0.0.2\r\n\r\n" 共 34 Bytes:
GET / HTTP/1.1\r\n = 16 BytesHost: 10.0.0.2\r\n = 16 Bytes\r\n = 2 Bytes1001 ~ 1034。
src/tcp.c)void tcp_dump_payload(const uint8_t *data, size_t len)
{
printf("\n[TCP DATA]\n");
fwrite(data, 1, len, stdout);
printf("\n");
fflush(stdout);
}
int tcp_send_ack(int fd, struct tcp_socket *conn)
{
uint8_t buffer[ETH_HEADER_LEN + sizeof(struct ipv4_hdr) + sizeof(struct tcp_hdr)];
memset(buffer, 0, sizeof(buffer));
struct ethernet_hdr *eth = (struct ethernet_hdr *)buffer;
struct ipv4_hdr *ip = (struct ipv4_hdr *)(buffer + ETH_HEADER_LEN);
struct tcp_hdr *tcp = (struct tcp_hdr *)(buffer + ETH_HEADER_LEN + sizeof(struct ipv4_hdr));
// 1. TCP Header (純 ACK,無 Payload)
tcp->src_port = htons(conn->dst_port);
tcp->dst_port = htons(conn->src_port);
tcp->seq = htonl(conn->seq);
tcp->ack = htonl(conn->ack);
tcp->data_offset = (5 << 4);
tcp->window = htons(4096);
tcp->flags = TCP_ACK;
tcp->checksum = 0;
tcp->urgent_ptr = 0;
// 2. IPv4 Header
ip->version_ihl = (4 << 4) | 5;
ip->tos = 0;
ip->total_length = htons(sizeof(struct ipv4_hdr) + sizeof(struct tcp_hdr));
ip->identification = htons(2002);
ip->flags_fragment = 0;
ip->ttl = 64;
ip->protocol = IPPROTO_TCP;
ip->src_ip = conn->dst_ip;
ip->dst_ip = conn->src_ip;
ip->checksum = 0;
ip->checksum = ipv4_checksum(ip, sizeof(struct ipv4_hdr));
// 3. Ethernet Header
struct arp_entry *entry = arp_table_lookup((const uint8_t *)&conn->src_ip);
if (!entry || !entry->valid) {
printf("[TCP] Send ACK failed: MAC not in ARP table\n");
return -1;
}
memcpy(eth->dst, entry->mac, ETH_ADDR_LEN);
memcpy(eth->src, LOCAL_MAC, ETH_ADDR_LEN);
eth->ethertype = htons(ETHERTYPE_IPV4);
// 4. 發送至虛擬網卡
ssize_t sent = write(fd, buffer, sizeof(buffer));
if (sent < 0) {
perror("[TCP] write ACK failed");
return -1;
}
printf("[TCP] Sent ACK: SEQ=%u, ACK=%u\n", conn->seq, conn->ack);
return 0;
}
tcp_receive() 處理資料傳輸TCP Payload 長度應該依照 IPv4 Header 裡的 total_length 計算,而不是依照 Ethernet Frame Length。原因是 Ethernet Frame 可能包含 L2 padding 或其他鏈路層資訊;真正屬於 IPv4 packet 的範圍,應由 IPv4 自己的 total_length 定義。
// 計算 TCP Header 長度與 Payload 位置
size_t tcp_hdr_len = (tcp->data_offset >> 4) * 4;
const uint8_t *payload = (const uint8_t *)tcp + tcp_hdr_len;
uint16_t ip_total_len = ntohs(ip->total_length);
size_t payload_len = 0;
if (ip_total_len >= ip_hdr_len + tcp_hdr_len) {
payload_len = ip_total_len - ip_hdr_len - tcp_hdr_len;
}
...
if (conn->state == TCP_ESTABLISHED) {
if (payload_len > 0) {
printf("\n[TCP] Received Payload, Length = %zu bytes\n", payload_len);
tcp_dump_payload(payload, payload_len);
// 更新 ACK 號碼 (累加收到的 byte 數)
uint32_t received_seq = ntohl(tcp->seq);
conn->ack = received_seq + payload_len;
// 回傳 ACK 封包
tcp_send_ack(fd, conn);
}
return;
}
在第一個終端機執行:
sudo ./network
在第二個終端機執行:
sudo ./send_tcp_data
./network 終端機:[TCP] ACK Received! Handshake Complete!
[TCP] Connection Established: State -> ESTABLISHED
[TCP] Received Payload, Length = 34 bytes
[TCP DATA]
GET / HTTP/1.1
Host: 10.0.0.2
[TCP] Sent ACK: SEQ=5001, ACK=1035
./send_tcp_data 測試終端機:[1/3 Client] 發送 TCP SYN (SEQ=1000) 到 tap0...
[2/3 Client] 等待 Server 回傳 SYN-ACK...
[2/3 Client] 收到 Server 的 SYN-ACK!(Server SEQ=5000, ACK=1001)
[3/3 Client] 發送最終 ACK (SEQ=1001, ACK=5001) 完成三向交握!
[4/4 Client] 發送 HTTP Request (34 bytes) 到 tap0...
[5/4 Client] 等待 Server 回傳資料的 ACK...
========================================
[Client] 成功收到 Server 回傳的 ACK!(ACK=1035)
[Client] 驗證成功!Server 正確確認了全部 34 bytes 資料 (1001 + 34 = 1035)!
========================================
今天我們成功實現了從 TCP 標頭解析 Payload,並根據收到的長度推進 ACK 序號且回覆純 ACK,這是整個協定棧能處理真實資料傳輸的起點!本日仍假設資料按序抵達,尚未處理亂序、重複封包與重組;這些可靠性細節會留到後續的 TCP Receive Buffer / Reassembly 主題。
今天我們是被動接收資料並確認。明天(Day 21),我們將實作 TCP Send(主動傳送資料)!
當收到客戶端的 GET / HTTP/1.1 時,伺服器會先回傳一段簡單文字:
Hello from My TCP Stack
Day21 的重點是先確認 TCP Stack 具備主動送出 Payload 的能力;真正的 HTTP Response 格式會留到後續再逐步補上。