恭喜!你已經完整走過了這段極具挑戰性的 30 天網路工程師硬核實作之路。
今天我們不再學習新的協定,而是把過去 29 天親手雕琢的所有模組——Ethernet、ARP、IPv4、ICMP、UDP、TCP、Socket API、HTTP Server 到 Static File Controller 全部串聯起來,透過真實的封包分析,見證資料是如何從最底層的虛擬網卡原始 Bytes,一路奔馳並渲染至應用層!
Browser / curl
↓ (HTTP Request)
HTTP Server (Router / Controller)
↓ (API Call)
Socket API (socket_accept / socket_recv / socket_send / socket_close)
↓ (Stream Buffer & State Machine)
TCP (3-Way Handshake, Sliding Window, Retransmission, Teardown)
↓ (IP Datagram / Checksum / Routing)
IPv4 (Local Delivery / Forwarding Lookup)
↓ (Frame Encapsulation / ARP Mapping)
Ethernet (MAC Header / EtherType Dispatcher)
↓ (Virtual Wire)
TAP Device (/dev/net/tun)
↓
Linux Kernel / Real World
10.0.0.2 is at 02:00:00:00:00:01)。www/index.html。[TCP Retransmission] 的底層成因,並在 src/tcp.c 修正競爭狀態。如果回到 Day 1:
面對虛擬網卡 tap0,我們只能看到毫無結構的原始二進位位元組:
01 02 03 04 a0 b1 c2 d3 08 00 45 00 ...
而今天在 Day 30:
你的程式碼已經具備完整的七層抽象能力:
Browser ──(HTTP)──> TCP ──(IPv4)──> Ethernet ──(Bytes)──> TAP (/dev/net/tun)
TAP (/dev/net/tun) ──(Frames)──> Ethernet ──(Packets)──> IPv4 ──(Segments)──> TCP ──(Streams)──> HTTP ──> Web Server
整個協定棧皆由 C 語言純手工打造,不依賴任何外部網路通訊庫:

┌────────────────────────────────────────────────────────┐
│ Web Browser / Client │
└───────────────────────────┬────────────────────────────┘
│ HTTP Request (GET /)
▼
┌────────────────────────────────────────────────────────┐
│ HTTP Server & Controller 模組 │
│ (http.c / controller.c / www/ 靜態磁碟讀取) │
└───────────────────────────┬────────────────────────────┘
│ socket_recv() / socket_send()
▼
┌────────────────────────────────────────────────────────┐
│ Socket API 抽象層 │
│ (socket.c) │
└───────────────────────────┬────────────────────────────┘
│ 狀態機流轉 & 傳送緩衝區
▼
┌────────────────────────────────────────────────────────┐
│ TCP 傳輸層 (Layer 4) │
│ (tcp.c: 三向交握 / 重傳佇列 / 四次揮手 / TIME_WAIT) │
└───────────────────────────┬────────────────────────────┘
│ TCP 標頭封裝 (Ports, Seq, Ack)
▼
┌────────────────────────────────────────────────────────┐
│ IPv4 網路層 (Layer 3) │
│ (ipv4.c / icmp.c / routing.c: Checksum / 路由決策) │
└───────────────────────────┬────────────────────────────┘
│ IP 標頭封裝 (Src/Dst IP, TTL)
▼
┌────────────────────────────────────────────────────────┐
│ Ethernet 資料鏈結層 (Layer 2) │
│ (ethernet.c / arp.c: 0x0800 & 0x0806 派送) │
└───────────────────────────┬────────────────────────────┘
│ Ethernet Frame (14 Bytes MAC)
▼
┌────────────────────────────────────────────────────────┐
│ TAP 虛擬網卡驅動 (Layer 1) │
│ (tap.c: 透過 /dev/net/tun 讀寫) │
└────────────────────────────────────────────────────────┘
當你在客戶端執行:
curl -i http://10.0.0.2:8080/
整個堆疊在幕後完成的 7 大驗收關卡如下:
10.0.0.2,但本地 ARP 表尚未有其紀錄。tap0 廣播:Who has 10.0.0.2? Tell 10.0.0.1。arp.c 收到廣播,確認查詢目標是自己的 LOCAL_IP(10.0.0.2),立即發送單播 ARP Reply:
10.0.0.2 is at 02:00:00:00:00:01
0x0800(IPv4)。dst_ip 是否等於 10.0.0.2:
[SYN](Seq = 0)。LISTEN 進入,並調用 tcp_send_syn_ack():
SYN | ACK,自身 Seq = 0,Ack = 1。[ACK](Seq = 1, Ack = 1)。TCP_ESTABLISHED!GET / HTTP/1.1
Host: 10.0.0.2:8080
http.c 解析出 Method 為 GET,Path 為 /。controller.c 將 / 映射至本地磁碟路徑 www/index.html。fopen("rb")、fseek、ftell、fread 二進位安全載入 HTML 內容。HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 233
<!DOCTYPE html>...
socket_send() 寫入 TCP 傳送緩衝區,自動切片並發送至虛擬網卡。socket_close(),送出 [FIN, ACK]。TIME_WAIT,釋放資源。若要從零重現 Day 30 的完整成果,可以依照以下流程驗收:
make clean && make network
sudo ./network
啟動後應看到伺服器監聽在 Port 8080,並等待 TCP 連線進入。
sudo ip addr add 10.0.0.1/24 dev tap0 2>/dev/null || true
sudo ip link set dev tap0 up
curl -i http://10.0.0.2:8080/
curl -i http://10.0.0.2:8080/api/time
curl -i http://10.0.0.2:8080/logo.png --output logo.out
建議同時開啟 Wireshark 監聽 tap0,過濾條件可使用:
arp || ip.addr == 10.0.0.2 || tcp.port == 8080
這組流程能一次驗證 ARP、IPv4、TCP、HTTP、靜態檔案讀取與二進位傳輸。
在 Windows Wireshark 監聽 tap0,發送 curl -i http://10.0.0.2:8080/ 實測,抓取到的完整封包序列如下:

| No. | Time (s) | Source | Destination | Protocol | Length | Info (關鍵旗標與序號) | 說明 |
|---|---|---|---|---|---|---|---|
| 1 | 0.000000 |
ae:f9:ff:0e:15:26 |
Broadcast |
ARP | 42 | Who has 10.0.0.2? Tell 10.0.0.1 |
Linux 主機廣播查詢 MAC |
| 2 | 0.000138 |
02:00:00:00:00:01 |
ae:f9:ff:0e:15:26 |
ARP | 42 | 10.0.0.2 is at 02:00:00:00:00:01 |
自製協定棧秒回單播回應 |
| 3 | 0.000142 |
10.0.0.1 |
10.0.0.2 |
TCP | 74 | 49844 -> 8080 [SYN] Seq=0 |
三向交握第 1 步 |
| 4 | 0.000300 |
10.0.0.2 |
10.0.0.1 |
TCP | 54 | 8080 -> 49844 [SYN, ACK] Seq=0 Ack=1 |
自製協定棧回覆交握 |
| 5 | 0.000333 |
10.0.0.1 |
10.0.0.2 |
TCP | 54 | 49844 -> 8080 [ACK] Seq=1 Ack=1 |
三向交握完成 (ESTABLISHED) |
| 6 | 0.000405 |
10.0.0.1 |
10.0.0.2 |
HTTP | 131 | GET / HTTP/1.1 |
客戶端發送 HTTP 請求 |
| 7 | 0.000714 |
10.0.0.2 |
10.0.0.1 |
TCP | 54 | 8080 -> 49844 [ACK] Seq=1 Ack=78 |
自製協定棧確認收到請求 |
| 8 | 0.004458 |
10.0.0.2 |
10.0.0.1 |
HTTP | 287 | HTTP/1.1 200 OK (text/html) |
自製協定棧回傳 HTML 網頁 |
| 9 | 0.004493 |
10.0.0.1 |
10.0.0.2 |
TCP | 54 | 49844 -> 8080 [ACK] Seq=78 Ack=234 |
客戶端確認收到網頁資料 |
| 10 | 0.004529 |
10.0.0.2 |
10.0.0.1 |
TCP | 54 | 8080 -> 49844 [FIN, ACK] Seq=234 Ack=78 |
自製協定棧主動發起關閉連線 |
| 11 | 0.004919 |
10.0.0.1 |
10.0.0.2 |
TCP | 54 | 49844 -> 8080 [FIN, ACK] Seq=78 Ack=235 |
Linux 客戶端捎帶回覆 FIN 與 ACK |
| 12 | 0.005280 |
10.0.0.2 |
10.0.0.1 |
TCP | 54 | 8080 -> 49844 [ACK] Seq=235 Ack=79 |
自製協定棧回覆最後 ACK,進入 TIME_WAIT |
[TCP Retransmission] 與捎帶機制在最初的驗收測試中,Wireshark 曾出現一筆黑底紅字的 [TCP Retransmission] 警告:
No. 23 (147.319569): 10.0.0.1 -> 10.0.0.2 [FIN, ACK] Seq=78 Ack=235
No. 24 (147.526343): 10.0.0.1 -> 10.0.0.2 [TCP Retransmission] [FIN, ACK] Seq=78 Ack=235
No. 25 (147.526499): 10.0.0.2 -> 10.0.0.1 [ACK] Seq=235 Ack=79
TCP 為保證可靠性,當送出一個關鍵封包(如帶有 FIN 旗標)時會啟動重傳計時器(RTO,Linux 預設約 200ms)。如果在 200ms 內未收到對方的 ACK,就會認為封包在網路中遺失,因而觸發重傳。
Linux 核心在收到我們的 FIN 後,為了節省網路封包,使用了**捎帶(Piggybacking)**機制:將「確認我們 FIN 的 ACK」與「自己想關閉的 FIN」合併在同一包 [FIN, ACK] 送出。
但在原本的 src/tcp.c 狀態機中:
// 原始代碼:
if (conn->state == TCP_FIN_WAIT_1) {
if (tcp->flags & TCP_ACK) {
uint32_t ack_num = ntohl(tcp->ack);
if (ack_num == conn->seq) {
conn->state = TCP_FIN_WAIT_2;
return; // ★ 兇手:直接 return,遺漏了同封包內的 TCP_FIN 旗標!
}
}
}
程式收到 ACK 後便直接退出,導致對方的 FIN 被「已讀不回」。直到 200ms 後 Linux 觸發重傳,才在 FIN_WAIT_2 狀態下被處理。
在 src/tcp.c 的 TCP_FIN_WAIT_1 判斷中,加入對捎帶 FIN 的即時偵測:
if (conn->state == TCP_FIN_WAIT_1) {
if (tcp->flags & TCP_ACK) {
uint32_t ack_num = ntohl(tcp->ack);
if (ack_num == conn->seq) {
// 若對方在同一個封包同時捎帶了 FIN (Piggybacked FIN+ACK)
if (tcp->flags & TCP_FIN) {
uint32_t received_seq = ntohl(tcp->seq);
conn->ack = received_seq + payload_len + 1;
tcp_send_ack(fd, conn); // ★ 立即回送最後的 ACK!
conn->state = TCP_TIME_WAIT;
conn->time_wait_start = get_current_time_ms();
return;
}
conn->state = TCP_FIN_WAIT_2;
return;
}
}
}
修改後重新編譯執行,重傳警告徹底消失,關閉連線瞬間在 0.35 毫秒 內優雅落幕!
很多人學習計算機網路,背誦了許多名詞:OSI 七層、三向交握、子網路遮罩、滑動窗口。
但現在面對經典面試題目:
「當你在瀏覽器輸入網址並按下 Enter 後,底層究竟發生了什麼事?」
你已經能以底層實作者的視角,清晰還原每一行程式碼的運作:

[SYN] 與亂數初始序號(ISN)完成三向交握,同步序號與接收視窗(Receive Window)。Content-Length 的 HTTP Response。FIN 與 ACK,進入 TIME_WAIT 狀態以確保殘留封包消散,完成生命週期循環。回顧這 30 天,你親手完成的模組清單:
這個 30 天版本已經打通完整教學閉環,但它仍然刻意保留一些簡化,方便聚焦在協定本質:
socket_recv() 一次讀到的 payload 會直接交給 parser;真實伺服器需要累積直到 \r\n\r\n 或完整 body 抵達。socket_send()。這些限制不是失敗,而是下一階段可以繼續擴建的清楚邊界。
這 30 天你從「發送者與伺服器」的視角親手建構了協定堆疊;而下一個里程碑,則是從「監聽者與分析者」的視角剖析全世界的網路流量:
Day 31 ~ Day 60:Build Your Own Wireshark (封包分析器與 AI 安全分析 Agent)
從手刻 PCAP 格式讀取器、Raw Socket 監聽、協定重組分析器、封包搜尋引擎,到結合 AI Agent 進行即時異常連線與攻擊偵測!
恭喜完成《Networking Fundamentals 30 天網路工程師路線》!這是一段扎實且令人驕傲的工程之旅!