循序漸進搞懂 TCP 三次握手核心

前言

本文旨在通過形象的例子和實操,把無形的、虛擬的網路轉為具體的、可視化的。帶領網路小白一步步的掌握 TCP 三次握手核心知識點,為後續深入學習 TCP 協議打基礎。

通俗版

如下圖所示,小明(客戶端)給小美(服務端)打電話,在經過互相詢問和應答,確認通訊暢通後,才開始愉快地聊天。(本例子不一定無懈可擊主要是意會即可)

細節版

一個 TCP 報文段分為首部和數據兩部分,TCP 所有的功能都體現在首部的各個欄位中。

序列號:本報文段所發送數據的第一個位元組的序號,在建立連接時會隨機生成初始序列號 ISN(Inital Sequence Number)。

確認號:下一次應該收到的數據的序列號,若確認號為 N,則代表到序列號 N-1 為止的數據都已經正確收到。

控制位:

  • ACK:為 1 時確認號才生效,在建立連接後所有傳送的報文段 ACK 均為 1。

  • SYN:為 1 時表示這是連接請求(SYN=1,ACK=0)或者連接接受(SYN=1,ACK=1)報文。

由於上述報文都未攜帶數據,即 len=0,所以響應的 ack 等於請求的 seq+1。

實戰版

為了抓到比較純凈的包,我們實現一個簡單的 TCP Server,接受客戶端的請求並回復。

func main() {
  // 監聽埠
  l, err := net.Listen("tcp", "0.0.0.0:8080")
  if err != nil {
    panic(err)
  }
  defer l.Close()

  for {
    // 接受連接
    conn, err := l.Accept()
    if err != nil {
      log.Printf("accept err: %s\n", err)
      continue
    }
    go hello(conn)
  }
}

func hello(conn net.Conn) {
  defer conn.Close()
  buf := make([]byte, 1024)
  // 讀數據
  if _, err := conn.Read(buf); err != nil {
    log.Printf("conn read err: %s\n", err)
    return
  }
  log.Printf("%s\n", string(buf))

  // 寫數據
  if _, err := conn.Write([]byte("hello, xiao ming.\n")); err != nil {
    log.Printf("conn write err: %s\n", err)
    return
  }
}

客戶端請求:

echo -n "hello, xiao mei." | nc 81.68.197.93 8080

使用 wireshark 分析(統計 - 流量圖)可以清楚的看到三次握手的過程。

同時也能看到 seq 和 ack 的關係,在握手成功後發送了 len=16 的包後,然後 ack=17 表示序列號在 16 之前的數據都已經正確收到。

注意:ISN 是一個隨機數,並不為 0,wrieshark 為了顯示更友好,使用了相對序號,在滑鼠右鍵選項中取消即可看到真正的序列號,如:Seq=3503481500。

握手包源文件下載地址

狀態機

TCP 所謂的面向連接本質就是客戶端和服務端的數據結構都各自維護了一個」連接狀態「,三次握手期間狀態變化如下:

LISTEN:服務端主動監聽一個埠,等待客戶端的連接請求

SYN-SENT:發送 SYN 包後的狀態

SYN-RECEIVED:收到 SYN 包,且發送了 SYN+ACK 包後的狀態

ESTABLISHED:連接建立成功

# 使用 netstat 命令查看當前系統的 TCP 連接狀態
$ netstat -t
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 ubuntu:58824            39.156.66.18:http       TIME_WAIT
tcp        0      0 localhost:20172         localhost:37276         ESTABLISHED

參考

圖解TCP/IP(第5版)

電腦網路(第7版)謝希仁

酷殼 - TCP 的那些事兒(上)