1.何为TIME_WAIT
time_wait实际上是TCP关闭连接4次挥手时的一种状态
TIME_WAIT is a socket state during TCP connection termination. It represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request.
它实际上是为了确保最后一次ACK包的发送成功(如果没送达对端会再发一遍FIN,实际上每一次的wait状态包括fin_wait、close_wait和last ack wait都是为了确保自己发送的数据包不丢失),从上图可知,time wait状态只会发生在发起连接关闭的一方。
那这个等待时间是多长?最长为2MSL(maxmum sgement lifetime),在LINUX系统中,由硬编码字段TCP_TIMEWAIT_LEN确定
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME- WAIT state, about 60 seconds */
一般是60秒,过了之后就进入closed关闭状态
1.1time wait的重要性
- 确保ACK到达
- 让旧连接的重复分节在网络中消失。
TCP的分节可能由于路由器异常发生“迷途”,在迷途时又触发发送端的超时重传机制,因而重传报文,迷途的分节在路由器修复后会被送到目的地。但是当迷途的分节还没到达最终目的地时,我们关闭一个TCP连接又立马重新建立一个相同端口ip的TCP,此时如果迷途的分节经过一段时间后也到达,那么上一次业务就会对这次业务造成影响(如果没有time wait,新的完全一样连接中可能出现上一次连接的报文),time wait的目的就是让两个方向上在网络中的迷途报文全都自然消失,确保再出现的分组一定是新连接产生的
1.2查看time wait状态
demo还是之前的网络协议概要 中的server和client,在启动后在客户端输入STOP关闭连接,之后查看连接状态
netstat -alepn

这个程序目前有个buf就是客户端没写关闭连接,服务端在收到STOP后发起连接关闭,因此服务端进入time wait状态

通过tcpdump抓包发现确实是server发起关闭连接请求,之后进行了4次握手
1.3TIME_WAIT危害
- 内存资源占用
- 端口占用。一个TCP连接至少消耗一个本地端口,如果TIME_WAIT状态过多,就会导致无法创建新连接

我们竟然惊讶的发现有两个占用了8080端口,那我再开呢?

有三个占用了相同的端口,这与我们上文分析的端口占用不对呀,那肯定是它内部做了优化
1.4优化TIME_WAIT
- net.ipv4.tcp_max_tw_buckets 调低系统值,默认为18000,一旦time wait连接数超过,将所有time wait状态重置并打印警告信息,治标不治本,没解决迷途分节问题,上面情况也不是这种
- 调低 TCP_TIMEWAIT_LEN,重新编译系统。麻烦
- SO_LINGER 的设置。即设置调用close或shutdown关闭连接时的行为:
1.为0时默认
2 跳过4次挥手,即跳过time wait
3 调用close后阻塞直到数据发送出去
第二种为跨越time wait提供可能但十分危险 - net.ipv4.tcp_tw_reuse:更安全的设置。
Allow to reuse TIME-WAIT sockets for new connections when it is safe from protocol viewpoint. Default value is 0.It should not be changed without advice/request of technical experts.
即在安全可控范围内,复用处于time wait的套接字为新连接使用,复用意味着端口也复用,这不就是上面的例子
但是什么是可控:1.连接发起方;2.time wait超过1s(使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即net.ipv4.tcp_timestamps=1)
net.ipv4.tcp_tw_recycle是客户端和服务器端都可以复用,但是容易造成端口接收数据混乱(这两个参数的配置分别对应 /proc/sys/net/ipv4下的两个文件)
2次挥手关闭
我们常说的tcp流是双向的,这里的双向是指数据的写入方向和读出方向,TCP连接的关闭分以下两种情况:
- 优雅关闭: TCP连接关闭都是先关闭一个方向,此时另一个方向可以正常数据传输(这里代码例子意味着服务端主动发起关闭连接,无法再向客户端写入数据),但不会再有新报文到达不意味着连接已经完全关闭,很有可能情况是客户端正在对服务端最后发送的报文进行处理,如访问数据库,当完成这些操作后把结果通过套接字写给服务端,我们说这个套接字目前状态是“半关闭”(A让B关闭,但是B要先把手头事情做完再关闭)
对应c底层源码的调用就是
int shutdown(int sockfd, int howto)
- 粗暴关闭: 对套接字进行彻底释放直接关闭两个方向。对应c源码的调用就是
int close(int sockfd)
2.1GO的Con.Close是哪种关闭连接方法?
修改我们的demo代码,让服务端在收到客户端的包后休眠5s模拟数据库查询动作,客户端没怎么变,增加了主动关闭close方法
server.go
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
func main() {
lintener, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
for {
con, err := lintener.Accept()
check(err)
fmt.Println("con create from :", con.RemoteAddr())
go func() {
defer func() {
fmt.Println("bye :", con.RemoteAddr())
con.Close()
}()
for {
netData, err := bufio.NewReader(con).ReadString('\n')
check(err)
if strings.TrimSpace(string(netData)) == "STOP" {
fmt.Println("read EOF!exiting TCP server")
return
}
//write to con
fmt.Println("->", string(netData))
t := time.Now()
mytime := t.Format(time.RFC3339) + "\n"
//Service Blocking
time.Sleep(time.Second * 3)
con.Write([]byte(mytime))
}
}()
}
}
client.go
func main() {
con, err := net.Dial("tcp", ":8080")
check(err)
for {
reader := bufio.NewReader(os.Stdin)
fmt.Print(">>")
text, _ := reader.ReadString('\n')
fmt.Fprintf(con, text+"\n")
message, _ := bufio.NewReader(con).ReadString('\n')
fmt.Println("->:" + message)
if strings.TrimSpace(string(text)) == "STOP" {
fmt.Println("TCP client exiting")
con.Close()
break
}
}
}
启动并使用tcpdump进行网络检测
sudo tcpdump -i ol port 8080
客户端情况:在输入新数据包(we)(we加油啊今年夏季赛都垫底了,好歹是御三家),立即输入STOP调用close关闭

我们发现实际上是等到we的处理结果出来后再关闭连接,但是tcpdump的抓包结果很奇怪

他只抓到了3次挥手,少了一次(第二次)webcache->33164 ack 28的应答(是不是可能和第四次挥手合并发送?),并且他连接的发起关闭方还是服务端webcache,我们开始发愁,建立以下假设:
-
close是半关闭
我们交换一下代码逻辑
2.因为我们的代码write和close之间还隔着read方法,是不是read也会被阻塞?
image.png
结果还是不变image.png
但此时握手结束的发起方确实是客户端(注意观察seq和ack序号)tcpdump
这不就是我们所期望的优雅的关闭吗?我们的业务没有被落下,这里不得不感叹go设计的强大
但是在没读懂源码前我们还是得抱着不到黄河心不死的想法,打个问号先
网友评论