位元組一面:TCP和UDP可以使用同一個埠嗎?

導讀:TCP 和 UDP 可以同時監聽相同的埠嗎?
作者 / 來源:小林coding(ID:CodingLin)
關於埠的知識點,還是挺多可以講的,比如還可以牽扯到這幾個問題:
  • 多個 TCP 服務程序可以同時繫結同一個埠嗎?
  • 客戶端的埠可以重複使用嗎?
  • 客戶端 TCP 連線 TIME_WAIT 狀態過多,會導致埠資源耗盡而無法建立新的連線嗎?
所以,這次就跟大家盤一盤這些問題。
01 TCP 和 UDP 可以同時繫結相同的埠嗎?
其實我感覺這個問題「TCP 和 UDP 可以同時監聽相同的埠嗎?」表述有問題,這個問題應該表述成「TCP 和 UDP 可以同時繫結相同的埠嗎?」
因為「監聽」這個動作是在 TCP 服務端網路程式設計中才具有的,而 UDP 服務端網路程式設計中是沒有「監聽」這個動作的。
TCP 和 UDP 服務端網路相似的一個地方,就是會呼叫 bind 繫結埠。
給大家貼一下  TCP 和 UDP 網路程式設計的區別就知道了。
TCP 網路程式設計如下,服務端執行 listen() 系統呼叫就是監聽埠的動作。
▲TCP 網路程式設計
UDP 網路程式設計如下,服務端是沒有監聽這個動作的,只有執行  bind()  系統呼叫來繫結埠的動作。
▲UDP 網路程式設計
  • TCP 和 UDP 可以同時繫結相同的埠嗎?
答案:可以的。
在資料鏈路層中,透過 MAC 地址來尋找區域網中的主機。在網際層中,透過 IP 地址來尋找網路中互連的主機或路由器。在傳輸層中,需要透過埠進行定址,來識別同一計算機中同時通訊的不同應用程式。
所以,傳輸層的「埠號」的作用,是為了區分同一個主機上不同應用程式的資料包。
傳輸層有兩個傳輸協議分別是 TCP 和 UDP,在核心中是兩個完全獨立的軟體模組。
當主機收到資料包後,可以在 IP 包頭的「協議號」欄位知道該資料包是 TCP/UDP,所以可以根據這個資訊確定送給哪個模組(TCP/UDP)處理,送給 TCP/UDP 模組的報文根據「埠號」確定送給哪個應用程式處理。
因此, TCP/UDP 各自的埠號也相互獨立,如 TCP 有一個 80 號埠,UDP 也可以有一個 80 號埠,二者並不衝突。
  • 驗證結果
我簡單寫了 TCP 和 UDP 服務端的程式,它們都繫結同一個埠號 8888。
執行這兩個程式後,透過 netstat 命令可以看到,TCP 和 UDP 是可以同時繫結同一個埠號的。
02 多個 TCP 服務程序可以繫結同一個埠嗎?
還是以前面的 TCP 服務端程式作為例子,啟動兩個同時繫結同一個埠的 TCP 服務程序。
執行第一個  TCP 服務程序之後,netstat 命令可以檢視,8888 埠已經被一個 TCP 服務程序繫結並監聽了,如下圖:
接著,執行第二個 TCP 服務程序的時候,就報錯了“Address already in use”,如下圖:
我上面的測試案例是兩個 TCP 服務程序同時繫結地址和埠是:0.0.0.0 地址和8888埠,所以才出現的錯誤。
如果兩個 TCP 服務程序繫結的 IP 地址不同,而埠相同的話,也是可以繫結成功的,如下圖:
所以,預設情況下,針對「多個 TCP 服務程序可以繫結同一個埠嗎?」這個問題的答案是:如果兩個 TCP 服務程序同時繫結的 IP 地址和埠都相同,那麼執行 bind() 時候就會出錯,錯誤是“Address already in use”。
注意,如果 TCP 服務程序 A 繫結的地址是  0.0.0.0 和埠 8888,而如果 TCP 服務程序 B 繫結的地址是 192.168.1.100 地址(或者其他地址)和埠 8888,那麼執行 bind() 時候也會出錯。
這是因為 0.0.0.0  地址比較特殊,代表任意地址,意味著綁定了 0.0.0.0  地址,相當於把主機上的所有 IP 地址都綁定了。
  • 重啟 TCP 服務程序時,為什麼會有“Address in use”的報錯資訊?
TCP 服務程序需要繫結一個 IP 地址和一個埠,然後就監聽在這個地址和埠上,等待客戶端連線的到來。
然後在實踐中,我們可能會經常碰到一個問題,當 TCP 服務程序重啟之後,總是碰到“Address in use”的報錯資訊,TCP 服務程序不能很快地重啟,而是要過一會才能重啟成功。
這是為什麼呢?
當我們重啟 TCP 服務程序的時候,意味著透過伺服器端發起了關閉連線操作,於是就會經過四次揮手,而對於主動關閉方,會在 TIME_WAIT 這個狀態裡停留一段時間,這個時間大約為 2MSL。
當 TCP 服務程序重啟時,服務端會出現 TIME_WAIT 狀態的連線,TIME_WAIT 狀態的連線使用的 IP+PORT 仍然被認為是一個有效的 IP+PORT 組合,相同機器上不能夠在該 IP+PORT 組合上進行繫結,那麼執行 bind() 函式的時候,就會返回了 Address already in use 的錯誤。
而等 TIME_WAIT 狀態的連線結束後,重啟 TCP 服務程序就能成功。
  • 重啟 TCP 服務程序時,如何避免“Address in use”的報錯資訊?

我們可以在呼叫 bind 前,對 socket 設定 SO_REUSEADDR 屬性,可以解決這個問題。

inton

 = 

1

;

setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &

on

sizeof

(

on

));

因為  SO_REUSEADDR 作用是:如果當前啟動程序繫結的 IP+PORT 與處於TIME_WAIT 狀態的連線佔用的 IP+PORT 存在衝突,但是新啟動的程序使用了 SO_REUSEADDR 選項,那麼該程序就可以繫結成功。
舉個例子,服務端有個監聽 0.0.0.0 地址和 8888 埠的 TCP 服務程序。
有個客戶端(IP地址:192.168.1.100)已經和服務端(IP 地址:172.19.11.200)建立了 TCP 連線,那麼在 TCP 服務程序重啟時,服務端會與客戶端經歷四次揮手,服務端的 TCP 連線會短暫處於 TIME_WAIT 狀態:

客戶端地址:埠           服務端地址:埠        

TCP

 連線狀態

192

.168.1.100:37272

     172

.19.11.200:8888TIME_WAIT

如果 TCP 服務程序沒有對 socket 設定 SO_REUSEADDR 屬性,那麼在重啟時,由於存在一個和繫結 IP+PORT 一樣的 TIME_WAIT 狀態的連線,那麼在執行 bind() 函式的時候,就會返回了 Address already in use 的錯誤。

如果 TCP 服務程序對 socket 設定 SO_REUSEADDR 屬性了,那麼在重啟時,即使存在一個和繫結 IP+PORT 一樣的 TIME_WAIT 狀態的連線,依然可以正常繫結成功,因此可以正常重啟成功。
因此,在所有 TCP 伺服器程式中,呼叫 bind 之前最好對 socket 設定 SO_REUSEADDR 屬性,這不會產生危害,相反,它會幫助我們在很快時間內重啟服務端程式。
前面我提到過這個問題:如果 TCP 服務程序 A 繫結的地址是  0.0.0.0 和埠 8888,而如果 TCP 服務程序 B 繫結的地址是 192.168.1.100 地址(或者其他地址)和埠 8888,那麼執行 bind() 時候也會出錯。
這個問題也可以由 SO_REUSEADDR 解決,因為它的另外一個作用是:繫結的 IP地址 + 埠時,只要 IP 地址不是正好(exactly)相同,那麼允許繫結。
比如,0.0.0.0:8888 和192.168.1.100:8888,雖然邏輯意義上前者包含了後者,但是 0.0.0.0 泛指所有本地 IP,而 192.168.1.100 特指某一IP,兩者並不是完全相同,所以在對 socket 設定 SO_REUSEADDR 屬性後,那麼執行 bind() 時候就會繫結成功。
03 客戶端的埠可以重複使用嗎?
客戶端在執行 connect 函式的時候,會在核心裡隨機選擇一個埠,然後向服務端發起 SYN 報文,然後與服務端進行三次握手。
所以,客戶端的埠選擇的發生在 connect 函式,核心在選擇埠的時候,會從 net.ipv4.ip_local_port_range 這個核心引數指定的範圍來選取一個埠作為客戶端埠。
該引數的預設值是 32768 61000,意味著埠總可用的數量是 61000 – 32768 = 28232 個。
當客戶端與服務端完成 TCP 連線建立後,我們可以透過 netstat 命令檢視 TCP 連線。

$ netstat -napt

協議  源ip地址:埠            目的ip地址:埠         狀態

tcp  

192.168.110.182.64992117.147.199.51.443

     ESTABLISHED

  • 那問題來了,上面客戶端已經用了 64992 埠,那麼還可以繼續使用該埠發起連線嗎?
這個問題,很多同學都會說不可以繼續使用該埠了,如果按這個理解的話, 預設情況下客戶端可以選擇的埠是 28232 個,那麼意味著客戶端只能最多建立  28232 個 TCP 連線,如果真是這樣的話,那麼這個客戶端併發連線也太少了吧,所以這是錯誤理解。
正確的理解是,TCP 連線是由四元組(源IP地址,源埠,目的IP地址,目的埠)唯一確認的,那麼只要四元組中其中一個元素髮生了變化,那麼就表示不同的 TCP 連線的。所以如果客戶端已使用埠 64992 與服務端 A 建立了連線,那麼客戶端要與服務端 B 建立連線,還是可以使用埠 64992 的,因為核心是透過四元祖資訊來定位一個 TCP 連線的,並不會因為客戶端的埠號相同,而導致連線衝突的問題。
比如下面這張圖,有 2 個 TCP 連線,左邊是客戶端,右邊是服務端,客戶端使用了相同的埠 50004 與兩個服務端建立了 TCP 連線。
仔細看,上面這兩條 TCP 連線的四元組資訊中的「目的 IP 地址」是不同的,一個是 180.101.49.12 ,另外一個是 180.101.49.11。
  • 多個客戶端可以 bind 同一個埠嗎?
bind 函式雖然常用於服務端網路程式設計中,但是它也是用於客戶端的。
前面我們知道,客戶端是在呼叫 connect 函式的時候,由核心隨機選取一個埠作為連線的埠。
而如果我們想自己指定連線的埠,就可以用 bind 函式來實現:客戶端先透過 bind 函式繫結一個埠,然後呼叫 connect 函式就會跳過埠選擇的過程了,轉而使用 bind 時確定的埠。
針對這個問題:多個客戶端可以 bind 同一個埠嗎?
要看多個客戶端繫結的 IP + PORT 是否都相同,如果都是相同的,那麼在執行 bind() 時候就會出錯,錯誤是“Address already in use”。
如果一個繫結在 192.168.1.100:6666,一個繫結在 192.168.1.200:6666,因為 IP 不相同,所以執行 bind() 的時候,能正常繫結。
所以, 如果多個客戶端同時繫結的 IP 地址和埠都是相同的,那麼執行 bind() 時候就會出錯,錯誤是“Address already in use”。
一般而言,客戶端不建議使用 bind 函式,應該交由 connect 函式來選擇埠會比較好,因為客戶端的埠通常都沒什麼意義。
  • 客戶端 TCP 連線 TIME_WAIT 狀態過多,會導致埠資源耗盡而無法建立新的連線嗎?
針對這個問題要看,客戶端是否都是與同一個伺服器(目標地址和目標埠一樣)建立連線。
如果客戶端都是與同一個伺服器(目標地址和目標埠一樣)建立連線,那麼如果客戶端 TIME_WAIT 狀態的連線過多,當埠資源被耗盡,就無法與這個伺服器再建立連線了。
但是,因為只要客戶端連線的伺服器不同,埠資源可以重複使用的。
所以,如果客戶端都是與不同的伺服器建立連線,即使客戶端埠資源只有幾萬個, 客戶端發起百萬級連線也是沒問題的(當然這個過程還會受限於其他資源,比如檔案描述符、記憶體、CPU 等)。
  • 如何解決客戶端 TCP 連線 TIME_WAIT 過多,導致無法與同一個伺服器建立連線的問題?
前面我們提到,如果客戶端都是與同一個伺服器(目標地址和目標埠一樣)建立連線,那麼如果客戶端 TIME_WAIT 狀態的連線過多,當埠資源被耗盡,就無法與這個伺服器再建立連線了。
針對這個問題,也是有解決辦法的,那就是開啟 net.ipv4.tcp_tw_reuse  這個核心引數。
因為開啟了這個核心引數後,客戶端呼叫 connect  函式時,如果選擇到的埠,已經被相同四元組的連線佔用的時候,就會判斷該連線是否處於  TIME_WAIT 狀態,如果該連線處於 TIME_WAIT 狀態並且 TIME_WAIT 狀態持續的時間超過了 1 秒,那麼就會重用這個連線,然後就可以正常使用該埠了。
舉個例子,假設客戶端已經與伺服器建立了一個 TCP 連線,並且這個狀態處於  TIME_WAIT 狀態:

客戶端地址:埠           服務端地址:埠         

TCP

 連線狀態

192

.168.1.100:2222

      172

.19.11.21:8888TIME_WAIT

然後客戶端又與該伺服器(172.19.11.21:8888)發起了連線,在呼叫 connect 函式時,核心剛好選擇了 2222 埠,接著發現已經被相同四元組的連線佔用了:
  • 如果沒有開啟  net.ipv4.tcp_tw_reuse  核心引數,那麼核心就會選擇下一個埠,然後繼續判斷,直到找到一個沒有被相同四元組的連線使用的埠, 如果埠資源耗盡還是沒找到,那麼 connect 函式就會返回錯誤。
  • 如果開啟了 net.ipv4.tcp_tw_reuse  核心引數,就會判斷該四元組的連線狀態是否處於 TIME_WAIT 狀態,如果連線處於 TIME_WAIT 狀態並且該狀態持續的時間超過了 1 秒,那麼就會重用該連線,於是就可以使用 2222 埠了,這時 connect 就會返回成功。
再次提醒一次,開啟了 net.ipv4.tcp_tw_reuse  核心引數,是客戶端(連線發起方) 在呼叫 connect() 函式時才起作用,所以在服務端開啟這個引數是沒有效果的。
  • 客戶端埠選擇的流程總結
至此,我們已經把客戶端在執行 connect 函式時,核心選擇埠的情況大致說了一遍,為了讓大家更明白客戶端埠的選擇過程,我畫了一流程圖。
總結
  • TCP 和 UDP 可以同時繫結相同的埠嗎?
可以的。
TCP 和 UDP 傳輸協議,在核心中是由兩個完全獨立的軟體模組實現的。
當主機收到資料包後,可以在 IP 包頭的「協議號」欄位知道該資料包是 TCP/UDP,所以可以根據這個資訊確定送給哪個模組(TCP/UDP)處理,送給 TCP/UDP 模組的報文根據「埠號」確定送給哪個應用程式處理。
因此, TCP/UDP 各自的埠號也相互獨立,互不影響。
  • 多個 TCP 服務程序可以同時繫結同一個埠嗎?
如果兩個 TCP 服務程序同時繫結的 IP 地址和埠都相同,那麼執行 bind() 時候就會出錯,錯誤是“Address already in use”。

如果兩個 TCP 服務程序繫結的埠都相同,而 IP 地址不同,那麼執行 bind() 不會出錯。

  • 如何解決服務端重啟時,報錯“Address already in use”的問題?
當我們重啟 TCP 服務程序的時候,意味著透過伺服器端發起了關閉連線操作,於是就會經過四次揮手,而對於主動關閉方,會在 TIME_WAIT 這個狀態裡停留一段時間,這個時間大約為 2MSL。
當 TCP 服務程序重啟時,服務端會出現 TIME_WAIT 狀態的連線,TIME_WAIT 狀態的連線使用的 IP+PORT 仍然被認為是一個有效的 IP+PORT 組合,相同機器上不能夠在該 IP+PORT 組合上進行繫結,那麼執行 bind() 函式的時候,就會返回了 Address already in use 的錯誤。
要解決這個問題,我們可以對 socket 設定 SO_REUSEADDR 屬性。
這樣即使存在一個和繫結 IP+PORT 一樣的 TIME_WAIT 狀態的連線,依然可以正常繫結成功,因此可以正常重啟成功。
  • 客戶端的埠可以重複使用嗎?
在客戶端執行 connect 函式的時候,只要客戶端連線的伺服器不是同一個,核心允許埠重複使用。
TCP 連線是由四元組(源IP地址,源埠,目的IP地址,目的埠)唯一確認的,那麼只要四元組中其中一個元素髮生了變化,那麼就表示不同的 TCP 連線的。
所以,如果客戶端已使用埠 64992 與服務端 A 建立了連線,那麼客戶端要與服務端 B 建立連線,還是可以使用埠 64992 的,因為核心是透過四元祖資訊來定位一個 TCP 連線的,並不會因為客戶端的埠號相同,而導致連線衝突的問題。
  • 客戶端 TCP 連線 TIME_WAIT 狀態過多,會導致埠資源耗盡而無法建立新的連線嗎?
要看客戶端是否都是與同一個伺服器(目標地址和目標埠一樣)建立連線。
如果客戶端都是與同一個伺服器(目標地址和目標埠一樣)建立連線,那麼如果客戶端 TIME_WAIT 狀態的連線過多,當埠資源被耗盡,就無法與這個伺服器再建立連線了。即使在這種狀態下,還是可以與其他伺服器建立連線的,只要客戶端連線的伺服器不是同一個,那麼埠是重複使用的。
  • 如何解決客戶端 TCP 連線 TIME_WAIT 過多,導致無法與同一個伺服器建立連線的問題?
開啟 net.ipv4.tcp_tw_reuse  這個核心引數。
因為開啟了這個核心引數後,客戶端呼叫 connect  函數時,如果選擇到的埠,已經被相同四元組的連線佔用的時候,就會判斷該連線是否處於  TIME_WAIT 狀態。
如果該連線處於 TIME_WAIT 狀態並且 TIME_WAIT 狀態持續的時間超過了 1 秒,那麼就會重用這個連線,然後就可以正常使用該埠了。
完,搞定!

延伸閱讀👇
延伸閱讀《TCP IP詳解
乾貨直達👇
據統計,99%的大咖都關注了這個公眾號
👇

相關文章