導讀: TCP 和 UDP 可以同時監聽相同的埠嗎?
作者 / 來源:小林coding(ID:CodingLin)
關於埠的知識點,還是挺多可以講的,比如還可以牽扯到這幾個問題:
客戶端 TCP 連線 TIME_WAIT 狀態過多,會導致埠資源耗盡而無法建立新的連線嗎?
01 TCP 和 UDP 可以同時繫結相同的埠嗎?
其實我感覺這個問題「TCP 和 UDP 可以同時監聽相同的埠嗎?」表述有問題,這個問題應該表述成「TCP 和 UDP 可以同時繫結相同的埠嗎?」
因為「監聽」這個動作是在 TCP 服務端網路程式設計中才具有的,而 UDP 服務端網路程式設計中是沒有「監聽」這個動作的。
TCP 和 UDP 服務端網路相似的一個地方,就是會呼叫 bind 繫結埠。
給大家貼一下 TCP 和 UDP 網路程式設計的區別就知道了。
TCP 網路程式設計如下,服務端執行 listen() 系統呼叫就是監聽埠的動作。
UDP 網路程式設計如下,服務端是沒有監聽這個動作的,只有執行 bind() 系統呼叫來繫結埠的動作。
在資料鏈路層中,透過 MAC 地址來尋找區域網中的主機。在網際層中,透過 IP 地址來尋找網路中互連的主機或路由器。在傳輸層中,需要透過埠進行定址,來識別同一計算機中同時通訊的不同應用程式。
所以,傳輸層的「埠號」的作用,是為了區分同一個主機上不同應用程式的資料包。
傳輸層有兩個傳輸協議分別是 TCP 和 UDP,在核心中是兩個完全獨立的軟體模組。
當主機收到資料包後,可以在 IP 包頭的「協議號」欄位知道該資料包是 TCP/UDP,所以可以根據這個資訊確定送給哪個模組(TCP/UDP)處理,送給 TCP/UDP 模組的報文根據「埠號」確定送給哪個應用程式處理。
因此, TCP/UDP 各自的埠號也相互獨立,如 TCP 有一個 80 號埠,UDP 也可以有一個 80 號埠,二者並不衝突。
我簡單寫了 TCP 和 UDP 服務端的程式,它們都繫結同一個埠號 8888。
執行這兩個程式後,透過 netstat 命令可以看到,TCP 和 UDP 是可以同時繫結同一個埠號的。
還是以前面的 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 屬性,可以解決這個問題。
int on
=
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 :8888 TIME_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() 時候就會繫結成功。
客戶端在執行 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 .64992 117.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 函式雖然常用於服務端網路程式設計中,但是它也是用於客戶端的。
前面我們知道,客戶端是在呼叫 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 :8888 TIME_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 傳輸協議,在核心中是由兩個完全獨立的軟體模組實現的。
當主機收到資料包後,可以在 IP 包頭的「協議號」欄位知道該資料包是 TCP/UDP,所以可以根據這個資訊確定送給哪個模組(TCP/UDP)處理,送給 TCP/UDP 模組的報文根據「埠號」確定送給哪個應用程式處理。
因此, TCP/UDP 各自的埠號也相互獨立,互不影響。
如果兩個 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 秒,那麼就會重用這個連線,然後就可以正常使用該埠了。
延伸閱讀
乾貨直達