Linux网络:传输层协议TCP(二)三次挥手四次握手详解
目录
一、TCP的连接管理机制
1.1三次握手
1.2四次挥手
二、理解 TIME_WAIT 状态
2.1解决TIME_WAIT 状态引起的 bind 失败的方法
三、理解CLOSE_WAIT状态
一、TCP的连接管理机制
在正常情况下, TCP 要经过三次握手建立连接, 四次挥手断开连接
1.1三次握手
三次握手顾名思义就是在刚开始建立连接时,一端向另一端发送SYN连接请求(一般时客户端向服务器),服务器收到后做出应答,也向客户端发送SYN连接请求顺便捎带ACK应答,客户端收到后再次向服务器发送ACK应答,至此三次握手完成,双方连接建立完毕。
前两次握手只要有一次没有成功那么连接都是无法建立的双方都是可以清楚的知道连接是没有建立的,那如何保证第三次握手的ACK对方收到了呢?
一般情况下如果前两次握手成功,第三次出现失败的概率很小,如果真的出现,比如客户端给服务器发的携带ACK应答的报头在中途丢包了,那么此时服务器就会进入超时等待,那么此时客户端会认为连接已经建立好了,可能就会直接进行数据报文的传输,此时服务器收到报文,但是对于服务器端,连接还未建立好,此时服务器就会对客户端做出应答,将报头中的RST置为1,表示重置。
此时就会进行重置,双方重新进行三次握手。所以RST主要就是解决连接问题。
所以为什么要三次握手?
1、因为建立连接维护连接是有成本的,如果连接只需要单次握手就能建立,那客户端疯狂的单方面给服务器发送连接最终就会导致服务器花费资源来维护大量连接,这种情况服务器是很容易被攻击的。而这种大量发送SYN的情况,我们也将其称为SYN洪水攻击。而三次握手我们可以发现,客户端也需要进行一次接收和一次应答,付出的代价和服务器是对等的,而在上图的逻辑中,服务器也就倒逼着客户端,服务器和客户端建立连接的前提是客户端先建立连接!所以三次握手虽然不能保证绝对的安全,但是起码可以避免遭受单机攻击就被搞挂掉,这也是为什么不一次两次握手的主要理由。
2、三次握手,client和server双方,都会有确定的一次收发,三次握手也是最小成本确认全双工的方式,确保双方OS是健康且愿意通信的,其实三次握手本质上也是4次握手,只不过中间两次被变成一次捎带应答了。
1.2四次挥手
一般情况下,依旧是客户端率先起手发送携带FIN的报头,表示断开连接的请求,此时是第一次挥手 ,当客户端发送携带FIN标志位的报头时 ,此时就表明用户层不会再有数据往发送缓冲区进行写入了,因为FIN往往就对应我们使用的close(fd),此时我们在代码中调用了close关闭了对应的文件描述符,那么我们就肯定无法再往对应的fd中去进行写入了。同样的对于服务器端也是一样的,所以双方发送FIN本质上也就是在用户层调用close(),去将对应缓冲区的数据冲刷干净。
而四次挥手,也是双方OS自动完成的(写到这里博主真的是有苦在心口难开啊,明明不需要博主去实现但还是要拿出100%的精力去学,因为唯有真正了解,才能运用自如啊!)。 而这里为什么不使用捎带应答将两次挥手变成一次呢?
因为与建立连接不同,建立连接时双方都是处于空闲,且目的都是与彼此建立连接,而断开时,客户端数据发送完毕调用close,而服务器此时不一定将所有的数据都完整传输给客户端了,所以无法直接无脑二合一,因此才要分为四次。而close也只是用户层的我们将文件描述符关闭,实际上,真正关闭也是等到对方的ACK以后才正式关闭,也可以调用shutdown来选择性关闭文件描述符的读/写功能。
三次握手与四次挥手就像一场凄美的爱情一样,在一起时总是迫不及待,轰轰烈烈,两步并一步奔向彼此,对未来充满向往,而最终因为现实也好,缘尽也罢,终会有一方先挥手作别,尽管再依依不舍,一次次的wait换来的还是双方的分离。
服务端状态转化:
• [CLOSED -> LISTEN] 服务器端调用 listen 后进入 LISTEN 状态, 等待客户端连接;
• [LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入 内核等待队列中, 并向客户端发送 SYN 确认报文.
• [SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入 ESTABLISHED 状态, 可以进行读写数据了.
• [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用 close), 服务 器会收到结束报文段, 服务器返回确认报文段并进入 CLOSE_WAIT;
• [CLOSE_WAIT -> LAST_ACK] 进入 CLOSE_WAIT 后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用 close 关闭连接时, 会向客户端发送 FIN, 此时服务器进入 LAST_ACK 状态, 等待最后一个 ACK 到来(这个 ACK 是客户 端确认收到了 FIN)
• [LAST_ACK -> CLOSED] 服务器收到了对 FIN 的 ACK, 彻底关闭连接.
客户端状态转化:
• [CLOSED -> SYN_SENT] 客户端调用 connect, 发送同步报文段;
• [SYN_SENT -> ESTABLISHED] connect 调用成功, 则进入 ESTABLISHED 状 态, 开始读写数据; • [ESTABLISHED -> FIN_WAIT_1] 客户端主动调用 close 时, 向服务器发送结 束报文段, 同时进入 FIN_WAIT_1;
• [FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入 FIN_WAIT_2, 开始等待服务器的结束报文段;
• [FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入 TIME_WAIT, 并发出 LAST_ACK;
• [TIME_WAIT -> CLOSED] 客户端要等待一个 2MSL(Max Segment Life, 报文 最大生存时间)的时间, 才会进入 CLOSED 状态.
二、理解 TIME_WAIT 状态
现在做一个测试,首先启动 server,然后启动 client,然后用 Ctrl-C 使 server 终止,这时马 上再运行 server, 结果是:
这是因为,虽然 server 的应用程序终止了,但 TCP 协议层的连接并没有完全断开,因此不 能再次监 听同样的server 端口. 我们用 netstat 命令查看一下:
• TCP 协议规定,主动关闭连接的一方要处于 TIME_ WAIT 状态,等待两个 MSL(maximum segment lifetime)的时间后才能回到 CLOSED 状态.
• 我们使用 Ctrl-C 终止了 server, 所以 server 是主动关闭连接的一方, 在 TIME_WAIT 期间仍然不能再次监听同样的 server 端口;
• MSL 在 RFC1122 中规定为两分钟,但是各操作系统的实现不同, 在 Centos7 上 默认配置的值是 60s;
• 可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 msl 的值;
想一想, 为什么是 TIME_WAIT 的时间是 2MSL?
• MSL 是 TCP 报文的最大生存时间, 因此 TIME_WAIT 持续存在 2MSL 的话
• 就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服 务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错 误的);
• 同时也是在理论上保证最后一个报文可靠到达(假设最后一个 ACK 丢失, 那么 服务器会再重发一个 FIN. 这时虽然客户端的进程不在了, 但是 TCP 连接还在, 仍然 可以重发 LAST_ACK);
2.1解决TIME_WAIT 状态引起的 bind 失败的方法
在 server 的 TCP 连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的
• 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是 每秒都有很大数量的客户端来请求).
• 这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务 器端主动清理掉), 就会产生大量 TIME_WAIT 连接.
• 由于我们的请求量很大, 就可能导致 TIME_WAIT 的连接数很多, 每个连接都会 占用一个通信五元组(源 ip, 源端口, 目的 ip, 目的端口, 协议). 其中服务器的 ip 和端 口和协议是固定的. 如果新来的客户端连接的 ip 和端口号和 TIME_WAIT 占用的链 接重复了, 就会出现问题.
使用 setsockopt()设置 socket 描述符的 选项 SO_REUSEADDR 为 1, 表示允许创建端 口号相同但 IP 地址不同的多个 socket 描述符
三、理解CLOSE_WAIT状态
当你看到CLOSE_WAIT状态时,说明此时只是断开了双方中一个方向上的连接,并没有完成四次挥手。我们可以做一个实验,让客户端一方主动断开连接,而服务端处在一个死循环中或者服务端不调用close函数。
假设现在客户端和服务端建立好连接,服务端一旦建立连接服务端到客户端方向上的连接,就会处于ESTABLISHED状态。
对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成. 这是一个 BUG。只需要加上对应的 close 即可解决问题。
服务端结束服务以后一定要调用close函数,否则会发生内存泄漏。
发生CLOSE_WAIT 的原因:
- 没有处理 read 返回值为0的情况。一般read 返回值为0 ,write一端就会认为对端要连接断开;如果read 返回值小于0,说明读取出错。
- 主要原因可能是客户端一端关闭了连接,然而此时服务端忙于处理其他 IO任务,没有关闭双方连接。