引言: 前两天朋友公司的服务器垮掉了, 最后查出的原因是发现大量的 time_wait 网络状态. 被问起来 time_wait 是什么, 当时就简单的给解释了两句, 后来想想正好博客没有特别好的话题, 拿来写一下也很不错.
简单的描述产生原因
因为本文较长, 如果没有耐心的可以简单了解一下, 有耐心的请阅读全文.
TCP 是面向连接的, 即使不知道具体的过程, 也都知道 TCP 的三次握手, 四次挥手. 挥手也就是关闭连接, 关闭连接的时候, 主动关闭的一方在接收到被动关闭方的回应前, 处于 time_wait 状态, 并保持一段时间. close_wait 是被动关闭方接收到关闭链接请求后所处的状态.
查看状态的代码:
$ netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
>结果
- $ netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
- CLOSE_WAIT 134
- ESTABLISHED 245 // 已经建立连接, 正在传输状态
- TIME_WAIT 3
TCP 的三次握手和四次挥手的状态转换
要说清楚这个状态, 需要了解一定的 TCP 知识, 我们在这里简单介绍一下
三次握手建立连接
TCP 必须经过三次握手来建立可靠连接才能彼此传输数数据. 假设请求方为客户端与服务端建立 TCP 连接
在握手前, TCP 服务器进程已经创建传输控制块 TCB(传输控制模块, 记录 TCP
运行过程中的变量), 准备接受客户连接请求, 此时服务器进入 LISTEN(监听)状态;
第一次握手: TCP 客户端先创建传输控制块 TCB, 然后向服务器发出连接请求报文, 这时报文首部中的同步位 SYN=1, 初始序列号 seq=x ,TCP 客户端进程进入了 SYN_SENT(同步已发送)状态. TCP 协议中 SYN 报文段 (SYN=1 的报文段) 不携带数据, 但需要消耗掉一个序号(序号每次报文要递增).
第二次握手: 服务端在收到客户端发过来的 SYN 请求报文后, 如果同意连接, 则发出确认报文. 确认报文 ACK=1,SYN=1, 确认号是 ack=x+1(这个 ack 是确认号, 大写的 ACK 是标志位, 文章最末尾有各个标志位的简单介绍), 同时自己也初始化一个序列号 seq=y, 此时, 服务器 TCP 进程进入了 SYN_RCVD(收到同步)状态. 这个 SYN 同样不携带数据, 且消耗一个序号.
第三次握手: 客户端口收到服务端发过来的 SYN 和 ACK 确认报文后, 还要向服务器给出确认, 确认报文的 ACK=1,ack=y+1, 自己的序列号 seq=x+1, 此时, 客户端状态由原来的 SYN_SENT 状态变为 ESTABLISHED(连接已确认建立).TCP 协议中 ACK 报文可以携带数据, 携带数据的时候消耗序号
服务端收到报文后, 由原来的 SYN_RCVD 变为 ESTABLISHED, 双方开始通信
三次握手有一个很形象的比喻
// 客户端和服务端打电话
客户端: 服务端你能听见吗
服务端: 哎, 能听见, 你能听见我说话吗
客户端: 能听见, 有点事找你(如果没有这一步, 服务端并不知道客户端是否能听见)
四次挥手关闭连接
先来解释一个 TCP 连接到底是谁关闭的, 一般来说, 谁断开都可以, 对于我们常用的 http 来说, 一般情况下是这样的
对于 http1.0 协议
如果响应头中有 content-length 头, 则以 content-length 的长度就可以知道 body 的长度了, 客户端在接收 body 时, 就可以依照这个长度来接收数据, 接收完后, 就表示这个请求完成了, 客户端请求断开.
如果没有 content-length 头, 则客户端会一直接收数据, 直到服务端主动断开连接, 才表示 body 接收完了.
对于 http1.1 协议
如果响应头中的 Transfer-encoding 为 chunked 传输, 则表示 body 是流式输出, body 会被分成多个块, 每块的开始会标识出当前块的长度, 此时, body 不需要通过长度来指定, 客户端主动请求断开.
如果是非 chunked 传输, 而且有 content-length, 则按照 content-length 来接收数据, 接收完成后客户端请求断开连接.
如果是非 chunked, 并且没有 content-length, 则客户端接收数据, 直到服务端主动断开连接.
下面说四次挥手的过程
我们假设客户端是请求关闭连接的一方, 服务端为被动关闭
第一次挥手: 客户端进程发请求关闭 (或称释放) 连接报文, 并且停止发送数据. 数据报文首部, FIN=1, 其序列号为 seq=u(等于前面已经传送过来的数据的最后一个字节的序号加 1), 此时, 客户端进入 FIN_WAIT_1(终止等待 1)状态. TCP 规定, FIN 报文段即使不携带数据, 也要消耗一个序号.
第二次挥手: 服务器收到请求关闭报文, 发出确认报文, ACK=1,ack=u+1, 并且带上自己的序列号 seq=v, 此时, 服务端就进入了 CLOSE_WAIT(关闭等待)状态. TCP 服务器通知高层的应用进程, 客户端向服务器的方向就释放了, 这时候处于半关闭状态, 即客户端已经没有数据要发送了, 但是服务器若发送数据, 客户端依然要接受. 这个状态还要持续一段时间, 也就是整个 CLOSE_WAIT 状态持续的时间.
客户端收到服务器的确认请求后, 此时, 客户端就进入 FIN_WAIT_2(终止等待 2)状态, 等待服务器发送关闭连接报文(在这之前还需要接受服务器发送的最后的数据).
第三次挥手: 服务器将最后的数据发送完毕后 (或者没有需要发送的数据), 就向客户端发送关闭连接报文, FIN=1,ack=u+1. 由于服务器很可能又发送了一些数据, 所以假定此时的序列号为 seq=w, 此时, 服务器就进入了 LAST_ACK(最后确认) 状态, 等待客户端的确认.
第四次挥手: 客户端收到服务器的连接释放报文后, 必须发出确认, ACK=1,ack=w+1, 而自己的序列号是 seq=u+1, 此时, 客户端就进入了 TIME_WAIT(时间等待)状态. 注意此时 TCP 连接还没有释放, 必须经过 2MSL(最大报文段寿命)的时间, 当客户端撤销相应的 TCB 后, 才进入 CLOSED 状态.
服务器只要收到了客户端发出的确认, 立即进入 CLOSED 状态. 撤销 TCB 后, 就结束了这次的 TCP 连接. 可以看到, 服务器 (被动断开方) 结束 TCP 连接的时间要比客户端 (主动断开方) 早一些.
// 客户端和服务端打电话
客户端: 我的事说完了, 有事情你说我听着, 没什么事挂了哈
服务端: 好的知道了,(好的, 我还要和你说个事, 你妈叫你回家吃饭)
服务端: 行了挂了吧
客户端: 哦, 知道了, 那我挂了
关于为什么要等待 2MSL:
MSL(Maximum Segment Lifetime), 这是 TCP 对 TCP Segment 生存时间的限制.
客户端发送最后一个 ACK 后, 不能确保服务端一定能收到, 假如 ACK 没有被服务端收到, 超时后服务端重新进行第三次挥手, 这时候如果 A 还在等待, 又收到第三次挥手的 FIN 消息, 证明 ACK 没有成功到达, 这个时间至少是: 服务端的超时时间 + FIN 的传输时间, 为了保证可靠, 采用更加保守的等待时间 2MSL.
如果客户端此时没有在等待状态直接 CLOSED, 服务端超时后发送 FIN 消息到客户端, 客户端表示并不知道这数据包是干什么的, 所以响应一个 RST(用来异常的关闭连接, 请自行了解), 如果客户端有一个和服务端的新连接在这个端口上建立. 这将可能导致后面建立的连接受到影响, TCP 是可靠的连接, 所以是不希望这种不靠谱的事情出现的. 这种错误可以比喻为
// 客户端和服务端打电话
客户端: 我的事说完了, 有事情你说我听着, 没什么事挂了哈
服务端: 好的知道了,(好的, 我还要和你说个事, 你妈叫你回家吃饭)
服务端: 行了挂了吧
客户端: 哦, 知道了, 那我挂了
(上面这一句因为信号不好等原因服务端没收到客户端就挂了电话)后面有事情又拨通了服务端电话(建立了新连接)
服务端: 挂了吧
客户端: 怎么回事, 蛇精病啊, 刚通了就让我挂
TCP 报文首部标志位
关于报文首部格式, 网上有很多, 而且大部分都是以正确的, 下面只介绍和本文有关的,
标志位:
SYN(synchronous 建立联机)
在连接建立时用来同步序号, 当 SYN=1 而 ACK=0 时, 表明这是一个连接请求报文段. 对方若同意时, 则应在响应的报文段中使 SYN=1 和 ACK=1, 因此, SYN 置 1 就表示这是一个连接请求或连接接受报文.
ACK(acknowledgement 确认)
仅当 ACK=1 时确认号字段才有效, TCP 规定, 连接建立后所有传送的报文段都必须把 ACK 置 1.
PSH(push 传送)
当两个应用进程进行交互式的通信时, 有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应. 在这种情况下, TCP 就可以使用推送操作.
FIN(finish 结束)
用来释放一个连接, 当 FIN=1 时, 表示此报文段的发送方的数据已发送完毕, 并要求释放运输连接.
RST(reset 重置)
当 RST=1 时, 表明 TCP 连接中出现严重错误, 必须释放连接, 然后再重新建立运输连接.
URG(urgent 紧急)
当 URG=1 时, 表明紧急字段有效, 告诉系统此报文中有紧急数据, 应尽快传送.
其他:
Sequence number(顺序号码)
seq 是发送的数据包本身的序列号;
Acknowledge number(确认号码)
ack 是对收到的数据包的确认, 值是等待接收的数据包的序列号. 即期望对方继续发送的那个数据包的序列号.
来源: https://www.cnblogs.com/vinter/p/9045639.html