首頁  >  文章  >  後端開發  >  詳談套接字中so_reuseport和so_reuseaddr的區別

詳談套接字中so_reuseport和so_reuseaddr的區別

不言
不言原創
2018-04-28 15:15:161677瀏覽

下面為大家分享一篇詳談套接字中so_reuseport和so_reuseaddr的區別,具有很好的參考價值,希望對大家有所幫助。一起過來看看吧

Socket的基本背景

在討論這兩個選項的區別時,我們需要知道的是BSD實現是所有socket實現的起源。基本上其他所有的系統某種程度上都參考了BSD socket實作(或至少是其介面),然後開始了它們自己的獨立發展進化。顯然,BSD本身也是隨著時間不斷發展的變化。所以較晚參考BSD的系統比較早期參考BSD的系統多一些特性。所以理解BSD socket實作是理解其他socket實現的基石。下面我們就來分析一下BSD socket實作。

在這之前,我們首先要明白如何唯一辨識TCP/UDP連線。 TCP/UDP是由以下五元組唯一地識別的:


{<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}


這些數值組成的任何獨特的組合可以唯一地的確一個連結。那麼,對於任意連接,這五個值都不能完全相同。否則的話操作系統就無法區別這些連接了。

一個socket的協定是在用socket()初始化的時候就設定好的。來源位址(source address)和來源連接埠(source port)在呼叫bind()的時候設定。目的位址(destination address)和目的連接埠(destination port)在呼叫connect()的時候設定。其中UDP是無連接的,UDP socket可以在未與目的連接埠連接的情況下使用。但UDP也可以在某些情況下先與目的位址和連接埠建立連線後使用。在使用無連接UDP發送資料的情況下,如果沒有明確地呼叫bind(),草錯系統會在第一次發送資料時自動將UDP socket與本機的位址和某個連接埠綁定(否則的話程式無法接受任何遠端主機回覆的資料)。同樣的,一個沒有綁定位址的TCP socket也會在建立連線時被自動綁定一個本機位址和連接埠。

如果我們手動綁定一個端口,我們可以將socket綁定至端口0,綁定至端口0的意思是讓系統自己決定使用哪個端口(一般是從一組操作系統特定的提前決定的連接埠數範圍中),所以也就是任何連接埠的意思。同樣的,我們也可以使用一個通配符來讓系統決定綁定哪個來源位址(ipv4通配符為0.0.0.0,ipv6通配符為::)。而與連接埠不同的是,一個socket可以被綁定到主機上所有介面所對應的位址中的任意一個。基於連接在本socket的目的位址和路由表中對應的訊息,作業系統將會選擇適當的位址來綁定這個socket,並用這個位址來取代先前的通配符IP位址。

在預設情況下,任兩個socket不能被綁定在同一個來源位址和來源埠組合上。比如說我們將socketA綁定在A:X位址,將socketB綁定在B:Y位址,其中A和B是IP位址,X和Y是埠。那麼在A==B的情況下X!=Y必須滿足,在X==Y的情況下A!=B必須滿足。要注意的是,如果某一個socket被綁定在通配符IP位址下,那麼事實上本機所有IP都會被系統認為與其綁定了。例如一個socket綁定了0.0.0.0:21,在這種情況下,任何其他socket不論選擇哪一個具體的IP位址,其都不能再綁定在21埠下。因為通配符IP0.0.0.0與所有本地IP都衝突。

以上所有內容基本上在主要作業系統中都相同。而各中SO_REUSEADDR會有不同的意義。首先我們來討論BSD實作。因為BSD試試其他所有socket實作方法的源頭。

BSD

SO_REUSEADDR

如果在一個socket綁定到某一位址和連接埠之前設定了其SO_REUSEADDR的屬性,那麼除非本socket與產生了嘗試與另一個socket綁定到完全相同的來源位址和來源埠組合的衝突,否則的話這個socket就可以成功的綁定這個位址埠對。這聽起來似乎和之前一樣。但是其中的關鍵字是完全。 SO_REUSEADDR主要改變了系統對待通配符IP位址衝突的方式。

如果不用SO_REUSEADDR的話,如果我們將socketA綁定到0.0.0.0:21,那麼任何將本機其他socket綁定到連接埠21的舉動(如綁定到192.168.1.1:21)都會導致EADDRINUSE錯誤。因為0.0.0.0是一個通配符IP位址,意味著任意一個IP位址,所以任何其他本機上的IP位址都被系統認為已被佔用。如果設定了SO_REUSEADDR選項,因為0.0.0.0:21和192.168.1.1:21並不是完全相同的位址連接埠對(其中一個是通配符IP位址,另一個是一個本機的特定IP位址),所以這樣的綁定定是可以成功的。要注意的是,無論socketA和socketB初始化的順序如何,只要設定了SO_REUSEADDR,綁定都會成功;而只要沒有設定SO_REUSEADDR,綁定都不會成功。

下面的表格列出了一些可能的情況及其結果。


##10.0.1.1:21OKON / OFF10.0.1.1:21192.168.1.1:21#OK#OFF192.168.1.1:21#0.0.0.0:21ERROR(EADDRINUSE)192.168.1.1:21
SO_REUSEADDR socketA socketB Result
ON / OFF #192.168.1.1:21 192.168.1.1:21 ERROR(EADDRINUSE)
ON / OFF 192.168.1.1:21
#ERROR(EADDRINUSE) # OFF 0.0.0.0:21
ERROR(EADDRINUSE) #ON
ON 192.168.1.1:21 0.0.0.0:21 OK
ON #0.0.0.0:21 192.168.1.1:21 OK
ON / OFF#######0.0.0.0:21######0.0 .0.0:21######OK############

這個表格假定socketA已經成功地綁定了表格中對應的位址,然後socketB被初始化了,其SO_REUSEADDR設定的情況如表格第一列所示,然後socketB試圖綁定表格中對應位址。 Result列是其綁定的結果。如果第一列中的值是ON/OFF,那麼SO_REUSEADDR設定與否都與結果無關。

上面討論了SO_REUSEADDR對通配符IP位址的作用,但其並不只有這一作用。其另一作用也是為什麼大家在進行伺服器端編程的時候會採用SO_REUSEADDR選項的原因。為了理解其另一個作用及其重要應用,我們需要先更深入地討論TCP協議的工作原理。

每一個socket都有其對應的發送緩衝區(buffer)。當成功呼叫其send()方法的時候,實際上我們所要求發送的資料並不一定被立即發送出去,而是被添加到了發送緩衝區中。對於UDP socket來說,即使不是馬上被發送,這些資料一般也會很快被送出去。但對於TCP socket來說,在將資料添加到發送緩衝區之後,可能需要等待相對較長的時間之後資料才會被真正發送出去。因此,當我們關閉了一個TCP socket之後,其發送緩衝區中可能實際上仍然有等待發送的資料。但此時因為send()回傳了成功,我們的程式碼認為資料已經實際上成功發送了。如果TCP socket在我們呼叫close()之後直接關閉,那麼所有這些資料都會遺失,而我們的程式碼根本不會知道。但是,TCP是一個可靠的傳輸層協議,直接丟棄這些待傳輸的資料顯然是不可取的。實際上,如果在socket的傳送緩衝區中還有待傳送資料的情況下呼叫了其close()方法,將會進入一個所謂的TIME_WAIT狀態。在這個狀態下,socket將會持續嘗試傳送緩衝區的資料直到所有資料都成功傳送或直到逾時,超時被觸發的情況下socket將會被強制關閉。

作業系統的kernel在強制關閉一個socket之前的最長等待時間被稱為延遲時間(Linger Time)。在大部分系統中延遲時間都已經被全域設定好了,並且相對較長(大部分系統將其設定為2分鐘)。我們也可以在初始化一個socket的時候使用SO_LINGER選項來特定地設定每一個socket的延遲時間。我們甚至可以完全關閉延遲等待。但是需要注意的是,將延遲時間設為0(完全關閉延遲等待)並不是一個好的程式設計實踐。因為優雅地關閉TCP socket是一個比較複雜的過程,過程中包含與遠端主機交換數個封包(包括在丟包的情況下的遺失重傳),而這個封包交換的過程所需要的時間也包括在延遲時間。如果我們停用延遲等待,socket不只在關閉的時候直接丟棄所有待發送的數據,而且總是會被強制關閉(由於TCP是面向連接的協議,不與遠端端口交換關閉數據包將會導致遠端連接埠處於長時間的等待狀態)。所以通常我們不建議在實際編程中這樣做。 TCP斷開連線的過程超出了本文討論的範圍,如果對此有興趣,可以參考這個頁面。並且實際上,如果我們禁用了延遲等待,而我們的程式沒有明確地關閉socket就退出了,BSD(可能包括其他系統)會忽略我們的設定進行延遲等待。例如,如果我們的程式呼叫了exit()方法,或者其進程被使用某個訊號終止了(包括進程因為非法記憶體存取之類的情況而崩潰)。所以我們無法百分之百保證一個socket在所有情況下忽略延遲等待時間而終止。

這裡的問題在於作業系統如何對待處於TIME_WAIT階段的socket。如果SO_REUSEADDR選項沒有被設置,處於TIME_WAIT階段的socket任然被認為是綁定在原來那個位址和連接埠上的。直到該socket完全關閉之前(結束TIME_WAIT階段),任何其他企圖將一個新socket綁定該該位址連接埠對的操作都無法成功。這一等待的過程可能和延遲等待的時間一樣長。所以我們並不能馬上將一個新的socket綁定到一個剛被關閉的socket對應的位址連接埠對上。在大多數情況下這種操作都會失敗。

然而,如果我們在新的socket上設定了SO_REUSEADDR選項,如果此時有另一個socket綁定在目前的位址連接埠對且處於TIME_WAIT階段,那麼這個已存在的綁定關係將會被忽略。事實上處於TIME_WAIT階段的socket已經是半關閉的狀態,將一個新的socket綁定在這個位址連接埠對上不會有任何問題。這樣的話原來綁定在這個連接埠上的socket一般不會對新的socket產生影響。但要注意的是,在某些時候,將一個新的socket綁定在一個處於TIME_WAIT階段但仍在工作的socket所對應的地址端口對會產生一些我們並不想要的,無法預料的負面影響。但這個問題超過了本文的討論範圍。而且幸運的是這些負面影響在實務上很少見到。

最後,關於SO_REUSEADDR,我們還要注意的一件事是,以上所有內容只要我們對新的socket設定了SO_REUSEADDR就成立。至於原有的已經綁定在目前位址連接埠對上的,處於或不處於TIME_WAIT階段的socket是否設定了SO_REUSEADDR並無影響。決定bind操作是否成功的程式碼只會檢查新的被傳遞到bind()方法的socket的SO_REUSEADDR選項。其他涉及到的socket的SO_REUSEADDR選項並不會被檢查。

SO_REUSEPORT

許多人將SO_REUSEADDR當成了SO_REUSEPORT。基本上來說,SO_REUSEPORT允許我們將任意數目的socket綁定到完全相同的來源位址連接埠對上,只要所有先前綁定的socket都設定了SO_REUSEPORT選項。如果第一個綁定在該位址連接埠對上的socket沒有設定SO_REUSEPORT,無論之後的socket是否設定SO_REUSEPORT,其都無法綁定在與這個位址連接埠完全相同的位址上。除非第一個綁定在這個位址連接埠對上的socket釋放了這個綁定關係。與SO_REUSEADDR不同的是 ,處理SO_REUSEPORT的程式碼不僅會檢查目前嘗試綁定的socket的SO_REUSEPORT,而且也會檢查先前已綁定了目前嘗試綁定的位址連接埠對的socket的SO_REUSEPORT選項。

SO_REUSEPORT並不等於SO_REUSEADDR。這麼說的意思是如果一個已經綁定了位址的socket沒有設定SO_REUSEPORT,而另一個新socket設定了SO_REUSEPORT且嘗試綁定到與目前socket完全相同的埠位址對,這次綁定嘗試將會失敗。同時,如果目前socket已經處於TIME_WAIT階段,而這個設定了SO_REUSEPORT選項的新socket嘗試綁定到目前位址,這個綁定操作也會失敗。為了能夠將新的socket綁定到目前處於TIME_WAIT階段的socket對應的位址連接埠對上,我們要麼需要在綁定之前設定這個新socket的SO_REUSEADDR選項,要麼需要在綁定之前給兩個socket都設定SO_REUSEPORT選項。當然,同時為socket設定SO_REUSEADDR和SO_REUSEPORT選項是也是可以的。

SO_REUSEPORT是在SO_REUSEADDR之後被加入到BSD系統中的。這也是為什麼現在有些系統的socket實作裡沒有SO_REUSEPORT選項。因為它們在這個選項被加入BSD系統之前參考了BSD的socket實作。而在這個選項被加入之前,BSD系統下沒有任何辦法可以將兩個socket綁定在完全相同的位址連接埠對上。

Connect()回傳EADDRINUSE?

有些時候bind()運算會回傳EADDRINUSE錯誤。但奇怪的是,當我們呼叫connect()操作時,也有可能得到EADDRINUSE錯誤。這是為什麼呢?為何一個我們嘗試令目前連接埠建立連線的遠端位址也會被佔用呢?難道將多個socket連接到同一個遠端位址的操作會有什麼問題產生嗎?

如本文之前所說,一個連結關係是由一個五元組決定的。對於任意的連結關係而言,這個五元組必須是唯一的。否則的話,系統將無法分辨兩個連接。而現在當我們採用了位址復用之後,我們可以將兩個採用相同協定的socket綁定到同一位址連接埠對上。這意味著對這兩個socket而言,五元組裡的{a88b79ba1ccee8890e978c768d80530d, 3037a7cee66f0ae45683db6cc2520e8a, 1b9006debff836a11384f3384ae84936}已經相同了。在這種情況下,如果我們嘗試將它們都連接到同一個遠端位址連接埠上,這兩個連接關係的五元組將完全相同。也就是說,產生了兩個完全相同的連接。在TCP協定中這是不被允許的(UDP是無連線的)。如果這兩個完全相同的連接種的某一個接收到了數據,系統將無法分辨這個數據到底屬於哪個連接。所以在這種情況下,至少這兩個socket所嘗試連接的遠端主機的位址和連接埠不能相同。只有如此,系統才能繼續區分這兩個連結關係。

所以當我們將兩個採用相同協議的socket綁定到同一個本地地址端口對上後,如果我們還嘗試讓它們和同一個目的地址端口對建立連接,第二個嘗試調用connect()方法的socket將會報EADDRINUSE的錯誤,這表示一個擁有完全相同的五元組的socket已經存在了。

Multicast Address

相對於一對一通訊的unicast位址,multicast位址用於一對多重通訊。 IPv4和IPv6都擁有multicast位址。但是IPv4中的multicast其實在公共網路道路上很少被使用。

SO_REUSEADDR的意義在multicast位址的情況下會與之前有所不同。在這種情況下,SO_REUSEADDR允許我們將多個socket綁定至完全相同的來源廣播位址連接埠對上。換句話說,對於multicast位址而言,SO_REUSEADDR的作用相當於unicast通訊中的SO_REUSEPORT。事實上,在multicast情況下,SO_REUSEADDR和SO_REUSEPORT的作用完全相同。

FreeBSD/OpenBSD/NetBSD

#所有這些系統都是參考了較新的原生BSD系統程式碼。所以這三個系統提供與BSD完全相同的socket選項,這些選項的意義與原生BSD完全相同。

MacOS X

MacOS X的核心程式碼實作是基於較新版的原生BSD的BSD風格的UNIX,所以MacOS X提供與BSD完全相同的socket選項,而且它們的意義也與BSD系統相同。

iOS

iOS事實上是稍微改造過的MacOS X,所以適用於MacOS X的也適用於iOS。

Linux

在Linux3.9之前,只有SO_REUSEADDR選項存在。這個選項的作用基本上是同BSD系統下相同。但其仍有兩個重要的差異。

第一個區別是如果一個處於監聽(伺服器)狀態下的TCP socket已經被綁定到了一個通配符IP位址和一個特定連接埠下,那麼不論這兩個socket有沒有設定SO_REUSEADDR選項,任何其他TCP socket都無法再被綁定到相同的連接埠下。即使另一個socket使用了一個特定IP位址(像在BSD系統中允許的那樣)也不行。而非監聽(客戶)TCP socket則無此限制。

第二個差異是對UDP socket來說,SO_REUSEADDR的作用和BSD中SO_REUSEPORT完全相同。所以兩個UDP socket如果都設定了SO_REUSEADDR的話,它們就可以被綁定在一組完全相同的位址連接埠對上。

Linux3.9加入了SO_REUSEPORT選項。只要所有socket(包括第一個)在綁定位址前設定了這個選項,兩個或多個,TCP或UDP,監聽(伺服器)或非監聽(客戶)socket就可以被綁定在完全相同的位址端口組合下。同時,為了防止連接埠劫持(port hijacking),還有一個特別的限制:所有試圖綁定在相同的位址連接埠組合的socket必須屬於擁有相同使用者ID的進程。所以一個用戶無法從另一個用戶那裡「偷竊」連接埠。

除此之外,對於設定了SO_REUSEPORT選項的socket,Linux kernel還會執行一些別的系統所沒有的特別的操作:對於綁定於同一位址連接埠組合上的UDP socket,kernel嘗試在它們之間平均分配收到的資料包;對於綁定於相同位址連接埠組合上的TCP監聽socket,kernel嘗試在它們之間平均分配收到的連線請求(呼叫accept()方法所得到的請求) 。這意味著相比於其他允許位址復用但隨機將收到的資料包或連接請求分配給連接在同一位址連接埠組合上的socket的系統而言,Linux嘗試了進行流量分配上的最佳化。例如一個簡單的伺服器進程的幾個不同實例可以方便地使用SO_REUSEPORT來實現一個簡單的負載平衡,而且這個負載平衡有kernel負責, 對程式來說完全免費!

Android

Android的核心部分是稍微修改過的Linux kernel,所以所有適用於Linux的操作也適用於Android。

Windows

#

Windows只有SO_REUSEADDR選項。在Windows中對一個socket設定SO_REUSEADDR的效果與在BSD下同時對一個socket設定SO_REUSEPORT和SO_REUSEADDR相同。但其區別在於:即使另一個已綁定位址的socket並沒有設定SO_REUSEADDR,一個設定了SO_REUSEADDR的socket總是可以綁定到與另一個已綁定的socket完全相同的位址連接埠組合上。這個行為可以說是有些危險的。因為它允許了一個應用程式從另一個引用已連接的連接埠上偷取資料。微軟意識到了這個問題,因此添加了另一個socket選項:SO_EXCLUSIVEADDRUSE。對一個socket設定SO_EXCLUSIVEADDRUSE可以確保一旦該socket綁定了一個位址連接埠組合,任何其他socket,不論設定SO_REUSEADDR與否,都無法再綁定目前的位址連接埠組合。

Solaris

Solaris是SunOS的繼承者。 SunOS從某種程度上來說也是一個較早版本的BSD的一個支路。因此Solaris只提供SO_REUSEADDR,其表現和BSD系統中基本相同。據我所知,在Solaris系統中無法實現與SO_REUSEPORT相同的功能。這意味著在Solaris中無法將兩個socket綁定到完全相同的位址連接埠組合下。

與Windows類似的是,Solaris也為socket提供獨佔綁定的選項-SO_EXCLBIND。如果一個socket在綁定位址前設定了這個選項,即使其他socket設定了SO_REUSEADDR也將無法綁定至相同位址。例如:如果socketA綁定在了通配符IP位址下,而socketB設定了SO_REUSEADDR且綁定在一個特定IP位址和與socketA相同的連接埠的組合下,這個操作在socketA沒有設定SO_EXCLBIND的情況下會成功,否則會失敗。

Reference:

http://stackoverflow.com/a/14388707/6037083


##

以上是詳談套接字中so_reuseport和so_reuseaddr的區別的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn