我有一个使用 TCP 的服务器程序。
有时,我需要重新启动程序以进行更新或出于任何其他原因,但当我这样做时,程序会关闭服务器端口,然后在重新启动后,它会尝试在该端口上创建一个新的侦听器,但似乎只有在端口先前关闭后约一分钟(约 63 秒)才能成功完成此操作。为什么会这样?有什么方法可以解决它吗?
该程序在 RamNode 的 Ubuntu 18.04 上运行。
也许我可以在操作系统中更改任何设置,或者可能是 RamNode 的事情,等等?
答案1
对于处理 TCP 套接字的应用程序,最有可能是因为TIME-WAIT
TCP协议的状态:
TIME-WAIT
- 表示等待足够的时间以确保远程 TCP 收到其连接终止请求的确认。
当关闭上一个连接时,两端中的一端(取决于哪一端发起关闭)将保持 TCP 套接字处于TIME-WAIT
状态,以防止相同的 TCP 连接在一段时间内存在。当这在客户端时,这不是问题,因为端口是一个随机的临时端口。当它位于具有固定端口的服务器端并且服务器重新启动时,这会阻止服务器再次绑定到同一端口。
下面是使用的示例socat
重现该行为。
术语1:
socat tcp4-listen:5555 -
期限2:
socat tcp4:127.0.0.1:5555 -
现在连接已建立,只需中断 term1 的socat
命令即可。如果立即重新启动,您将获得 1 分钟的延迟:
术语1:
$ socat tcp4-listen:5555 -
2021/07/03 21:32:09 socat[320904] E bind(5, {AF=2 0.0.0.0:5555}, 16): Address already in use
防止此行为的功能(对于服务器尤其有用)是套接字选项 SO_REUSEADDR
:
SO_REUSEADDR
表示验证地址时使用的规则 绑定(2)呼叫应该允许重用本地地址。对于
AF_INET
套接字,这意味着套接字可以绑定,除非有活动侦听套接字绑定到该地址。当侦听套接字INADDR_ANY
与特定端口绑定时,则不可能将任何本地地址绑定到此端口。参数是一个整数布尔标志。
相反,当第一次开始聆听socat
时:
socat tcp4-listen:5555,reuseaddr -
TIME-WAIT
即使像以前一样在连接后中断,现在也可以立即重新启动,而无需使用此端口评估 TCP 连接(这包括处于状态的以前的 TCP 连接)SO_REUSEADDR
如果它在之前的运行中也使用,才能bind()
成功。
这是一个奇怪的例子(表明它不仅限于TIME-WAIT
州):
术语1:
socat tcp4:127.0.0.1:5555,bind=127.0.0.1:5555,reuseaddr -
(上述命令在同时的 TCP 启动中连接到自身,如RFC 793)
$ ss -tn sport == 5555
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 127.0.0.1:5555 127.0.0.1:5555
它不会阻止在端口 5555 之后创建监听套接字
期限2:
socat tcp4-listen:5555,reuseaddr
术语3:
$ ss -atnp sport == 5555
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 5 0.0.0.0:5555 0.0.0.0:* users:(("socat",pid=321093,fd=5))
ESTAB 0 0 127.0.0.1:5555 127.0.0.1:5555 users:(("socat",pid=321047,fd=5))
$ socat -d -d tcp4:127.0.0.1:5555 -
2021/07/03 22:16:27 socat[322125] N opening connection to AF=2 127.0.0.1:5555
2021/07/03 22:16:27 socat[322125] N successfully connected from local address AF=2 127.0.0.1:52218
2021/07/03 22:16:27 socat[322125] N reading from and writing to stdio
2021/07/03 22:16:27 socat[322125] N starting data transfer loop with FDs [5,5] and [0,1]
回到问题:它在你的应用程序上。
任何一个:
- 更正应用程序以
setsockopt()
在将要监听的套接字上使用,以SO_REUSEADDR
允许bind()
成功,尽管使用该端口的连接也存在(TIME-WAIT
算作这样的连接,并且可能是实际情况下的唯一原因)。
或者如果你不能,在动态链接上(使用库) 应用:
- 使用包装器(使用
LD_PRELOAD
机制)来强制您的应用程序执行此操作。这是我的问答答案中的一个类似(但不相同)的示例,SO_REUSEPORT
可以轻松改编为SO_REUSEADDR
:sshd“监听所有接口”行为,OSX 和 Ubuntu