【网络通信与信息安全】之深入解析TCP连接中如何确定客户端的端口号
生活随笔
收集整理的這篇文章主要介紹了
【网络通信与信息安全】之深入解析TCP连接中如何确定客户端的端口号
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
一、前言
- 在 TCP 連接中,客戶端在發起連接請求前會先確定一個客戶端的端口,然后用這個端口去和服務器端進行握手建立連接。那么在 Linux 上,客戶端的端口到底是如何被確定下來的呢?
- 事實上,我們平時很多遇到的問題都和這個端口選擇過程相關,如果能深度理解這個過程,將有助于我們對這些問題進行更深刻理解:
-
- Cannot assign requested address 報錯是怎么回事?
-
- 一個客戶端的端口可以同時用在兩條 TCP 連接上嗎?
- 借助一段簡單到只有兩句的代碼說起:
二、創建 socket
- 客戶端在發起連接的時候,需要事先創建一個 socket。在 c 語言中,就是調用 socket 函數,如下:
- socket 函數執行完畢后,在用戶層視角,可以看到返回一個文件描述符 fd,但在內核中其實是一套內核對象組合,大體結構如下:
- 從上圖可以看到,socket 在內核里并不是一個內核對象,而是包含 file、socket、sock 等多個相關內核對象構成,每個內核對象還定義了 ops 操作函數集合,這些內核對象都是在 socket 系統調用執行過程中創建出來的。為了避免喧賓奪主,這里只列出入口代碼,詳細過程就不展開介紹。
三、connect 發起連接
① connect 調用鏈展開
- 當在客戶端機上調用 connect 函數的時候,事實上會進入到內核的系統調用源碼中進行執行:
- 從上面的代碼可以看出:首先根據用戶傳入的 fd(文件描述符)來查詢對應的 socket 內核對象。了解了上文中的 socket 內核對象結構,據此可以知道接下來 sock->ops->connect 其實調用的是 inet_stream_connect 函數:
- 剛創建完畢的 socket 的狀態就是 SS_UNCONNECTED,所以在 __inet_stream_connect 中的 switch 判斷會進入到 case SS_UNCONNECTED 的處理邏輯中。
- 上述代碼中 sk 取的是 sock 對象,繼續回顧上文中的 socket 的內核數據結構圖,可以得知 sk->sk_prot->connect 實際上對應的是 tcp_v4_connect 方法。
- 現在來看 tcp_v4_connect 的代碼,它位于 net/ipv4/tcp_ipv4.c:
- 在 tcp_v4_connect 中終于看到了選擇端口的函數,那就是 inet_hash_connect。
② 選擇可用端口
- 找到 inet_hash_connect 的源碼,來看看到底端口是如何選擇出來的:
- 這里需要提一下在調用 __inet_hash_connect 時傳入的兩個重要參數:
-
- inet_sk_port_offset(sk):這個函數是根據要連接的目的 IP 和端口等信息生成一個隨機數;
-
- __inet_check_established:檢查是否和現有 ESTABLISH 的連接是否沖突的時候用的函數。
- 了解了這兩個參數后,繼續進入 __inet_hash_connect,這個函數比較長,為了方便理解,先看前面這一段:
- 在這個函數中首先判斷了 inet_sk(sk)->inet_num,如果調用過 bind,那么這個函數會選擇好端口并設置在 inet_num 上。這里假設沒有調用過 bind,所以 snum 為 0。
- 接著調用 inet_get_local_port_range,這個函數讀取的是 net.ipv4.ip_local_port_range 這個內核參數,來讀取管理員配置的可用的端口范圍:
- 接下來進入到了 for 循環中,其中 offset 是前面所說的,通過 inet_sk_port_offset(sk) 計算出的隨機數,那這段循環的作用就是從某個隨機數開始,把整個可用端口范圍來遍歷一遍,直到找到可用的端口后停止。
- 那么接著來看,如何來確定一個端口是否可以使用呢?
- 首先判斷的是 inet_is_reserved_local_port,這個很簡單,就是判斷要選擇的端口是否在 net.ipv4.ip_local_reserved_ports 中,在的話就不能用(如果因為某種原因不希望某些端口被內核使用,那么就把它們寫到 ip_local_reserved_ports 這個內核參數中就行)。
- 整個系統中會維護一個所有使用過的端口的哈希表,它就是 hinfo->bhash,接下來的代碼就會在這里進行查找。如果在哈希表中沒有找到,那么說明這個端口是可用的,至此端口就算是找到了。
- 遍歷完所有端口都沒找到合適的,就返回 -EADDRNOTAVAIL,在用戶程序上看到的就是 Cannot assign requested address 這個錯誤,怎么樣?是不是很眼熟,有沒有?我相信大家都見過它的,對吧?
- 以后當再遇到 Cannot assign requested address 錯誤,我們應該想到去查一下 net.ipv4.ip_local_port_range 中設置的可用端口的范圍是不是太小了。
③ 端口被使用過怎么辦?
- 回顧剛才的 __inet_hash_connect, 為了描述簡單,我們之前跳過了已經在 bhash 中存在時候的判斷,這是由于其一這個過程比較長,其二這段邏輯很有價值:
- port 已經在 bhash 中如果已經存在,就表示有其它的連接使用過該端口;如果 check_established 返回 0,該端口仍然可以接著使用。可能你會有困惑,一個端口怎么可以被用多次呢?
- 回憶下四元組的概念,兩對兒四元組中只要任意一個元素不同,都算是兩條不同的連接。以下的兩條 TCP 連接完全可以同時存在(假設 192.168.1.101 是客戶端,192.168.1.100 是服務端)
- check_established 作用就是檢測現有的 TCP 連接中是否四元組和要建立的連接四元素完全一致,如果不完全一致,那么該端口仍然可用。這個 check_established 是由調用方傳入的,實際上使用的是 __inet_check_established,我們來看它的源碼:
- 該函數首先找到 inet_ehash_bucket,這個和 bhash 類似,只不過是所有 ESTABLISH 狀態的 socket 組成的哈希表,然后遍歷這個哈希表,使用 INET_MATCH 來判斷是否可用。
- INET_MATCH 源碼如下:
- 在 INET_MATCH 中將 __saddr、__daddr、__ports 都進行了比較,當然除了 ip 和端口,INET_MATCH還比較了其它一些東西,所以 TCP 連接還有五元組、七元組之類的說法。為了統一,這里還是沿用四元組的說法:
-
- 如果 MATCH,就是說就四元組完全一致的連接,所以這個端口不可用,也返回 -EADDRNOTAVAIL;
-
- 如果不 MATCH,哪怕四元組中有一個元素不一樣,例如服務器的端口號不一樣,那么就 return 0,表示該端口仍然可用于建立新連接。
- 所以一臺客戶端機最大能建立的連接數并不是 65535,只要 server 足夠多,單機發出百萬條連接沒有任何問題。
④ 發起 syn 請求
- 再回到 tcp_v4_connect,這時 inet_hash_connect 已經返回了一個可用端口,接下來就進入到 tcp_connect,如下源碼所示:
- 到這里,其實就和本文要討論的主題沒有太大的關系,所以只是簡單看一下:
- tcp_connect 主要處理了以下邏輯:
-
- 申請一個 skb,并將其設置為 SYN 包;
-
- 添加到發送隊列上;
-
- 調用 tcp_transmit_skb 將該包發出;
-
- 啟動一個重傳定時器,超時會重發。
四、bind 時端口如何選擇
- 在上文中,我們看到 connect 選擇端口之前先判斷了 inet_sk(sk)->inet_num 有沒有值,如果有的話就直接用這個,而會跳過端口選擇過程。那么這個值是從哪兒來的呢?其實,它就是在對 socket 使用 bind 時設置的。
- 不只是服務器端,哪怕是對于客戶端,也可以對 socket 使用 bind 來綁定 IP 或者端口,如果使用了 bind,那么在 bind 的時候就會確定好端口,并設置到 inet_num 變量中(一般非常不推薦在客戶端角色下使用 bind,因為這會打亂 connect 里的端口選擇過程)。
- bind 的時候,如果傳了端口,那么 bind 就會嘗試使用該端口,如果端口號傳的是 0 ,那么 bind 有一套獨立的選擇端口號的邏輯:
- 根據上文中的 socket 內核對象,能找到 sk->sk_prot->get_port 實際調用的是 inet_csk_get_port,該函數來嘗試確定端口號,如果嘗試失敗,返回 EADDRINUSE,應用程序將會顯示一條錯誤信息 “Address already in use”。
- 簡單看一下,如果用戶沒有傳入端口(傳入的為 0),bind 是怎么選擇端口的:
- 這段邏輯和 connect 很像,通過 net_random 來從 net.ipv4.ip_local_port_range 指定的端口范圍內一個隨機位置開始遍歷,也會跳開 ip_local_reserved_ports 保留端口配置,通過 inet_csk(sk)->icsk_af_ops->bind_conflict 進行沖突檢測。
- inet_csk_bind_conflict 這個函數整體比較復雜,不過只需要了解一點就好,該函數和 connect 中端口選擇邏輯不同的是,并不會到 ESTABLISH 的哈希表進行可用檢測,只在 bind 狀態的 socket 里查。所以默認情況下,只要端口用過一次就不會再次使用。
五、結論
- 客戶端建立連接前需要確定一個端口,該端口會在兩個位置進行確定。
- 第一個位置,也是最主要的確定時機是 connect 系統調用執行過程。
-
- 在 connect 的時候,會隨機地從 ip_local_port_range 選擇一個位置開始循環判斷;找到可用端口后,發出 syn 握手包,如果端口查找失敗,會報錯 “Cannot assign requested address”,這時,應該首先想到去檢查一下服務器上的 net.ipv4.ip_local_port_range 參數,是不是可以再放的多一些。
-
- 如果因為某種原因不希望某些端口被使用到,那么就把它們寫到 ip_local_reserved_ports 這個內核參數中就行了,內核在選擇的時候會跳過這些端口。
-
- 另外注意即使是一個端口是可以被用于多條 TCP 連接的,所以一臺客戶端機最大能建立的連接數并不是 65535,只要 server 足夠多,單機發出百萬條連接沒有任何問題。
-
- 如下所示,在客戶機上實驗時的實際截圖,來實際看一下一個端口號確實是被用在了多條連接上:
-
- 截圖中左邊的 192 是客戶端,右邊的 119 是服務器的 ip,可以看到客戶端的 10000 這個端口號是用在了多條連接上了的。
- 第二個位置,如果在 connect 之前使用了 bind,將會使得 connect 時的端口選擇方式無效,轉而使用 bind 時確定的端口。
-
- bind 時如果傳入了端口號,會嘗試首先使用該端口號,如果傳入了 0 ,也會自動選擇一個。但默認情況下一個端口只會被使用一次,所以對于客戶端角色的 socket,不建議使用 bind。
-
- 上面的選擇端口的都是從 ip_local_port_range 范圍中的某一個隨機位置開始循環的,如果可用端口很充足,則能快一點找到可用端口,那循環很快就能退出。
-
- 假設實際中 ip_local_port_range 中的端口快被用光,這時候內核就大概率得把循環多執行很多輪才能找到,這會導致 connect 系統調用的 CPU 開銷的上漲。
-
- 所以,最好不要等到端口不夠用了才加大 ip_local_port_range 的范圍,而是事先就應該保持一個充足的范圍。
總結
以上是生活随笔為你收集整理的【网络通信与信息安全】之深入解析TCP连接中如何确定客户端的端口号的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iOS经典面试题之深入分析block相关
- 下一篇: Swift之五个让Swift代码更加优雅