SO_REUSEADDR SO_REUSEPORT 解析
首先我們需要了解一些基本知識,一個TCP/UDP連接是被一個五元組確定的{源地址,源端口,協議,目的端口,目的地址}。
因此,任何兩個連接都不可能擁有相同的五元組,否則系統將無法區別這兩個連接。
當使用socket()函數創建套接字的時候,我們就指定了該套接字使用的protocol(協議),bind()函數設置了源地址和源端口號,而目的地址和目的端口號則由connect()函數設定。
盡管允許對UDP進行"連接",但由于UDP是一個無連接協議,UDP套接字仍然可以不經連接就使用。"未連接"的UDP套接字在數據被第一次發送之前并不會綁定,只有在發送的時候被系統自動綁定,因此未綁定的UDP套接字也就無法收到(回復)數據。未綁定的TCP也一樣,它將在連接的時候自動綁定。
如果你明確綁定一個socket,把它綁定到端口0是可行的,它意味著"any port"("任意端口")。由于一個套接字無法真正的被綁定到系統上的所有端口,那么在這種情況下系統將不得不選擇一個具體的端口號。源地址使用類似的通配符,也就是"0.0.0.0" 。和端口不同的是,一個套接字可以被綁定到任意地址,這里指本地網絡接口的所有地址。由于socket無法在連接的時候同時綁定到所有源IP地址,因此當接下來有一個連接過來的時候,系統將不得不挑選一個源IP地址。考慮到目的地址和路由表中的路由信息,系統將會選擇一個合適的源地址,并將任意地址替換為一個選定的地址作為源地址。
默認情況下,任意兩個socket都無法綁定到相同的源IP地址和源端口(即源地址和源端口號均相同)。只要源端口號不相同,那么源地址實際上沒什么關系。將socket A綁定到地址A和端口X (A:X),socket B綁定到地址B和端口Y (B:Y),只要X != Y,那么這種綁定都是可行的。然而當X==Y的時候只要A != B,這種綁定方式也仍然可行。記住:一個socket可能綁定到本地"any address"。例如一個socket綁定為 0.0.0.0:21,那么它同時綁定了所有的本地地址,在這種情況下,不論其它的socket選擇什么特定的IP地址,它們都無法綁定到21端口,因為0.0.0.0和所有的本地地址都會沖突。
SO_REUSEADDR
如果在綁定一個socket之前設置了SO_REUSEADDR,除非兩個socket綁定的源地址和端口號都一樣,那么這兩個綁定都是可行的。也許你會疑惑這跟之前的有什么不一樣?關鍵是SO_REUSEADDR改變了在處理源地址沖突時對通配地址的處理方式。
當沒有設置SO_REUSEADDR的時候,socket A先綁定到0.0.0.0:21,然后socket B綁定到192.168.0.1:21的時候將會失敗(EADDRINUSE錯誤),因為0.0.0.0意味著"任意本地IP地址”,也就是"所有本地IP地址“,因此包括192.168.0.1在內的所有IP地址都被認為是已經使用了。但是在設置SO_REUSEADDR之后socket B的綁定將會成功,因為0.0.0.0和192.168.0.1事實上不是同一個IP地址,一個是代表所有地址的通配地址,另一個是一個具體的地址。注意上面的表述對于socket A和socket B的綁定順序是無關的,沒有設置SO_REUSEADDR,它們將失敗,設置了SO_REUSEADDR,它將成功。
下面給出了一個表格列出了所有的可能組合:
?
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,然而當你開始使用地址重用,你可能會碰到奇怪的情況:connect()失敗返回同樣的錯誤EADDRINUSE。怎么會出現這種情況了? 一個遠端地址畢竟是connect添加到socket上的,怎么會已經被使用了? 將多個socket連接到相同的遠端地址從來沒有出現過這樣的情況,這是為什么了?? ? ?正如我在開頭說過的,一個連接是被一個五元組定義的。同樣我也說了任意兩個連接的五元組不能完全一樣,因為這樣的話內核就沒辦法區分這兩個連接了。然而,在地址重用的情況下,你可以把同協議的兩個socket綁定到完全相同的源地址和源端口,這意味著五元組中已經有三個元素相同了(協議,源地址,源端口)。如果你嘗試將這些socket連接到同樣的目的地址和目的端口,你就創建了兩個完全相同的連接。這是不行的,至少對TCP不行(UDP實際上沒有真實的連接)。如果數據到達這兩個連接中的任何一個,那么系統將無法區分數據到底屬于誰。因此當源地址和源端口相同時,目的地址或者目的端口必須不同,否則內核無法進行區分,這種情況下,connect()將在第二個socket嘗試連接時返回EADDRINUSE。 ?Linux 3.9加入了SO_REUSEPORT。這個選項允許多個socket(TCP or UDP)不管是監聽socket還是非監聽socket只要都在綁定之前都設置了它,那么就可以綁定到完全相同的地址和端口。為了阻止"port 劫持"有一個特別的限制:所有希望共享源地址和端口的socket都必須擁有相同的有效用戶id。因此一個用戶就不能從另一個用戶那里"偷取"端口。另外,內核在處理SO_REUSEPORT socket的時候使用了其它系統上沒有用到的"特別魔法":對于UDP socket,內核嘗試平均的轉發數據報,對于TCP監聽socket,內核嘗試將新的客戶連接請求(由accept返回)平均的交給共享同一地址和端口的socket(監聽socket)。這意味著在其他系統上socket收到一個數據報或連接請求或多或少是隨機的,但是linux嘗試優化分配。例如:一個簡單的服務器程序的多個實例可以使用SO_REUSEPORT socket實現一個簡單的負載均衡,因為內核已經把復制的分配都做了。 2015年12月15日00:34:22
總結
以上是生活随笔為你收集整理的SO_REUSEADDR SO_REUSEPORT 解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深度解析PHP数组函数array_chu
- 下一篇: [bzoj1187][HNOI2007]