天天看點

位元組一面:TCP 三次握手,問的好細!

大家好,我是小林。

有位讀者在面試位元組時,被問到這麼個問題:

位元組一面:TCP 三次握手,問的好細!

概括起來,是這兩個問題:

  • TCP 三次握手中,用戶端收到的第二次握手中 ack 确認号不是自己期望的,會發生什麼?是直接丢棄 or 回 RST 封包?
  • 什麼情況下會收到不正确的 ack(第二次握手中的 ack) 呢?

問題解答

不賣關子,直接說這個問題,是回 RST 封包。過程如下圖:

位元組一面:TCP 三次握手,問的好細!

三次握手避免曆史連接配接

當用戶端連續發送多次建立連接配接的 SYN 封包,然後在網絡擁堵的情況,就會發生用戶端收到不正确的 ack 的情況。具體過程如下:

  • 用戶端先發送了 SYN(seq = 90) 封包,但是被網絡阻塞了,服務端并沒有收到,接着用戶端又重新發送了 SYN(seq = 100) 封包,注意不是重傳 SYN,重傳的 SYN 的序列号是一樣的。
  • 「舊 SYN 封包」比「最新的 SYN 」 封包早到達了服務端,那麼此時服務端就會回一個 SYN + ACK 封包給用戶端,此封包的确認号是 91(90+1)。
  • 用戶端收到後,發行自己期望收到的确認号應該是 100+1,而不是 90 + 1,于是就會回 RST 封包。
  • 服務端收到 RST 封包後,就會中止連接配接。
  • 後續最新的 SYN 抵達了服務端後,用戶端與服務端就可以正常的完成三次握手了。

上述中的「舊 SYN 封包」稱為曆史連接配接,TCP 使用三次握手建立連接配接的最主要原因就是防止「曆史連接配接」初始化了連接配接。

我們也可以從 RFC 793 知道 TCP 連接配接使用三次握手的首要原因:

The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.

簡單來說,三次握手的首要原因是為了防止舊的重複連接配接初始化造成混亂。RFC 給出的三次握手防止曆史連接配接的案例圖如下:

位元組一面:TCP 三次握手,問的好細!

RFC 793

如果是兩次握手連接配接,就無法阻止曆史連接配接,那為什麼 TCP 兩次握手為什麼無法阻止曆史連接配接呢?

我先直接說結論,主要是因為在兩次握手的情況下,「被動發起方」沒有中間狀态給「主動發起方」來阻止曆史連接配接,導緻「被動發起方」可能建立一個曆史連接配接,造成資源浪費。

你想想,兩次握手的情況下,「被動發起方」在收到 SYN 封包後,就進入 ESTABLISHED 狀态,意味着這時可以給對方發送資料給,但是「主動發」起方此時還沒有進入 ESTABLISHED 狀态,假設這次是曆史連接配接,主動發起方判斷到此次連接配接為曆史連接配接,那麼就會回 RST 封包來斷開連接配接,而「被動發起方」在第一次握手的時候就進入 ESTABLISHED 狀态,是以它可以發送資料的,但是它并不知道這個是曆史連接配接,它隻有在收到 RST 封包後,才會斷開連接配接。

位元組一面:TCP 三次握手,問的好細!

兩次握手無法阻止曆史連接配接

可以看到,上面這種場景下,「被動發起方」在向「主動發起方」發送資料前,并沒有阻止掉曆史連接配接,導緻「被動發起方」建立了一個曆史連接配接,又白白發送了資料,妥妥地浪費了「被動發起方」的資源。

是以,要解決這種現象,最好就是在「被動發起方」發送資料前,也就是建立連接配接之前,要阻止掉曆史連接配接,這樣就不會造成資源浪費,而要實作這個功能,就需要三次握手。

源碼分析

我說回 RST 就回 RST 嗎?當然不是了,肯定得用源碼證明我說的這個結論。

聽到要源碼分析,可能有的同學就慫了。

其實要分析我們今天這個問題,隻要懂 if else 就行了,我也會用中文來表述代碼的邏輯,是以單純看我的文字也是可以的。

這次我們重點分析的是,在 SYN_SENT 狀态下,收到不正确的确認号的 syn+ack 封包是如何處理的。

處于 SYN_SENT 狀态下的用戶端,在收到服務端的 syn+ack 封包後,最終會調用 tcp_rcv_state_process,在這裡會根據 TCP 狀态做對應的處理,這裡我們隻關注 SYN_SENT 狀态。

// net/ipv4/tcp_ipv4.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
 ...
  
 int queued = 0;
  
  ...
  
 switch (sk->sk_state) {
 case TCP_CLOSE:
  ...
 case TCP_LISTEN:
  ...
 case TCP_SYN_SENT:
    ....
  queued = tcp_rcv_synsent_state_process(sk, skb, th);
  if (queued >= 0)
   return queued;
    ...
 }      

可以看到,接下來,會繼續調用 tcp_rcv_synsent_state_process 函數。

static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
      const struct tcphdr *th)
{
 ....

 if (th->ack) {
  /* rfc793:
   * "If the state is SYN-SENT then
   *    first check the ACK bit
   *      If the ACK bit is set
   *   If SEG.ACK =< ISS, or SEG.ACK > SND.NXT, send
   *        a reset (unless the RST bit is set, if so drop
   *        the segment and return)"
   */
    // ack 的确認号不是預期的
  if (!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_una) ||
      after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt))
      //回 RST 封包
   goto reset_and_undo;

  ...
}      

從上面的函數,就可以得知了,用戶端在 SYN_SENT 狀态下,收到不正确的确認号的 syn+ack 封包會回 RST 封包。

小結

TCP 三次握手中,用戶端收到的第二次握手中 ack 确認号不是自己期望的,會發生什麼?是直接丢棄 or 回 RST 封包?

回 RST 封包。

什麼情況下會收到不正确的 ack(第二次握手中的 ack) 呢?

當用戶端發起多次 SYN 封包,然後網絡擁堵的情況下,「舊的 SYN 封包」比「新的 SYN 封包」早抵達服務端,此時服務端就會按照收到的「舊的 SYN 封包」回複 syn+ack 封包,而此封包的确認号并不是用戶端期望收到的,于是用戶端就會回 RST 封包。

繼續閱讀