前些天,與另外一個項目組的同事聊天的時候,談到他遇到的一個有意思的BUG。在window上啟動服務器,然后客戶端連接的時候收到一些奇怪的消息,查證了,原來是他自己的另一個工具也在相同的地址上監聽,客戶端連接到了后面這個工具程序上。我問他,是相同的IP和端口?他說是的,因為服務器代碼和工具程序都設置了SO_REUSEADDR這個socket選項,所以可以在同樣的地址上監聽。
可是,在我的認知里面, SO_REUSEADDR這個選項並不是說讓兩個程序在相同地址(相同的IP 和 端口)上監聽,而是說可以讓處於time_wait狀態的socket可以快速復用,搜了一下,看到的這篇文章,也是這么說的:
SO_REUSEADDR allows your server to bind to an address which is in a TIME_WAIT state. It does not allow more than one server to bind to the same address.
看了一下Linux manual,關於這個選項是這么描述的:
SO_REUSEADDR Indicates that the rules used in validating addresses supplied in a bind(2) call should allow reuse of local addresses. For AF_INET sockets this means that a socket may bind, except when there is an active listening socket bound to the address. When the listening socket is bound to INADDR_ANY with a specific port then it is not possible to bind to this port for any local address. Argument is an integer boolean flag.
manual並沒有提到time_wait的事情,但是明確指出,如果一個socket處於listen狀態,那么同樣的端口(port)是不能再次被綁定的(binding),不能binding,自然也不能再次listen,因此是不可能兩個程序在相同的地址(IP PORT)上監聽的。
於是自己用python在寫了一個小的測試程序:
服務端代碼:

1 # -*- coding: utf-8 -*- 2 import socket, sys 3 import time 4 5 def main(): 6 HOST, PORT = sys.argv[1], 8888 7 8 listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 9 listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 10 # print listen_socket.getsockopt(socket.SOL_SOCKET, socket.SO_EXECLUSIVEADDRUS) 11 12 listen_socket.bind((HOST, PORT)) 13 listen_socket.listen(10) 14 15 print 'Serving on host %s port %s ...' %(HOST, PORT) 16 while True: 17 client_connection, client_address = listen_socket.accept() 18 request = client_connection.recv(1024) 19 print 'client ', request 20 21 for i in range(5): 22 http_response = """\ 23 hello 24 """ 25 client_connection.sendall(http_response) 26 time.sleep(3) 27 client_connection.close() 28 29 if __name__ == '__main__': 30 main()
客戶端代碼:

1 import socket, sys 2 3 def main(): 4 server_address = ("localhost" if len(sys.argv) == 1 else sys.argv[1],8888) 5 s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 6 s.connect(server_address) 7 print s.getpeername() 8 s.send('I AM CLIENT') 9 while True: 10 data = s.recv(1024) 11 print " %s received %s" % (s.getpeername(),data) 12 if not data: 13 print "closing socket ",s.getpeername() 14 s.close() 15 16 if __name__ == '__main__': 17 main()
服務端代碼設置了SO_REUSEADDR,在Linux下, 確實不能在相同的地址(IP, Port)上監聽, 但是在windows上,卻又是可以的。於是想到,這個選項可能與平台相關。
平台差異性
本文記錄一下這個問答的要點,並用上面的小程序在各個平台(Linux, Mac, Windows)上進行測試。注意,本文只關注TCP、單播,事實上原問答還包括UDP、多播知識,感興趣的讀者可以自行閱讀。
第零:一條tcp連接是一個五元祖: {<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}
第一:SO_REUSEPORT和SO_REUSEADDR在不同的操作系統上行為是不一樣的
第二:默認情況下,任意兩個socket都無法綁定到相同的源IP地址和源端口, 0.0.0.0 (即INADDR_ANY )和所有其他地址沖突
第三:BSD系統下
SO_REUSEADDR 使得0.0.0.0 與 其他地址不沖突
SO_REUSEPORT允許你將多個socket綁定到相同的地址和端口, 但第一個啟動的socket必須設置SO_REUSEPORT
第四:MacOS IOS 表現同 BSD
第五:Linux
SO_REUSEADDR 只要有socket處於listen狀態, 就不能在同樣的地址和端口上listen, 0.0.0.0 與其他所有地址沖突
只要監聽前設置了SO_REUSEPORT(在Linux3.9版本之后可用) ,就可以在相同的(ip port)上監聽
對於SO_REUSEPORT:為了阻止"port 劫持"(Port hijacking)有一個特別的限制,所有希望共享源地址和端口的socket都必須擁有相同的有效用戶id(effective user ID);對於TCP監聽socket,內核嘗試將新的客戶連接請求(由accept返回)平均的交給共享同一地址和端口的socket(監聽socket)
第六:Android同Linux
第七:Windows
只有SO_REUSEADDR選項,沒有SO_REUSEPORT。
設置SO_REUSEADDR 等價於BSD上設定了SO_REUSEPORT和SO_REUSEADDR,而且不管之前的端口是否設定了SO_REUSEADDR(存疑)
上述選項存在風險:因為允許一個應用程序從別的應用程序上"偷取"已連接的端口。因此在windows上加入了另一個socket選項: SO_EXECLUSIVEADDRUSE。設置了SO_EXECLUSIVEADDRUSE的socket確保一旦綁定成功,那么被綁定的源端口和地址就只屬於這一個socket,其它的socket不能綁定,甚至他們使用了SO_REUSEADDR也沒用。
測試
在后文涉及到的三個平台(Linux 、MacOS、Windows),都涉及到三個IP:127.0.0.1, 0.0.0.0,10.0.0.x(局域網IP)。使用的腳本如上(tcp_server.py, tcp_client.py),運行的時候需要簡單修改tcp_server.py中第9、10行的注釋,以便測試不同選項下的效果。
MAC
由於沒有BSD系統,而且前文提到MacOS和BSD系統的表現是一樣的,因此在這里實在MAC上測試
在不使用SO_REUSEADDR (此時未使用SO_REUSEPORT)時:
注意:first指第一條監聽的socket,second指第二條希望在同樣的端口(port)上監聽的連接。兼容指第二條連接可以成功監聽,不兼容則指第二條連接不能成功監聽。下同
在使用SO_REUSEADDR(此時未使用SO_REUSEPORT)時:
在使用SO_REUSEADDR情況下,如果第一個scoket在0.0.0.0上監聽,第二個scoket在127.0.0.1上監聽。那么客戶端使用127.0.0.1連接的時候會連接到第二個socket;使用10.0.0.x則會連接到第一個socket
使用SO_REUSEPORT(同時使用了SO_REUSEADDR):
如果兩個socket都在127.0.0.1上監聽,客戶端也通過127.0.0.1去連接,那么客戶端連接都會發被第二個socket accept, 筆者並發實驗了幾十次都是這樣, 但並沒有找到明確的官方文檔說明是否是這樣。
Linux


從上面兩個測試可以看到,在linux下,是否使用SO_REUSEADDR並不影響兩個socket的監聽
使用SO_REUSEPORT(同時使用了SO_REUSEADDR):
如果兩個socket都在127.0.0.1上監聽,客戶端也通過127.0.0.1去連接, 那么客戶端連接會被操作系統分發到兩個socket上,具體如下
客戶端並發10次連接: for ((a=1;a<=10;a++)) ; do (python tcp_client.py 127.0.0.1 &); done
第一個socket accept了六次, 第二個socket accept了10次。
Windows
前面已經提到,windows下面只有SO_REUSEADDR選項,但其功能類似bsd系統下的SO_REUSEADDR與SO_REUSEPORT
在不使用SO_REUSEADDR時:

比如都在127.0.0.1 上監聽時,第二個socket會報錯: socket.error: [Errno 10048] 通常每個套接字地址(協議/網絡地址/端口)
使用SO_REUSEADDR時:

上面也提到,如果第一個socket使用了SO_EXECLUSIVEADDRUSE選項,那么第二個連接即使使用了SO_REUSEADDR也無濟於事,那么是否SO_EXECLUSIVEADDRUSE是默認開啟的呢?但是在Python2.7中,socket並沒有這個屬性
查了一下MSDN,有附圖清晰了說明了在window下SO_REUSEADDR與SO_EXECLUSIVEADDRUSE的關系,如下:
但為什么使用Python的時候 效果不一樣呢,這個就沒細究了
總結
本文測試了一下socket中SO_REUSEADDR與SO_REUSEPORT在各個平台下的差異性,一些結論只是實驗結果,並沒有查到官方權威定論,如果有差錯,還請指正!
references
http://www.unixguide.net/network/socketfaq/4.11.shtml
http://man7.org/linux/man-pages/man7/socket.7.html
http://blog.chinaunix.net/uid-28587158-id-4006500.html
https://msdn.microsoft.com/en-us/library/windows/desktop/cc150667(v=vs.85).aspx