1. 程式人生 > >SO_REUSEADDR和SO_REUSEPORT異同

SO_REUSEADDR和SO_REUSEPORT異同

kill程序後重啟,發現telnet 埠繫結失敗,採用SO_REUSEADDR還是沒有用, linux2.6.35 上SO_REUSEPORT未定義,要打補丁。

http://patchwork.ozlabs.org/patch/50430/

      雖然不同的系統上socket的實現方式有一些差異,但都來源於對BSD socket的實現,因此在討論其它系統之前瞭解BSD socket的實現是非常有益的。首先我們需要了解一些基本知識,一個TCP/UDP連線是被一個五元組確定的:
       {, , , , }
     因此,任何兩個連線都不可能擁有相同的五元組,否則系統將無法區別這兩個連線。
     當使用socket()函式建立套接字的時候,我們就指定了該套接字使用的protocol(協議),bind()函式設定了源地址和源埠號,而目的地址和目的埠號則由connect()函式設定。儘管允許對UDP進行"連線"(在某些情況下這對應用程式的設計非常有幫助)但由於UDP是一個無連線協議,UDP套接字仍然可以不經連線就使用。"未連線"的UDP套接字在資料被第一次傳送之前並不會繫結,只有在傳送的時候被系統自動繫結,因此未繫結的UDP套接字也就無法收到(回覆)資料。未繫結的TCP也一樣,它將在連線的時候自動繫結。
     如果你明確繫結一個socket,把它繫結到埠0是可行的,它意味著"any port"("任意埠")。由於一個套接字無法真正的被繫結到系統上的所有埠,那麼在這種情況下系統將不得不選擇一個具體的埠號(指的是"any port")。源地址使用類似的萬用字元,也就是"any address" (IPv4中的0.0.0.0和IPv6中的::)。和埠不同的是,一個套接字可以被繫結到任意地址(any address),這裡指的是本地網路介面的所有地址
。由於socket無法在連線的時候同時繫結到所有源IP地址,因此當接下來有一個連線過來的時候,系統將不得不挑選一個源IP地址。考慮到目的地址和路由表中的路由資訊,系統將會選擇一個合適的源地址,並將任意地址替換為一個選定的地址作為源地址。
      預設情況下,任意兩個socket都無法繫結到相同的源IP地址和源埠(即源地址和源埠號均相同)。只要源埠號不相同,那麼源地址實際上沒什麼關係。將socketA繫結到地址A和埠X (A:X),socketB繫結到地址B和埠Y (B:Y),只要X != Y,那麼這種繫結都是可行的。然而當X==Y的時候只要A != B,這種繫結方式也仍然可行,比如:一個FTP server的socketA繫結為192.168.0.1:21而屬於另一個FTP server的socketB繫結為 10.0.0.1:21,這兩個繫結都將成功。記住:一個socket可能繫結到本地"any address"。例如一個socket繫結為 0.0.0.0:21,那麼它同時
綁定了所有的本地地址,在這種情況下,不論其它的socket選擇什麼特定的IP地址,它們都無法繫結到21埠,因為0.0.0.0和所有的本地地址都會衝突。
     上面說的對所有主流作業系統都是一樣的。當涉及到地址重用的時候,OS之間的差異就顯現出來了,正如之前所說的那樣,其它的實現方案都來源於BSD的實現,因此我們首先從BSD說起。

     BSD
     SO_REUSEADDR

     如果在繫結一個socket之前設定了SO_REUSEADDR,除非兩個socket繫結的源地址和埠號都一樣,那麼這兩個繫結都是可行的。也許你會疑惑這跟之前的有什麼不一樣?關鍵是SO_REUSEADDR改變了在處理源地址衝突時對通配地址("any ip address")的處理方式。

     當沒有設定SO_REUSEADDR的時候,socketA先繫結到0.0.0.0:21,然後socketB繫結到192.168.0.1:21的時候將會失敗(EADDRINUSE錯誤),因為0.0.0.0意味著"任意本地IP地址”,也就是"所有本地IP地址“,因此包括192.168.0.1在內的所有IP地址都被認為是已經使用了。但是在設定SO_REUSEADDR之後socketB的繫結將會成功,因為0.0.0.0和192.168.0.1事實上不是同一個IP地址,一個是代表所有地址的通配地址,另一個是一個具體的地址。注意上面的表述對於socketA和socketB的繫結順序是無關的,沒有設定SO_REUSEADDR,它們將失敗,設定了SO_REUSEADDR,它將成功。
     下面給出了一個表格列出了所有的可能組合:
SO_REUSEADDR       socketA        socketB       Result
---------------------------------------------------------------------
  ON/OFF       192.168.0.1:21   192.168.0.1:21    Error (EADDRINUSE)
  ON/OFF       192.168.0.1:21      10.0.0.1:21    OK
  ON/OFF          10.0.0.1:21   192.168.0.1:21    OK
   OFF             0.0.0.0:21   192.168.1.0:21    Error (EADDRINUSE)
   OFF         192.168.1.0:21       0.0.0.0:21    Error (EADDRINUSE)
   ON              0.0.0.0:21   192.168.1.0:21    OK
   ON          192.168.1.0:21       0.0.0.0:21    OK
  ON/OFF           0.0.0.0:21       0.0.0.0:21    Error (EADDRINUSE)
上面的表格假定socketA已經成功繫結,然後建立socketB繫結給定地址在是否設定SO_REUSEADDR的情況下的結果。Result代表socketB的繫結行為是否會成功。如果第一列是ON/OFF,那麼SO_REUSEADDR的值將是無關緊要的。

      現在我們知道SO_REUSEADDR對通配地址有影響,但這不是它唯一影響到的方面。還有一個眾所周知的影響同時也是大多數人在伺服器程式上使用SO_REUSEADDR的首要原因。為了瞭解其它SO_REUSEADDR重要的使用方式,我們需要深入瞭解TCP協議的工作方式。
      一個socket有一個傳送緩衝區,當呼叫send()函式成功後,這並不意味著所有資料都真正被髮送出去了,它只意味著資料都被送到了傳送緩衝區中。對於UDP socket來說,如果不是立刻傳送的話,資料通常也會很快的傳送出去,但對於TCP socket,在資料加入到緩衝區和真正被髮送出去之間的時延會相當長。這就導致當我們close一個TCP socket的時候,可能在傳送緩衝區中儲存著等待發送的資料(由於send()成功返回,因此你也許認為資料已經被髮送了)。如果TCP的實現是立刻關閉socket,那麼所有這些資料都會丟失而你的程式根本不可能知道。TCP被稱為可靠協議,像這種丟失資料的方式就不那麼可靠了。這也是為什麼當我們close一個TCP socket的時候,如果它仍然有資料等待發送,那麼該socket會進入TIME_WAIT狀態。這種狀態將持續到資料被全部發送或者發生超時。
     在核心徹底關閉socket之前等待的總時間(不管是否有資料在傳送緩衝區中等待發送)叫做Linger Time。Linger Time在大部分系統上都是一個全域性性的配置項而且在預設情況下時間相當長(在大部分系統上是兩分鐘)。當然對於每個socket我們也可以使用socket選項SO_LINGER進行配置,可以將等待時間設定的更長一點兒或更短一點兒甚至禁用它。禁用Linger Time絕對是一個壞主意,雖然優雅的關閉socket是一個稍微複雜的過程並且涉及到來回的傳送資料包(以及在資料包丟失後重發它們),並且這個過程還受到Linger Time的限制。如果禁用Linger Time,socket可能丟失的不僅僅是待發送的資料,而且還會粗暴的關閉socket,在絕大部分情況下,都不應該這樣使用。如何優雅的關閉TCP連線的細節不在這裡進行討論,如果你想了解更多,我建議你閱讀:http://www.freesoft.org/CIE/Course/Section4/11.htm。而且如果你用SO_LINGER禁用了Linger Time,而你的程式在顯式的關閉socket之前就終止的話,BSD(其它的系統也有可能)仍然會等待,而不管已經禁用了它。這種情況的一個例子就是你的程式呼叫了exit() (在小的伺服器程式很常見)或者程序被訊號殺死(也有可能是程序訪問了非法記憶體而終止)。這樣的話,不管在什麼情況下,你都無法對某一個socket禁用linger了。
      問題在於,系統是怎樣看待TIME_WAIT狀態的?如果SO_REUSEADDR還沒有設定,一個處在TIME_WAIT的socket仍然被認為繫結在源地址和埠,任何其它的試圖在同樣的地址和埠上繫結一個socket行為都會失敗直到原來的socket真正的關閉了,這通常需要等待Linger Time的時長。所以不要指望在一個socket關閉後立刻將源地址和埠繫結到新的socket上,在絕大部分情況下,這種行為都會失敗。然而,在設定了SO_REUSEADDR之後試圖這樣繫結(繫結相同的地址和埠)僅僅只會被忽略,而且你可以將相同的地址繫結到不同的socket上。注意當一個socket處於TIME_WAIT狀態,而你試圖將它繫結到相同的地址和埠,這會導致未預料的結果,因為處於TIME_WAIT狀態的socket仍在"工作",幸運的是這種情況極少發生。
      對於SO_REUSEADDR你需要知道的最後一點是隻有在你想繫結的socket開啟了地址重用(address reuse)之後上面的才會生效,不過這並不需要檢查之前已經繫結或處於TIME_WAIT的socket在它們繫結的時候是否也設定這個選項。也就是說,繫結的成功與否只會檢查當前bind的socket是否開啟了這個標誌,不會檢視其它的socket。

 SO_REUSEPORT

      SO_REUSEPORT的含義與絕大部分人對SO_REUSEADDR的理解一樣。基本上說來,SO_REUSEPORT允許你將多個socket繫結到相同的地址和埠只要它們在繫結之前都設定了SO_REUSEPORT。如果第一個繫結某個地址和埠的socket沒有設定SO_REUSEPORT,那麼其他的socket無論有沒有設定SO_REUSEPORT都無法繫結到該地址和埠直到第一個socket釋放了繫結。
      SO_REUSEPORT並不表示SO_REUSEADDR。這意味著如果一個socket在繫結時沒有設定SO_REUSEPORT,那麼同預期的一樣,其它的socket對相同地址和埠的繫結會失敗,但是如果繫結相同地址和埠的socket正處在TIME_WAIT狀態,新的繫結也會失敗。當有個socket繫結後處在TIME_WAIT狀態(釋放時)時,為了使得其它socket繫結相同地址和埠能夠成功,需要設定SO_REUSEADDR或者在這兩個socket上都設定SO_REUSEPORT。當然,在socket上同時設定SO_REUSEPORT和SO_REUSEADDR也是可行的。
    關於SO_REUSEPORT除了它在被新增到系統的時間比SO_REUSEPORT晚就沒有其它需要說的了,這也是為什麼在有些系統的socket實現上你找不到這個選項,因為這些系統的程式碼都是在這個選項被新增到BSD之前fork了BSD,這樣就不能將兩個socket繫結到真正相同的“地址” (address+port)。

 Connect() Returning EADDRINUSE?

      絕大部分人都知道bind()可能失敗返回EADDRINUSE,然而當你開始使用地址重用(address reuse),你可能會碰到奇怪的情況:connect()
失敗返回同樣的錯誤EADDRINUSE。怎麼會出現這種情況了? 一個遠端地址(remote address)畢竟是connect新增到socket上的,怎麼會已經被使用了? 將多個socket連線到相同的遠端地址從來沒有出現過這樣的情況,這是為什麼了?
     正如我在開頭說過的,一個連線是被一個五元組定義的。同樣我也說了任意兩個連線的五元組不能完全一樣,因為這樣的話核心就沒辦法區分這兩個連線了。然而,在地址重用的情況下,你可以把同協議的兩個socket繫結到完全相同的源地址和源埠,這意味著五元組中已經有三個元素相同了(協議,源地址,源埠)。如果你嘗試將這些socket連線到同樣的目的地址和目的埠,你就建立了兩個完全相同的連線。這是不行的,至少對TCP不行(UDP實際上沒有真實的連線)。如果資料到達這兩個連線中的任何一個,那麼系統將無法區分資料到底屬於誰。因此當源地址和源埠相同時,目的地址或者目的埠必須不同,否則核心無法進行區分,這種情況下,connect()將在第二個socket嘗試連線時返回EADDRINUSE。

Multicast Address(多播地址)

      大部分人都會忽略多播地址的存在,但它們的確存在。單播地址(unicast address)用於單對單通訊,多播地址用於單對多通訊。大部分人在他們學習了IPv6後才注意到多播地址的存在,但在IPv4中多播地址就有了,儘管它們在公共網際網路上用的並不多。
      對多播地址來說,SO_REUSEADDR的含義發生了改變,因為它許多個socket繫結到完全一樣的多播地址和埠,也就是說,對多播地址SO_REUSEADDR的行為與SO_REUSEPORT對單播地址完全一樣。事實上,對於多播地址,對SO_REUSEADDR和SO_REUSEPORT的處理完全一樣,對所有多播地址,SO_REUSEADDR也就意味著SO_REUSEPORT。

FreeBSD/OpenBSD/NetBSD

     它們都是很晚的時候衍生自原生BSD的系統,它們與原生BSD的選項和行為都一樣。

MacOS X

     MacOS X的核心就是一個BSD型別的UNIX,基於很新的BSD程式碼,甚至Mac OS 10.3的釋出與FreeBSD 5都是同步的,因此MacOS與BSD一樣提供相同的選項,處理行為也一樣。

IOS

     IOS只是在核心上稍微修改了MacOS,因此選項和處理行為也和MacOS一樣。

Linux

在linux 3.9之前,只存在選項SO_REUSEADDR。除了兩個重要的差別,大體上與BSD一樣。第一個差別:當一個監聽(listening)TCP socket繫結到通配地址和一個特定的埠,無論其它的socket或者是所有的socket(包括監聽socket)都設定了SO_REUSEADDR,其它的TCP socket都無法繫結到相同的埠(BSD中可以),就更不用說使用一個特定地址了。這個限制並不用在非監聽TCP socket上,當一個監聽socket繫結到一個特定的地址和埠組合,然後另一個socket繫結到通配地址和相同的埠,這樣是可行的。第二個差別: 當把SO_REUSEADDR用在UDP socket上時,它的行為與BSD上SO_REUSEPORT完全相同,因此兩個UDP socket只要都設定了SO_REUSEADDR,那麼它們可以繫結到相同的地址和埠。
    Linux 3.9加入了SO_REUSEPORT。這個選項允許多個socket(TCP or UDP)不管是監聽socket還是非監聽socket只要都在繫結之前都設定了它,那麼就可以繫結到完全相同的地址和埠。為了阻止"port 劫持"(Port hijacking)有一個特別的限制:所有希望共享源地址和埠的socket都必須擁有相同的有效使用者id(effective user ID)。因此一個使用者就不能從另一個使用者那裡"偷取"埠。另外,核心在處理SO_REUSEPORT socket的時候使用了其它系統上沒有用到的"特別魔法":對於UDP socket,核心嘗試平均的轉發資料報,對於TCP監聽socket,核心嘗試將新的客戶連線請求(由accept返回)平均的交給共享同一地址和埠的socket(監聽socket)。這意味著在其他系統上socket收到一個數據報或連線請求或多或少是隨機的,但是linux嘗試優化分配。例如:一個簡單的伺服器程式的多個例項可以使用SO_REUSEPORT socket實現一個簡單的負載均衡,因為核心已經把複製的分配都做了。

Android

     儘管整個Android系統與大多數linux發行版都不一樣,但是它的核心是個稍加修改的linux核心,因此它的SO_REUSEADDR和SO_REUSEPORT與linux一樣。

Windows

    windows上只有SO_REUSEADDR選項,沒有SO_REUSEPORT。在windows上設定了SO_REUSEADDR的socket其行為與BSD上設定了SO_REUSEPORT和SO_REUSEADDRd的行為大致一樣,只有一個差別:一個設定了SO_REUSEADDR的socket總是可以繫結到已經被繫結過的源地址和源埠,不管之前在這個地址和埠上繫結的socket是否設定了SO_REUSEADDR沒有。這種行為在某種程度上有些危險因為它允許一個應用程式從別的應用程式上"偷取"已連線的埠。不用說,這對安全性有極大的影響,Microsoft意識到了這個問題,就加入了另一個socket選項: SO_EXECLUSIVEADDRUSE。設定了SO_EXECLUSIVEADDRUSE的socket確保一旦繫結成功,那麼被繫結的源埠和地址就只屬於這一個socket,其它的socket不能繫結,甚至他們使用了SO_REUSEADDR也沒用。

Solaris

     Solaris是SunOS的后羿,SunOS起源於BSD,SunOS 5和之後的版本則基於SVR4,然而SVR4是BSD,System V和Xenix的集合體,所以從某種程度上說,Solaris也是BSD的分支,而且是相當早的一個分支。這就導致了Solaris只有SO_REUSEADDR而沒有SO_REUSEPORT。Solaris上SO_REUSEADDR的行為與BSD的非常相似。從我知道的來看,在Solaris上沒辦法實現SO_REUSEPORT的行為,也就是說,想把兩個socket繫結到相同的源地址和埠上是不可能的。
     與Windows類似,Solaris也有一個選項提供互斥繫結,這個選項叫SO_EXCLBIND。如果在一個socket在繫結之前設定這個選項,那麼在其他的socket上設定SO_REUSEADDR將沒有任何影響。比如socketA綁定了一個通配地址,socketB設定了SO_REUSEADDR並且繫結到一個非通配地址和相同的埠,那麼這個繫結將成功,除非socketA設定了SO_EXCLBIND,在這種情況下,socketB的繫結將失敗不管它是否設定了SO_REUSEADDR。