在编写远控工具DanceShell的shell连接部分,功能需求是当本次连接终止时,自动启动socket监听,依然是在此前的IP和端口进行监听,并且在监听前关闭socket连接:

1
2
clientSocket.shutdown(2)
clientSocket.close()

此时在Windows上运行并断开重复监听时是成功的:

image1

但是同样的项目代码,在Linux就会报错:

image2

抛出来这样的错误:

1
2
3
4
5
6
7
8
Traceback (most recent call last):
File "server.py", line 67, in <module>
main()
File "server.py", line 35, in main
server, clientSocket, clientAddress = connect(host, port)
File "server.py", line 19, in connect
server.bind((serverHost, serverPort))
OSError: [Errno 98] Address already in use

报错显示地址已经被使用,无法再次进行socket bind()绑定。

起初以为是仅仅终止了clientSocket而没有结束(关闭)serverSocket,于是在关闭clientSocket后又加入一行关闭serverSocket的代码:

1
2
3
clientSocket.shutdown(2)
clientSocket.close()
server.close()

Windows上同样可以,但是Linux上运行,关闭再次监听依然会报错。

当我以为仅仅是Windows和Linux不同操作系统上的socket存在差异时,查询资料得知,Windows和Linux上第一次关闭socket连接并不会立即释放地址和端口,也就是不会立即释放该socket(ip:port),而是处于time_wait的一个状态

关于该状态的含义,书籍《TCP/IP详解卷》中给出的解释是,由于TCP连接存在一个最大段生存期MSL(Maximum Segment Lifetime),它代表任何报文段在被丢弃前在网络中被允许存在的最大时长,而当连接断开时,为了防止可能存在于网络中的的数据包,在重新建立新连接后被当作新连接的数据报文,所以会设定一个time_wait,也称之为2MSL,即时长是MSL的2倍。所以在这2MSL的time_wait时间内,该地址端口不允许再次绑定使用

image3

Windows上time_wait的时间是30-300s,不同的版本有不同的默认值,所以不能够立即再次绑定此前的端口(socket),当遇到以下两种情况时,才会完全释放:

  • 1、程序结束
  • 2、time_wait结束

但是,由于在实际应用中,服务端同一服务所监听的端口通常是固定的,比如80、443等,如果服务端终止了已经建立连接的服务器进程,此时需要立刻恢复监听,而TCP处于time_wait状态,无法立刻再次监听相同端口,需要等待2MSL,这会大大降低服务器的效率,影响正常的服务。所以针对此问题,之后又提出了socket地址复用SO_REUSEADDR)。

SO_REUSEADDR地址复用就是为了绕过time_wait这个2MSL等待时间而设定的选项,所以它的作用范围仅仅是出于time_wait状态的socket。在bind()之前使用setsockopt()函数来设定socket参数,Python示例如下:

1
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

在服务器端加入该代码,代码块部分:

1
2
3
4
5
6
7
8
9
10
def connect(host, port):
serverHost = host
serverPort = port
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((serverHost, serverPort))
server.listen(5)
msg = " Server is Listening <%s:%s>\n Waiting……" % (serverHost, serverPort)
print(Colors.CYAN + "[*]" + Colors.END + msg)
clientSocket, clientAddress = server.accept()

此时再次在Linux上进行测试,成功监听

image4

除了SO_REUSEADDR,还有SO_REUSEPORT,具体的区别可以参考

https://www.jianshu.com/p/a23b7e8a4c6a

1
2
socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)

参考:

https://www.cnblogs.com/hqutcy/p/7142121.html

https://book.douban.com/reading/33303128/