iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Build on Google AI

用 Google AI 從零打造 Network Stack:30 天實作封包、TCP 與 HTTP Server系列 第 20 篇

Day 20:TCP Data Transfer (Part 1) — 接收 Payload 並回 ACK

  • 分享至 

  • xImage
  •  

Day 20:TCP Data Transfer (Part 1) — 接收 Payload 並回 ACK

在 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,確認全部收妥!)
  │                                                  │

今日學習目標與成果

  • [x] 使用 Data Offset 找到 TCP Payload 起點。
  • [x] 用 IPv4 total_length 計算 Payload 長度。
  • [x] 使用 fwrite() 安全印出 Payload。
  • [x] 在 TCP_ESTABLISHED 狀態處理帶資料的 TCP 封包。
  • [x] 根據收到的資料長度更新 conn->ack。
  • [x] 回傳純 ACK,確認已收到 Client 的資料。
  • [x] 使用 send_tcp_data 驗證 HTTP GET 請求與 ACK 回覆。

核心概念深入剖析

1. 為什麼 TCP 標頭長度不固定?

在 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 精準定位資料本體。


2. 為什麼 Data Offset 要右移 4 位再乘以 4?

在 TCP Header 的第 12 個 Byte 結構如下:

+-+-+-+-+-+-+-+-+
|Data Offset|Res|
+-+-+-+-+-+-+-+-+
 7 6 5 4 3 2 1 0
  1. 右移 4 位 (>> 4):
    因為長度值存放在高 4 位(bits 7~4),低 4 位為保留位(Reserved),右移能取出數值。
  2. 乘以 4 (* 4):
    4 個 bit 最大只能表示 $15$。若單位是 Byte,連最基本的 20 Bytes 都裝不下。因此協定規定以「32-bit (4 Bytes) 字組」為計數單位:
    • 數值 5:$5 \times 4 = 20\text{ Bytes}$(無 Options)
    • 數值 8:$8 \times 4 = 32\text{ Bytes}$(包含 12 Bytes Options)

Day20 TCP Payload 長度計算架構圖


3. ACK Number 的計算規則(Cumulative ACK 累積確認)

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 Bytes
    • Host: 10.0.0.2\r\n = 16 Bytes
    • 結尾空行 \r\n = 2 Bytes
  • 客戶端消耗的序號空間是 1001 ~ 1034。
  • 伺服器回覆的確認號碼公式:
    $$\text{Next ACK} = \text{received_seq} + \text{payload_len} = 1001 + 34 = \mathbf{1035}$$
  • 這代表伺服器向客戶端宣告:「1034 以前的位元組我全部收妥,請你下次從 1035 開始送!」

Day20 TCP 累積確認 ACK 計算流程圖


核心程式碼實作

1. Payload 輸出與純 ACK 發送器 (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;
}

2. 在 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;
    }

實機驗收步驟

步驟 1:啟動協定棧

在第一個終端機執行:

sudo ./network

步驟 2:執行自動化資料傳輸測試

在第二個終端機執行:

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 預告:

今天我們是被動接收資料並確認。明天(Day 21),我們將實作 TCP Send(主動傳送資料)!

當收到客戶端的 GET / HTTP/1.1 時,伺服器會先回傳一段簡單文字:

Hello from My TCP Stack

Day21 的重點是先確認 TCP Stack 具備主動送出 Payload 的能力;真正的 HTTP Response 格式會留到後續再逐步補上。


上一篇
Day 19:TCP Three-Way Handshake (Part 2) — 收到 ACK,進入 ESTABLISHED
下一篇
Day 21:TCP Data Transfer (Part 2) — 主動送出 Payload
系列文
用 Google AI 從零打造 Network Stack:30 天實作封包、TCP 與 HTTP Server 共 23 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言