一起看看 Linux的TCP 三次握手

大雄45發表於2022-07-24
導讀 在後端相關崗位的入職面試中,三次握手的出場頻率非常的高,甚至說它是必考題也不為過。一般的答案都是說客戶端如何發起 SYN 握手進入 SYN_SENT 狀態,伺服器響應 SYN 並回復 SYNACK,然後進入 SYN_RECV 等諸如此類。但今天我想給出一份不一樣的答案。

其實三次握手在核心的實現中,並不只是簡單的狀態的流轉,還包括埠選擇,半連線佇列、syncookie、全連線佇列、重傳計時器等關鍵操作。如果能深刻理解這些,你對線上把握和理解將更進一步。如果有面試官問起你三次握手,相信這份答案一定能幫你在面試官面前贏得非常多的加分。

在基於 TCP 的服務開發中,三次握手的主要流程圖如下:

一起看看 Linux的TCP 三次握手一起看看 Linux的TCP 三次握手

伺服器中的核心程式碼是建立 socket,繫結埠,listen 監聽,最後 accept 接收客戶端的請求。
客戶端的相關程式碼是建立 socket,然後呼叫 connect 連線 server。
看起來簡單的幾個系統呼叫,實際上卻包含了非常複雜的核心底層操作。根據核心工作原理,我深度展開一下三次握手過程中的內部操作。

友情提示:本文中核心原始碼會比較多。如果你能理解的了更好,如果覺得理解起來有困難,那直接重點看本文中的描述性的文字,尤其是加粗部分的即可。另外文章最後有一張總結圖歸納和整理了全文內容。

一、伺服器的 listen

我們都知道,伺服器在開始提供服務之前都需要先 listen 一下。但 listen 內部究竟幹了啥,我們平時很少去琢磨。

今天就讓我們詳細來看看,直接上一段 listen 時執行到的核心程式碼。

在這段程式碼裡,核心計算了半連線佇列的長度。然後據此算出半連線佇列所需要的實際記憶體大小,開始申請用於管理半連線佇列物件的記憶體(半連線佇列需要快速查詢,所以核心是用雜湊表來管理半連線佇列的,具體在 listen_sock 下的 syn_table 下)。最後將半連線佇列掛到了接收佇列 queue 上。

另外 queue->rskq_accept_head 代表的是全連線佇列,它是一個連結串列的形式。在 listen 這裡因為還沒有連線,所以將全連線佇列頭 queue->rskq_accept_head 設定成 NULL。

當全連線佇列和半連線佇列中有元素的時候,他們在核心中的結構圖大致如下。

一起看看 Linux的TCP 三次握手一起看看 Linux的TCP 三次握手

在伺服器 listen 的時候,主要是進行了全/半連線佇列的長度限制計算,以及相關的記憶體申請和初始化。全/連線佇列初始化了以後才可以相應來自客戶端的握手請求。

二、客戶端 connect

客戶端透過呼叫 connect 來發起連線。在 connect 系統呼叫中會進入到核心原始碼的 tcp_v4_connect。

在這裡將完成把 socket 狀態設定為 TCP_SYN_SENT。再透過 inet_hash_connect 來動態地選擇一個可用的埠後(埠選擇詳細過程參考前文《TCP 連線中客戶端的埠號是如何確定的?》),進入到 tcp_connect 中。

在 tcp_connect 申請和構造 SYN 包,然後將其發出。同時還啟動了一個重傳定時器,該定時器的作用是等到一定時間後收不到伺服器的反饋的時候來開啟重傳。在 3.10 版本中首次超時時間是 1 s,一些老版本中是 3 s。

總結一下,客戶端在 connect 的時候,把本地 socket 狀態設定成了 TCP_SYN_SENT,選了一個可用的埠,接著發出 SYN 握手請求並啟動重傳定時器。

三、伺服器響應 SYN

在伺服器端,所有的 TCP 包(包括客戶端發來的 SYN 握手請求)都經過網路卡、軟中斷,進入到 tcp_v4_rcv。在該函式中根據網路包(skb)TCP 頭資訊中的目的 IP 資訊查到當前在 listen 的 socket。然後繼續進入 tcp_v4_do_rcv 處理握手過程。

在 tcp_v4_do_rcv 中判斷當前 socket 是 listen 狀態後,首先會到 tcp_v4_hnd_req 去檢視半連線佇列。伺服器第一次響應 SYN 的時候,半連線佇列裡必然是空空如也,所以相當於什麼也沒幹就返回了。

在 tcp_rcv_state_process 里根據不同的 socket 狀態進行不同的處理。

其中 conn_request 是一個函式指標,指向 tcp_v4_conn_request。 伺服器響應 SYN 的主要處理邏輯都在這個 tcp_v4_conn_request 裡。

在這裡首先判斷半連線佇列是否滿了,如果滿了的話進入 tcp_syn_flood_action 去判斷是否開啟了 tcp_syncookies 核心引數。 如果佇列滿,且未開啟 tcp_syncookies,那麼該握手包將直接被丟棄!!

接著還要判斷全連線佇列是否滿。因為全連線佇列滿也會導致握手異常的,那乾脆就在第一次握手的時候也判斷了。 如果全連線佇列滿了,且有 young_ack 的話,那麼同樣也是直接丟棄。

young_ack 是半連線佇列裡保持著的一個計數器。記錄的是剛有 SYN 到達,沒有被 SYN_ACK 重傳定時器重傳過 SYN_ACK,同時也沒有完成過三次握手的 sock 數量

接下來是構造 synack 包,然後透過 ip_build_and_send_pkt 把它傳送出去。

最後把當前握手資訊新增到半連線佇列,並開啟計時器。計時器的作用是如果某個時間之內還收不到客戶端的第三次握手的話,伺服器會重傳 synack 包。

總結一下,伺服器響應 ack 是主要工作是判斷下接收佇列是否滿了,滿的話可能會丟棄該請求,否則發出 synack。申請 request_sock 新增到半連線佇列中,同時啟動定時器。

四、客戶端響應 SYNACK

客戶端收到伺服器端發來的 synack 包的時候,也會進入到 tcp_rcv_state_process 函式中來。不過由於自身 socket 的狀態是 TCP_SYN_SENT,所以會進入到另一個不同的分支中去。

tcp_rcv_synsent_state_process 是客戶端響應 synack 的主要邏輯。

tcp_ack()->tcp_clean_rtx_queue()

客戶端修改自己的 socket 狀態為 ESTABLISHED,接著開啟 TCP 的保活計時器。

在 tcp_send_ack 中構造 ack 包,並把它傳送了出去。

客戶端響應來自伺服器端的 synack 時清除了 connect 時設定的重傳定時器,把當前 socket 狀態設定為 ESTABLISHED,開啟保活計時器後發出第三次握手的 ack 確認。

五、伺服器響應 ACK

伺服器響應第三次握手的 ack 時同樣會進入到 tcp_v4_do_rcv

不過由於這已經是第三次握手了,半連線佇列裡會存在上次第一次握手時留下的半連線資訊。所以 tcp_v4_hnd_req 的執行邏輯會不太一樣。

inet_csk_search_req 負責在半連線佇列裡進行查詢,找到以後返回一個半連線 request_sock 物件。然後進入到 tcp_check_req 中。

5.1 建立子 socket

icsk_af_ops->syn_recv_sock 對應的是 tcp_v4_syn_recv_sock 函式。

注意,在第三次握手的這裡又繼續判斷一次全連線佇列是否滿了,如果滿了修改一下計數器就丟棄了。如果佇列不滿,那麼就申請建立新的 sock 物件。

5.2 刪除半連線佇列

把連線請求塊從半連線佇列中刪除。

reqsk_queue_unlink 中把連線請求塊從半連線佇列中刪除。

5.3 新增全連線佇列

接著新增到全連線佇列裡邊來。

在 reqsk_queue_add 中將握手成功的 request_sock 物件插入到全連線佇列連結串列的尾部。

5.4 設定連線為 ESTABLISHED

tcp_v4_do_rcv => tcp_child_process => tcp_rcv_state_process

將連線設定為 TCP_ESTABLISHED 狀態。

伺服器響應第三次握手 ack 所做的工作是把當前半連線物件刪除,建立了新的 sock 後加入到全連線佇列中,最後將新連線狀態設定為 ESTABLISHED。

六、伺服器 accept

最後 accept 一步我們們長話短說。

reqsk_queue_remove 這個操作很簡單,就是從全連線佇列的連結串列裡獲取出第一個元素返回就行了。

所以,accept 的重點工作就是從已經建立好的全連線佇列中取出一個返回給使用者程式。

全文總結

在後端相關崗位的入職面試中,三次握手的出場頻率非常的高。其實在三次握手的過程中,不僅僅是一個握手包的傳送 和 TCP 狀態的流轉。還包含了埠選擇,連線佇列建立與處理等很多關鍵技術點。透過今天一篇文章,我們深度去了解了三次握手過程中核心中的這些內部操作。

全文洋洋灑灑上萬字字,其實可以用一幅圖總結起來。

一起看看 Linux的TCP 三次握手一起看看 Linux的TCP 三次握手

伺服器 listen 時,計算了全/半連線佇列的長度,還申請了相關記憶體並初始化。

客戶端 connect 時,把本地 socket 狀態設定成了 TCP_SYN_SENT,選則一個可用的埠,發出 SYN 握手請求並啟動重傳定時器。

伺服器響應 ack 時,會判斷下接收佇列是否滿了,滿的話可能會丟棄該請求。否則發出 synack,申請 request_sock 新增到半連線佇列中,同時啟動定時器。

客戶端響應 synack 時,清除了 connect 時設定的重傳定時器,把當前 socket 狀態設定為 ESTABLISHED,開啟保活計時器後發出第三次握手的 ack 確認。

伺服器響應 ack 時,把對應半連線物件刪除,建立了新的 sock 後加入到全連線佇列中,最後將新連線狀態設定為 ESTABLISHED。

accept 從已經建立好的全連線佇列中取出一個返回給使用者程式。

另外要注意的是,如果握手過程中發生丟包(網路問題,或者是連線佇列溢位),核心會等待定時器到期後重試,重試時間間隔在 3.10 版本里分別是 1s 2s 4s ...。在一些老版本里,比如 2.6 裡,第一次重試時間是 3 秒。最大重試次數分別由 tcp_syn_retries 和 tcp_synack_retries 控制。

如果你的線上介面正常都是幾十毫秒內返回,但偶爾出現了 1 s、或者 3 s 等這種偶發的響應耗時變長的問題,那麼你就要去定位一下看看是不是出現了握手包的超時重傳了。

以上就是三次握手中一些更詳細的內部操作。深度理解這個握手過程對於你排查線上問題會有極大的幫助的。

原文來自:



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2907323/,如需轉載,請註明出處,否則將追究法律責任。

相關文章