字節(jié)一面:TCP 和 UDP 可以使用同一個(gè)端口嗎?
大家好,我是小林。
之前有讀者在字節(jié)面試的時(shí)候,被問到:TCP 和 UDP 可以同時(shí)監(jiān)聽相同的端口嗎?
關(guān)于端口的知識點(diǎn),還是挺多可以講的,比如還可以牽扯到這幾個(gè)問題:
- 多個(gè) TCP 服務(wù)進(jìn)程可以同時(shí)綁定同一個(gè)端口嗎?
- 客戶端的端口可以重復(fù)使用嗎?
- 客戶端 TCP 連接 TIME_WAIT 狀態(tài)過多,會導(dǎo)致端口資源耗盡而無法建立新的連接嗎?
所以,這次就跟大家盤一盤這些問題。
TCP 和 UDP 可以同時(shí)綁定相同的端口嗎?
其實(shí)我感覺這個(gè)問題「TCP 和 UDP 可以同時(shí)監(jiān)聽相同的端口嗎?」表述有問題,這個(gè)問題應(yīng)該表述成「TCP 和 UDP 可以同時(shí)綁定相同的端口嗎?」
因?yàn)椤副O(jiān)聽」這個(gè)動作是在 TCP 服務(wù)端網(wǎng)絡(luò)編程中才具有的,而 UDP 服務(wù)端網(wǎng)絡(luò)編程中是沒有「監(jiān)聽」這個(gè)動作的。
TCP 和 UDP 服務(wù)端網(wǎng)絡(luò)相似的一個(gè)地方,就是會調(diào)用 bind 綁定端口。
給大家貼一下 TCP 和 UDP 網(wǎng)絡(luò)編程的區(qū)別就知道了。
TCP 網(wǎng)絡(luò)編程如下,服務(wù)端執(zhí)行 listen() 系統(tǒng)調(diào)用就是監(jiān)聽端口的動作。
TCP 網(wǎng)絡(luò)編程
UDP 網(wǎng)絡(luò)編程如下,服務(wù)端是沒有監(jiān)聽這個(gè)動作的,只有執(zhí)行 bind() 系統(tǒng)調(diào)用來綁定端口的動作。
UDP 網(wǎng)絡(luò)編程
TCP 和 UDP 可以同時(shí)綁定相同的端口嗎?
答案:可以的。
在數(shù)據(jù)鏈路層中,通過 MAC 地址來尋找局域網(wǎng)中的主機(jī)。在網(wǎng)際層中,通過 IP 地址來尋找網(wǎng)絡(luò)中互連的主機(jī)或路由器。在傳輸層中,需要通過端口進(jìn)行尋址,來識別同一計(jì)算機(jī)中同時(shí)通信的不同應(yīng)用程序。
所以,傳輸層的「端口號」的作用,是為了區(qū)分同一個(gè)主機(jī)上不同應(yīng)用程序的數(shù)據(jù)包。
傳輸層有兩個(gè)傳輸協(xié)議分別是 TCP 和 UDP,在內(nèi)核中是兩個(gè)完全獨(dú)立的軟件模塊。
當(dāng)主機(jī)收到數(shù)據(jù)包后,可以在 IP 包頭的「協(xié)議號」字段知道該數(shù)據(jù)包是 TCP/UDP,所以可以根據(jù)這個(gè)信息確定送給哪個(gè)模塊(TCP/UDP)處理,送給 TCP/UDP 模塊的報(bào)文根據(jù)「端口號」確定送給哪個(gè)應(yīng)用程序處理。
因此, TCP/UDP 各自的端口號也相互獨(dú)立,如 TCP 有一個(gè) 80 號端口,UDP 也可以有一個(gè) 80 號端口,二者并不沖突。
驗(yàn)證結(jié)果
我簡單寫了 TCP 和 UDP 服務(wù)端的程序,它們都綁定同一個(gè)端口號 8888。
運(yùn)行這兩個(gè)程序后,通過 netstat 命令可以看到,TCP 和 UDP 是可以同時(shí)綁定同一個(gè)端口號的。
多個(gè) TCP 服務(wù)進(jìn)程可以綁定同一個(gè)端口嗎?
還是以前面的 TCP 服務(wù)端程序作為例子,啟動兩個(gè)同時(shí)綁定同一個(gè)端口的 TCP 服務(wù)進(jìn)程。
運(yùn)行第一個(gè) TCP 服務(wù)進(jìn)程之后,netstat 命令可以查看,8888 端口已經(jīng)被一個(gè) TCP 服務(wù)進(jìn)程綁定并監(jiān)聽了,如下圖:
接著,運(yùn)行第二個(gè) TCP 服務(wù)進(jìn)程的時(shí)候,就報(bào)錯(cuò)了“Address already in use”,如下圖:
我上面的測試案例是兩個(gè) TCP 服務(wù)進(jìn)程同時(shí)綁定地址和端口是:0.0.0.0 地址和8888端口,所以才出現(xiàn)的錯(cuò)誤。
如果兩個(gè) TCP 服務(wù)進(jìn)程綁定的 IP 地址不同,而端口相同的話,也是可以綁定成功的,如下圖:
所以,默認(rèn)情況下,針對「多個(gè) TCP 服務(wù)進(jìn)程可以綁定同一個(gè)端口嗎?」這個(gè)問題的答案是:如果兩個(gè) TCP 服務(wù)進(jìn)程同時(shí)綁定的 IP 地址和端口都相同,那么執(zhí)行 bind() 時(shí)候就會出錯(cuò),錯(cuò)誤是“Address already in use”。
注意,如果 TCP 服務(wù)進(jìn)程 A 綁定的地址是 0.0.0.0 和端口 8888,而如果 TCP 服務(wù)進(jìn)程 B 綁定的地址是 192.168.1.100 地址(或者其他地址)和端口 8888,那么執(zhí)行 bind() 時(shí)候也會出錯(cuò)。
這是因?yàn)?0.0.0.0 地址比較特殊,代表任意地址,意味著綁定了 0.0.0.0 地址,相當(dāng)于把主機(jī)上的所有 IP 地址都綁定了。
重啟 TCP 服務(wù)進(jìn)程時(shí),為什么會有“Address in use”的報(bào)錯(cuò)信息?
TCP 服務(wù)進(jìn)程需要綁定一個(gè) IP 地址和一個(gè)端口,然后就監(jiān)聽在這個(gè)地址和端口上,等待客戶端連接的到來。
然后在實(shí)踐中,我們可能會經(jīng)常碰到一個(gè)問題,當(dāng) TCP 服務(wù)進(jìn)程重啟之后,總是碰到“Address in use”的報(bào)錯(cuò)信息,TCP 服務(wù)進(jìn)程不能很快地重啟,而是要過一會才能重啟成功。
這是為什么呢?
當(dāng)我們重啟 TCP 服務(wù)進(jìn)程的時(shí)候,意味著通過服務(wù)器端發(fā)起了關(guān)閉連接操作,于是就會經(jīng)過四次揮手,而對于主動關(guān)閉方,會在 TIME_WAIT 這個(gè)狀態(tài)里停留一段時(shí)間,這個(gè)時(shí)間大約為 2MSL。
當(dāng) TCP 服務(wù)進(jìn)程重啟時(shí),服務(wù)端會出現(xiàn) TIME_WAIT 狀態(tài)的連接,TIME_WAIT 狀態(tài)的連接使用的 IP+PORT 仍然被認(rèn)為是一個(gè)有效的 IP+PORT 組合,相同機(jī)器上不能夠在該 IP+PORT 組合上進(jìn)行綁定,那么執(zhí)行 bind() 函數(shù)的時(shí)候,就會返回了 Address already in use 的錯(cuò)誤。
而等 TIME_WAIT 狀態(tài)的連接結(jié)束后,重啟 TCP 服務(wù)進(jìn)程就能成功。
重啟 TCP 服務(wù)進(jìn)程時(shí),如何避免“Address in use”的報(bào)錯(cuò)信息?
我們可以在調(diào)用 bind 前,對 socket 設(shè)置 SO_REUSEADDR 屬性,可以解決這個(gè)問題。
int on = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));1.2.
因?yàn)?nbsp; SO_REUSEADDR 作用是:如果當(dāng)前啟動進(jìn)程綁定的 IP+PORT 與處于TIME_WAIT 狀態(tài)的連接占用的 IP+PORT 存在沖突,但是新啟動的進(jìn)程使用了 SO_REUSEADDR 選項(xiàng),那么該進(jìn)程就可以綁定成功。
舉個(gè)例子,服務(wù)端有個(gè)監(jiān)聽 0.0.0.0 地址和 8888 端口的 TCP 服務(wù)進(jìn)程。?
有個(gè)客戶端(IP地址:192.168.1.100)已經(jīng)和服務(wù)端(IP 地址:172.19.11.200)建立了 TCP 連接,那么在 TCP 服務(wù)進(jìn)程重啟時(shí),服務(wù)端會與客戶端經(jīng)歷四次揮手,服務(wù)端的 TCP 連接會短暫處于 TIME_WAIT 狀態(tài):
客戶端地址:端口 服務(wù)端地址:端口 TCP 連接狀態(tài)192.168.1.100:37272 172.19.11.200:8888 TIME_WAIT1.2.
如果 TCP 服務(wù)進(jìn)程沒有對 socket 設(shè)置 SO_REUSEADDR 屬性,那么在重啟時(shí),由于存在一個(gè)和綁定 IP+PORT 一樣的 TIME_WAIT 狀態(tài)的連接,那么在執(zhí)行 bind() 函數(shù)的時(shí)候,就會返回了 Address already in use 的錯(cuò)誤。
如果 TCP 服務(wù)進(jìn)程對 socket 設(shè)置 SO_REUSEADDR 屬性了,那么在重啟時(shí),即使存在一個(gè)和綁定 IP+PORT 一樣的 TIME_WAIT 狀態(tài)的連接,依然可以正常綁定成功,因此可以正常重啟成功。
因此,在所有 TCP 服務(wù)器程序中,調(diào)用 bind 之前最好對 socket 設(shè)置 SO_REUSEADDR 屬性,這不會產(chǎn)生危害,相反,它會幫助我們在很快時(shí)間內(nèi)重啟服務(wù)端程序。?
前面我提到過這個(gè)問題:如果 TCP 服務(wù)進(jìn)程 A 綁定的地址是 0.0.0.0 和端口 8888,而如果 TCP 服務(wù)進(jìn)程 B 綁定的地址是 192.168.1.100 地址(或者其他地址)和端口 8888,那么執(zhí)行 bind() 時(shí)候也會出錯(cuò)。
這個(gè)問題也可以由 SO_REUSEADDR 解決,因?yàn)樗牧硗庖粋€(gè)作用是:綁定的 IP地址 + 端口時(shí),只要 IP 地址不是正好(exactly)相同,那么允許綁定。
比如,0.0.0.0:8888 和192.168.1.100:8888,雖然邏輯意義上前者包含了后者,但是 0.0.0.0 泛指所有本地 IP,而 192.168.1.100 特指某一IP,兩者并不是完全相同,所以在對 socket 設(shè)置 SO_REUSEADDR 屬性后,那么執(zhí)行 bind() 時(shí)候就會綁定成功。
客戶端的端口可以重復(fù)使用嗎?
客戶端在執(zhí)行 connect 函數(shù)的時(shí)候,會在內(nèi)核里隨機(jī)選擇一個(gè)端口,然后向服務(wù)端發(fā)起 SYN 報(bào)文,然后與服務(wù)端進(jìn)行三次握手。
所以,客戶端的端口選擇的發(fā)生在 connect 函數(shù),內(nèi)核在選擇端口的時(shí)候,會從 net.ipv4.ip_local_port_range 這個(gè)內(nèi)核參數(shù)指定的范圍來選取一個(gè)端口作為客戶端端口。
該參數(shù)的默認(rèn)值是 32768 61000,意味著端口總可用的數(shù)量是 61000 - 32768 = 28232 個(gè)。
當(dāng)客戶端與服務(wù)端完成 TCP 連接建立后,我們可以通過 netstat 命令查看 TCP 連接。
$ netstat -napt 協(xié)議 源ip地址:端口 目的ip地址:端口 狀態(tài) tcp 192.168.110.182.64992 117.147.199.51.443 ESTABLISHED1.2.3.
那問題來了,上面客戶端已經(jīng)用了 64992 端口,那么還可以繼續(xù)使用該端口發(fā)起連接嗎?
這個(gè)問題,很多同學(xué)都會說不可以繼續(xù)使用該端口了,如果按這個(gè)理解的話, 默認(rèn)情況下客戶端可以選擇的端口是 28232 個(gè),那么意味著客戶端只能最多建立 28232 個(gè) TCP 連接,如果真是這樣的話,那么這個(gè)客戶端并發(fā)連接也太少了吧,所以這是錯(cuò)誤理解。
正確的理解是,TCP 連接是由四元組(源IP地址,源端口,目的IP地址,目的端口)唯一確認(rèn)的,那么只要四元組中其中一個(gè)元素發(fā)生了變化,那么就表示不同的 TCP 連接的。所以如果客戶端已使用端口 64992 與服務(wù)端 A 建立了連接,那么客戶端要與服務(wù)端 B 建立連接,還是可以使用端口 64992 的,因?yàn)閮?nèi)核是通過四元祖信息來定位一個(gè) TCP 連接的,并不會因?yàn)榭蛻舳说亩丝谔栂嗤?,而?dǎo)致連接沖突的問題。
比如下面這張圖,有 2 個(gè) TCP 連接,左邊是客戶端,右邊是服務(wù)端,客戶端使用了相同的端口 50004 與兩個(gè)服務(wù)端建立了 TCP 連接。
仔細(xì)看,上面這兩條 TCP 連接的四元組信息中的「目的 IP 地址」是不同的,一個(gè)是 180.101.49.12 ,另外一個(gè)是 180.101.49.11。
多個(gè)客戶端可以 bind 同一個(gè)端口嗎?
bind 函數(shù)雖然常用于服務(wù)端網(wǎng)絡(luò)編程中,但是它也是用于客戶端的。
前面我們知道,客戶端是在調(diào)用 connect 函數(shù)的時(shí)候,由內(nèi)核隨機(jī)選取一個(gè)端口作為連接的端口。
而如果我們想自己指定連接的端口,就可以用 bind 函數(shù)來實(shí)現(xiàn):客戶端先通過 bind 函數(shù)綁定一個(gè)端口,然后調(diào)用 connect 函數(shù)就會跳過端口選擇的過程了,轉(zhuǎn)而使用 bind 時(shí)確定的端口。
針對這個(gè)問題:多個(gè)客戶端可以 bind 同一個(gè)端口嗎?
要看多個(gè)客戶端綁定的 IP + PORT 是否都相同,如果都是相同的,那么在執(zhí)行 bind() 時(shí)候就會出錯(cuò),錯(cuò)誤是“Address already in use”。
如果一個(gè)綁定在 192.168.1.100:6666,一個(gè)綁定在 192.168.1.200:6666,因?yàn)?IP 不相同,所以執(zhí)行 bind() 的時(shí)候,能正常綁定。
所以, 如果多個(gè)客戶端同時(shí)綁定的 IP 地址和端口都是相同的,那么執(zhí)行 bind() 時(shí)候就會出錯(cuò),錯(cuò)誤是“Address already in use”。
一般而言,客戶端不建議使用 bind 函數(shù),應(yīng)該交由 connect 函數(shù)來選擇端口會比較好,因?yàn)榭蛻舳说亩丝谕ǔ6紱]什么意義。
客戶端 TCP 連接 TIME_WAIT 狀態(tài)過多,會導(dǎo)致端口資源耗盡而無法建立新的連接嗎?
針對這個(gè)問題要看,客戶端是否都是與同一個(gè)服務(wù)器(目標(biāo)地址和目標(biāo)端口一樣)建立連接。
如果客戶端都是與同一個(gè)服務(wù)器(目標(biāo)地址和目標(biāo)端口一樣)建立連接,那么如果客戶端 TIME_WAIT 狀態(tài)的連接過多,當(dāng)端口資源被耗盡,就無法與這個(gè)服務(wù)器再建立連接了。
但是,因?yàn)橹灰蛻舳诉B接的服務(wù)器不同,端口資源可以重復(fù)使用的。
所以,如果客戶端都是與不同的服務(wù)器建立連接,即使客戶端端口資源只有幾萬個(gè), 客戶端發(fā)起百萬級連接也是沒問題的(當(dāng)然這個(gè)過程還會受限于其他資源,比如文件描述符、內(nèi)存、CPU 等)。
如何解決客戶端 TCP 連接 TIME_WAIT 過多,導(dǎo)致無法與同一個(gè)服務(wù)器建立連接的問題?
前面我們提到,如果客戶端都是與同一個(gè)服務(wù)器(目標(biāo)地址和目標(biāo)端口一樣)建立連接,那么如果客戶端 TIME_WAIT 狀態(tài)的連接過多,當(dāng)端口資源被耗盡,就無法與這個(gè)服務(wù)器再建立連接了。
針對這個(gè)問題,也是有解決辦法的,那就是打開 net.ipv4.tcp_tw_reuse 這個(gè)內(nèi)核參數(shù)。
因?yàn)殚_啟了這個(gè)內(nèi)核參數(shù)后,客戶端調(diào)用 connect 函數(shù)時(shí),如果選擇到的端口,已經(jīng)被相同四元組的連接占用的時(shí)候,就會判斷該連接是否處于 TIME_WAIT 狀態(tài),如果該連接處于 TIME_WAIT 狀態(tài)并且 TIME_WAIT 狀態(tài)持續(xù)的時(shí)間超過了 1 秒,那么就會重用這個(gè)連接,然后就可以正常使用該端口了。
舉個(gè)例子,假設(shè)客戶端已經(jīng)與服務(wù)器建立了一個(gè) TCP 連接,并且這個(gè)狀態(tài)處于 TIME_WAIT 狀態(tài):
客戶端地址:端口 服務(wù)端地址:端口 TCP 連接狀態(tài)192.168.1.100:2222 172.19.11.21:8888 TIME_WAIT1.2.
然后客戶端又與該服務(wù)器(172.19.11.21:8888)發(fā)起了連接,在調(diào)用 connect 函數(shù)時(shí),內(nèi)核剛好選擇了 2222 端口,接著發(fā)現(xiàn)已經(jīng)被相同四元組的連接占用了:
- 如果沒有開啟net.ipv4.tcp_tw_reuse 內(nèi)核參數(shù),那么內(nèi)核就會選擇下一個(gè)端口,然后繼續(xù)判斷,直到找到一個(gè)沒有被相同四元組的連接使用的端口, 如果端口資源耗盡還是沒找到,那么 connect 函數(shù)就會返回錯(cuò)誤。
- 如果開啟了 net.ipv4.tcp_tw_reuse 內(nèi)核參數(shù),就會判斷該四元組的連接狀態(tài)是否處于 TIME_WAIT 狀態(tài),如果連接處于 TIME_WAIT 狀態(tài)并且該狀態(tài)持續(xù)的時(shí)間超過了 1 秒,那么就會重用該連接,于是就可以使用 2222 端口了,這時(shí) connect 就會返回成功。
再次提醒一次,開啟了 net.ipv4.tcp_tw_reuse 內(nèi)核參數(shù),是客戶端(連接發(fā)起方) 在調(diào)用 connect() 函數(shù)時(shí)才起作用,所以在服務(wù)端開啟這個(gè)參數(shù)是沒有效果的。
客戶端端口選擇的流程總結(jié)
至此,我們已經(jīng)把客戶端在執(zhí)行 connect 函數(shù)時(shí),內(nèi)核選擇端口的情況大致說了一遍,為了讓大家更明白客戶端端口的選擇過程,我畫了一流程圖。
總結(jié)
TCP 和 UDP 可以同時(shí)綁定相同的端口嗎?
可以的。
TCP 和 UDP 傳輸協(xié)議,在內(nèi)核中是由兩個(gè)完全獨(dú)立的軟件模塊實(shí)現(xiàn)的。
當(dāng)主機(jī)收到數(shù)據(jù)包后,可以在 IP 包頭的「協(xié)議號」字段知道該數(shù)據(jù)包是 TCP/UDP,所以可以根據(jù)這個(gè)信息確定送給哪個(gè)模塊(TCP/UDP)處理,送給 TCP/UDP 模塊的報(bào)文根據(jù)「端口號」確定送給哪個(gè)應(yīng)用程序處理。
因此, TCP/UDP 各自的端口號也相互獨(dú)立,互不影響。
多個(gè) TCP 服務(wù)進(jìn)程可以同時(shí)綁定同一個(gè)端口嗎?
如果兩個(gè) TCP 服務(wù)進(jìn)程同時(shí)綁定的 IP 地址和端口都相同,那么執(zhí)行 bind() 時(shí)候就會出錯(cuò),錯(cuò)誤是“Address already in use”。
如果兩個(gè) TCP 服務(wù)進(jìn)程綁定的端口都相同,而 IP 地址不同,那么執(zhí)行 bind() 不會出錯(cuò)。
如何解決服務(wù)端重啟時(shí),報(bào)錯(cuò)“Address already in use”的問題?
當(dāng)我們重啟 TCP 服務(wù)進(jìn)程的時(shí)候,意味著通過服務(wù)器端發(fā)起了關(guān)閉連接操作,于是就會經(jīng)過四次揮手,而對于主動關(guān)閉方,會在 TIME_WAIT 這個(gè)狀態(tài)里停留一段時(shí)間,這個(gè)時(shí)間大約為 2MSL。
當(dāng) TCP 服務(wù)進(jìn)程重啟時(shí),服務(wù)端會出現(xiàn) TIME_WAIT 狀態(tài)的連接,TIME_WAIT 狀態(tài)的連接使用的 IP+PORT 仍然被認(rèn)為是一個(gè)有效的 IP+PORT 組合,相同機(jī)器上不能夠在該 IP+PORT 組合上進(jìn)行綁定,那么執(zhí)行 bind() 函數(shù)的時(shí)候,就會返回了 Address already in use 的錯(cuò)誤。
要解決這個(gè)問題,我們可以對 socket 設(shè)置 SO_REUSEADDR 屬性。
這樣即使存在一個(gè)和綁定 IP+PORT 一樣的 TIME_WAIT 狀態(tài)的連接,依然可以正常綁定成功,因此可以正常重啟成功。
客戶端的端口可以重復(fù)使用嗎?
在客戶端執(zhí)行 connect 函數(shù)的時(shí)候,只要客戶端連接的服務(wù)器不是同一個(gè),內(nèi)核允許端口重復(fù)使用。
TCP 連接是由四元組(源IP地址,源端口,目的IP地址,目的端口)唯一確認(rèn)的,那么只要四元組中其中一個(gè)元素發(fā)生了變化,那么就表示不同的 TCP 連接的。
所以,如果客戶端已使用端口 64992 與服務(wù)端 A 建立了連接,那么客戶端要與服務(wù)端 B 建立連接,還是可以使用端口 64992 的,因?yàn)閮?nèi)核是通過四元祖信息來定位一個(gè) TCP 連接的,并不會因?yàn)榭蛻舳说亩丝谔栂嗤鴮?dǎo)致連接沖突的問題。
客戶端 TCP 連接 TIME_WAIT 狀態(tài)過多,會導(dǎo)致端口資源耗盡而無法建立新的連接嗎?
要看客戶端是否都是與同一個(gè)服務(wù)器(目標(biāo)地址和目標(biāo)端口一樣)建立連接。
如果客戶端都是與同一個(gè)服務(wù)器(目標(biāo)地址和目標(biāo)端口一樣)建立連接,那么如果客戶端 TIME_WAIT 狀態(tài)的連接過多,當(dāng)端口資源被耗盡,就無法與這個(gè)服務(wù)器再建立連接了。即使在這種狀態(tài)下,還是可以與其他服務(wù)器建立連接的,只要客戶端連接的服務(wù)器不是同一個(gè),那么端口是重復(fù)使用的。
如何解決客戶端 TCP 連接 TIME_WAIT 過多,導(dǎo)致無法與同一個(gè)服務(wù)器建立連接的問題?
打開 net.ipv4.tcp_tw_reuse 這個(gè)內(nèi)核參數(shù)。
因?yàn)殚_啟了這個(gè)內(nèi)核參數(shù)后,客戶端調(diào)用 connect 函數(shù)時(shí),如果選擇到的端口,已經(jīng)被相同四元組的連接占用的時(shí)候,就會判斷該連接是否處于 TIME_WAIT 狀態(tài)。
如果該連接處于 TIME_WAIT 狀態(tài)并且 TIME_WAIT 狀態(tài)持續(xù)的時(shí)間超過了 1 秒,那么就會重用這個(gè)連接,然后就可以正常使用該端口了。