我们知道,一个 TCP 连接是通过四元组(源地址、源端口、目的地址、目的端口)来唯一确定的,如果每次 Telnet 客户端使用的本地端口都不同,就不会和已有的四元组冲突,也就不会有 TIME_WAIT 的新旧连接化身冲突的问题。
事实上,即使在很小的概率下,客户端 Telnet 使用了相同的端口,从而造成了新连接和旧连接的四元组相同,在现代 Linux 操作系统下,也不会有什么大的问题,原因是现代 Linux 操作系统对此进行了一些优化。
第一种优化是新连接 SYN 告知的初始序列号,一定比 TIME_WAIT 老连接的末序列号大,这样通过序列号就可以区别出新老连接。
第二种优化是开启了 tcp_timestamps,使得新连接的时间戳比老连接的时间戳大,这样通过时间戳也可以区别出新老连接。
在这样的优化之下,一个 TIME_WAIT 的 TCP 连接可以忽略掉旧连接,重新被新的连接所使用。
这就是重用套接字选项,通过给套接字配置可重用属性,告诉操作系统内核,这样的 TCP 连接完全可以复用 TIME_WAIT 状态的连接。代码片段已经放在文章中了:
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
SO_REUSEADDR 套接字选项,允许启动绑定在一个端口,即使之前存在一个和该端口一样的连接。前面的例子已经表明,在默认情况下,服务器端历经创建 socket、bind 和 listen 重启时,如果试图绑定到一个现有连接上的端口,bind 操作会失败,但是如果我们在创建 socket 和 bind 之间,使用上面的代码片段设置 SO_REUSEADDR 套接字选项,情况就会不同。
SO_REUSEADDR 套接字选项还有一个作用,那就是本机服务器如果有多个地址,可以在不同地址上使用相同的端口提供服务。
比如,一台服务器有 192.168.1.101 和 10.10.2.102 连个地址,我们可以在这台机器上启动三个不同的 HTTP 服务,第一个以本地通配地址 ANY 和端口 80 启动;第二个以 192.168.101 和端口 80 启动;第三个以 10.10.2.102 和端口 80 启动。
这样目的地址为 192.168.101,目的端口为 80 的连接请求会被发往第二个服务;目的地址为 10.10.2.102,目的端口为 80 的连接请求会被发往第三个服务;目的端口为 80 的所有其他连接请求被发往第一个服务。
我们必须给这三个服务设置 SO_REUSEADDR 套接字选项,否则第二个和第三个服务调用 bind 绑定到 80 端口时会出错。
最佳实践
这里的最佳实践可以总结成一句话: 服务器端程序,都应该设置 SO_REUSEADDR 套接字选项,以便服务端程序可以在极短时间内复用同一个端口启动。
有些人可能觉得这不是安全的。其实,单独重用一个套接字不会有任何问题。我在前面已经讲过,TCP 连接是通过四元组唯一区分的,只要客户端不使用相同的源端口,连接服务器是没有问题的,即使使用了相同的端口,根据序列号或者时间戳,也是可以区分出新旧连接的。
而且,TCP 的机制绝对不允许在相同的地址和端口上绑定不同的服务器,即使我们设置 SO_REUSEADDR 套接字选项,也不可能在 ANY 通配符地址下和端口 9527 上重复启动两个服务器实例。如果我们启动第二个服务器实例,不出所料会得到 Address already in use 的报错,即使当前还没有任何一条有效 TCP 连接产生。
tcp_tw_reuse 和SO_REUSEADDR
这两个东西一点关系也没有。
tcp_tw_reuse 是内核选项,主要用在连接的发起方。TIME_WAIT 状态的连接创建时间超过 1 秒后,新的连接才可以被复用,注意,这里是连接的发起方;
SO_REUSEADDR 是用户态的选项,SO_REUSEADDR 选项用来告诉操作系统内核,如果端口已被占用,但是 TCP 连接状态位于 TIME_WAIT ,可以重用端口。如果端口忙,而 TCP 处于其他状态,重用端口时依旧得到“Address already in use”的错误信息。注意,这里一般都是连接的服务方。
总结
今天我们分析了“Address already in use”产生的原因和解决方法。你只要记住一句话,在所有 TCP 服务器程序中,调用 bind 之前请设置 SO_REUSEADDR 套接字选项。这不会产生危害,相反,它会帮助我们在很快时间内重启服务端程序,而这一点恰恰是很多场景所需要的。
网友评论