淺析套接字中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>}linux

這些數值組成的任何獨特的組合能夠惟一地確一個鏈接。那麼,對於任意鏈接,這五個值都不能徹底相同。不然的話操做系統就沒法區別這些鏈接了。android

一個socket的協議是在用socket()初始化的時候就設置好的。源地址(source address)和源端口(source port)在調用bind()的時候設置。目的地址(destination address)和目的端口(destination port)在調用connect()的時候設置。其中UDP是無鏈接的,UDP socket能夠在未與目的端口鏈接的狀況下使用。但UDP也能夠在某些狀況下先與目的地址和端口創建鏈接後使用。在使用無鏈接UDP發送數據的狀況下,若是沒有顯式地調用bind(),草錯系統會在第一次發送數據時自動將UDP socket與本機的地址和某個端口綁定(不然的話程序沒法接受任何遠程主機回覆的數據)。一樣的,一個沒有綁定地址的TCP socket也會在創建鏈接時被自動綁定一個本機地址和端口。ios

若是咱們手動綁定一個端口,咱們能夠將socket綁定至端口0,綁定至端口0的意思是讓系統本身決定使用哪一個端口(通常是從一組操做系統特定的提早決定的端口數範圍中),因此也就是任何端口的意思。一樣的,咱們也可使用一個通配符來讓系統決定綁定哪一個源地址(ipv4通配符爲0.0.0.0,ipv6通配符爲::)。而與端口不一樣的是,一個socket能夠被綁定到主機上全部接口所對應的地址中的任意一個。基於鏈接在本socket的目的地址和路由表中對應的信息,操做系統將會選擇合適的地址來綁定這個socket,並用這個地址來取代以前的通配符IP地址。macos

在默認狀況下,任意兩個socket不能被綁定在同一個源地址和源端口組合上。好比說咱們將socketA綁定在A:X地址,將socketB綁定在B:Y地址,其中AB是IP地址,XY是端口。那麼在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實現方法的源頭。windows

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:21192.168.1.1:21並非徹底相同的地址端口對(其中一個是通配符IP地址,另外一個是一個本機的具體IP地址),因此這樣的綁定是能夠成功的。須要注意的是,不管socketAsocketB初始化的順序如何,只要設置了SO_REUSEADDR,綁定都會成功;而只要沒有設置SO_REUSEADDR,綁定都不會成功。負載均衡

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

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 10.0.1.1:21 OK
ON / OFF 10.0.1.1:21 192.168.1.1:21 OK
OFF 192.168.1.1:21 0.0.0.0:21 ERROR(EADDRINUSE)
OFF 0.0.0.0:21 192.168.1.1:21 ERROR(EADDRINUSE)
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_REUSEADDRSO_REUSEPORT選項是也是能夠的。

SO_REUSEPORT是在SO_REUSEADDR以後被添加到BSD系統中的。這也是爲何如今有些系統的socket實現裏沒有SO_REUSEPORT選項。由於它們在這個選項被加入BSD系統以前參考了BSD的socket實現。而在這個選項被加入以前,BSD系統下沒有任何辦法可以將兩個socket綁定在徹底相同的地址端口對上。

Connect()返回EADDRINUSE?

有些時候bind()操做會返回EADDRINUSE錯誤。但奇怪的是,在咱們調用connect()操做時,也有可能獲得EADDRINUSE錯誤。這是爲何呢?爲什麼一個咱們嘗試令當前端口創建鏈接的遠程地址也會被佔用呢?難道將多個socket鏈接到同一個遠程地址的操做會有什麼問題產生嗎?

正如本文以前所說,一個鏈接關係是由一個五元組肯定的。對於任意的鏈接關係而言,這個五元組必須是惟一的。不然的話,系統將沒法分辨兩個鏈接。而如今當咱們採用了地址複用以後,咱們能夠將兩個採用相同協議的socket綁定到同一地址端口對上。這意味着對這兩個socket而言,五元組裏的{<protocol>, <src addr>, <src port>}已經相同了。在這種狀況下,若是咱們嘗試將它們都鏈接到同一個遠程地址端口上,這兩個鏈接關係的五元組將徹底相同。也就是說,產生了兩個徹底相同的鏈接。在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_REUSEADDRSO_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_REUSEPORTSO_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

轉自https://blog.csdn.net/yaokai_assultmaster/article/details/68951150
相關文章
相關標籤/搜索