对于 TCP 通信中关闭连接时需要的四次握手,最值得一说的就是 TIME_WAIT 状态。
在阅读相关资料和实践的基础上,我对这个状态的总结是
1. 仅在主动关闭一段产生;
2. 设置 SO_REUSEADDR 选项需要在 bind 之前进行。
以下分别从客户端和服务器端来分析。
从客户端的角度来看。如果采用的是长连接,那么根本就不会有 close 的行为;如果采用的是短连接,那么重新 connect 时会临时分配随机端口,不会与之前处于 TIME_WAIT 端口冲突。
举例说明,在服务器端开 10000 的端口号,观察不同阶段的 socket 状态。服务器和客户端部署在同一台机器上,以下部分以 LOCAL 代替实际的终端输出 "localhost.localdomain"
服务器启动完毕后 :
LOCAL:10000 *:* LISTEN
一个客户端实例连接成功后 :
LOCAL:10000 *:* LISTEN
LOCAL:10000 LOCAL:45358 ESTABLISHED
LOCAL:45358 LOCAL:10000 ESTABLISHED
该客户端实例关闭成功后 :
LOCAL:10000 *:* LISTEN
LOCAL:45358 LOCAL:10000 TIME_WAIT // 服务器端进入 CLOSE 状态后已成功关闭
客户端实例再次连接成功后 :
LOCAL:10000 *:* LISTEN
LOCAL:10000 LOCAL:45359 ESTABLISHED
LOCAL:45358 LOCAL:10000 TIME_WAIT // 这个连接仍在存在
LOCAL:45359 LOCAL:10000 ESTABLISHED
可见,原有的 45358 端口的 TIME_WAIT 不影响 45359 端口连接实例的创建。
从负面影响来看,客户端的 TIME_WAIT 会占用客户端的 fd ,处于 TIME_WAIT 状态的 fd 过多,以至于超出系统的 limit 限制,会导致无法再建立新的连接。
从服务器端角度来看。如果服务器主动关闭,情况就有些复杂。
服务器启动完毕后 :
LOCAL:10000 *:* LISTEN
一个客户端实例连接成功后 :
LOCAL:10000 *:* LISTEN
LOCAL:10000 LOCAL:45360 ESTABLISHED
LOCAL:45360 LOCAL:10000 ESTABLISHED
服务器关闭后 :
LOCAL:45360 LOCAL:10000 TIME_WAIT // 客户端进入 CLOSE 状态后已成功关闭
服务器再次启动 :
unable to bind address:Address already in use
由于服务器的启动并不是绑定临时端口,而是仍要建立在固定的 10000 端口上,此时 10000 端口处于 TIME_WAIT 的状态。到时 bind 无法正常进行。
解决该问题就需要 SO_REUSEADDR 参数,而该参数的设置很显然要在 bind 之前。 SO_REUSEADDR 官方解释是:允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将该端口用作他们的本地端口的连接仍存在。设置该参数后。
服务器再次启动 :
LOCAL:10000 *:* LISTEN
LOCAL:10000 LOCAL:45360 TIME_WAIT //10000 端口被复用
一个客户端实例连接成功后 :
LOCAL:10000 *:* LISTEN
LOCAL:10000 LOCAL:45360 TIME_WAIT
LOCAL:45361 LOCAL:10000 ESTABLISHED
LOCAL:10000 LOCAL:45361 ESTABLISHED
TIME_WAIT 的值与系统实现相关。在我的机器上测试为 60s 。那么启动时间小于 60s 的服务示例都要面临这个端口占用不能重启的问题。
SO_REUSEADDR 其他用途如可以允许同时启动多个 IP(IP 别名 ) 的服务器绑定在相同的端口上。但这也带来一些安全问题,恶意的服务器可以绑定同样的端口,截获部分请求。
参考文献:《 UNIX 网络编程》第一卷