TCP 三次握手 / 四次挥手
Posted: 11.12.2019
介绍
在讲三次握手与四次挥手之前,我们势必要先讲一下 TCP 本身。
TCP 是 OSI 模型第四层(传输层)的协议,位于第三层(网络层)的 IP 协议之上。
TCP = Transmission Control Protocol,传输控制协议,目的自然是为了控制传输。
TCP 是连接导向的(connection oriented),因此在传输数据前,需要建立会话(session)。
而建立会话的方式,就是三次握手。三次握手后,TCP 便会建立一个全双工(full-duplex)的通信,然后开始传输数据。
但是在讲三次握手与四次挥手之前,我们必须先讲一下 TCP 的报文格式。
TCP 报文格式

Source Port 和 Destination Port 分别指明了发送和接受数据的端口。
接下来就是三次握手中需要用到的序列号(Sequence Number)和确认号(Acknowledge Number)。
接下来的 Header Length 指明了 TCP 报头的长度,和握手挥手没关系,这里就不细讲了。
Reserved 则是保留起来,未来可能会用到,也和握手挥手没关系,不细讲了。
接下来,非常重要的是六个 flag 里面的,ACK 和 SYN 这俩 flag(flag 的意思就是设置为 true 或者 false,中文是标志)。
我们经常可以看到有的文章在讲三次握手的时候,一会儿用大写的 ACK,一会儿又用小写的 ack,是不是很奇怪。是他们弄错了吗?其实不是,大写的 ACK 指的是这六个 flag,而小写的 ack 则是指的上面那个 32 bit 大小的确认号。如果大小写 SYN 和 ACK 分开用,说明那个人其实是很懂的。
ACK 这个 flag 如果设置成 true,说明确认序号有效。
SYN 这个 flag,只有在发送方和接收方分别发送第一个报文(packet)的时候,才会设置为 true。
FIN 这个 flag,指明了发送方已经没有数据需要发送了,要结束 TCP 链接。
剩下的部分在三次握手和四次挥手中不会涉及到,因此不细讲了。想要了解更多的话,可以去看这篇文章:TCP/IP 数据包报文格式(IP包、TCP报头、UDP报头)
三次握手
流程
我们先来看一下三次握手的流程:

- 首先,要建立 TCP 连接,于是发送方给接收方发送了一个报文,这个报文的 SYN 标志被设置成了 1,说明要开始握手了
- 我们假设这个时候的序列号为 x(这说明发送方的数据是从 x 的位置开始发送的)
- 接收方收到了来自发送方的 TCP 报文。因为窗口大小(Window Size)为 1,所以他其实收到的就是序列号为 x 的 segment,而且就只有这一个 segment。然后他想要让发送方知道自己已经收到报文了,因此就发送了一个新的报文(只有一个报文,并不是很多文章里写的两个)给发送方
- 在这个报文里,ACK 和 SYN 被设置为 1
- 确认号(ack)被设置为 x+1,这意味着接收方现在收到了 x,所以他希望发送方接下来发的玩意儿要从 x+1 开始
- 并且接收方随机生成了一个新的序列号 y,作为这个报文的序列号,发给发送方。
- 发送方接收到了来自接收方的报文,但这还不太够,还需要返回一个确认给接受方
- 这里三次握手已经要结束了,所以这次发送的报文里,SYN 并没有被设置成 1,但是仍然需要确认,因此 ACK 被设置成了 1
- 刚才来自接收方的报文里写了,ack = x+1,说明接收方希望下面收到 x+1 的玩意儿,那么就满足他呗,把当前报文的序列号设置成 x+1
- 不过对面设置了 ack,我这里也得设置啊,刚才他发了 y,那么我自然希望下一个收到的是从 y+1 开始的,于是把确认号设置成了 y+1。
- 接收方收到了来自发送方的报文
- TCP 连接建立,双方可以进行数据传送了。
如果只有两次握手
如果只有两次握手的话,就会出现死锁。
考虑两个计算机 sb 和 wdnmd 之间的通信。
- 假定 sb 给 wdnmd 发送一个连接请求分组
- wdnmd 收到了这个分组,并发送了确认应答分组
- 然后假设 sb 发送给 wdnmd 的确认丢了
- 按照两次握手的协定,sb 认为连接已经成功地建立了,可以开始发送数据分组
- 可是,wdnmd 在 sb 的应答分组在传输中被丢失的情况下,不知道 sb 是否已准备好,不知道 sb 建立什么样的序列号
- 在这种情况下,wdnmd 认为连接还未建立成功,将忽略 sb 发来的任何数据分组,只等待连接确认应答分组
- 而 sb 在发出的分组超时后,重复发送同样的分组
- 这样就形成了死锁
举个🌰:假设你和女朋友出去开房,你先去开房,然后把房号告诉女朋友。
- 你开了间房,房号为 110,然后你发短信给女朋友,说 ”房号是 110“
- 你女朋友收到了房号,但还要再确认一下,不然走错就尴尬了,所以她给你回了条短信,问 ”房号是 110 吧“
- 你收到了这条短信,于是发了条确认过去,”没错,房号就是 110 “
- 网不太好,你的女朋友没收到这条短信
- 但从你的角度来说,你觉得你女朋友应该已经确认房号了,就直接过去了
- 然而你女朋友没有收到你的确认,不能确定房号就是 110,于是一直在等你的回复
- 你到了房间里,等了好几个小时,女朋友都没来
- 然后你意识到,自己被放鸽子了。wdnmd
在这个例子里,只有你自己觉得连接建立了,但你女朋友不这么觉得,这就是只有二次握手的后果。
四次挥手
流程

- 首先,客户端要通知服务器,咱俩拜拜了,因此停止发送数据,发送了一个报文,并且把 FIN 标志设置成 1
- 并且在这个报文里,客户端指定了序列号为 u(等于前面已经传送过来的数据的最后一个字节的序列号+1)
- 此时,客户端进入FIN-WAIT-1状态
- 服务器收到连接释放报文,要发一个确认报文来通知客户端自己知道了。于是这个报文的 ACK = 1,确认号(ack)就是 u+1
- 此时,客户端进入 CLOSE-WAIT 状态:这是一种半关闭的状态,意味着客户端已经没有数据要发送了,但服务器若发送数据,客户端依然接受
- 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文。因为是连接释放报文,所以 FIN = 1,并且 ack 和确认报文的 ack 一样
- 此时客户端进入TIME-WAIT状态
- TCP 连接还没有释放,必须经过(2*最长报文段寿命的时间)后,当客户端撤销 TCB 后,才关闭
- 服务器只要收到了客户端发出的确认,立即进入关闭状态(因此服务器结束 TCP 连接的时间要比客户端早一些)
为什么是四次挥手?
因为三次握手的时候,ACK 和 SYN 其实是在一个报文里发送的(只不过是分别设置了对应的 flag)。
而关闭 TCP 连接时,当服务器收到含有 FIN 报文后,它知道客户端已经没有数据了。但服务器自己可能还有数据没有发送完毕,不会立即关闭 SOCKET,所以没办法立刻回复一个 FIN 报文,只能先回复一个ACK报文,告诉客户端收到了。只有当服务器端所有报文都发送完了,才可以发送 FIN。
为什么需要经过 2MSL?
MSL(Maximum Segment Lifetime),最大报文段生存时间。2MSL 就是一个发送和一个回复所需的最大时间
虽然按道理,四个报文都发送完毕,我们可以直接进入 CLOSE 状态了,但是我们必须假设网络是不可靠的,有可以最后一个 ACK 丢失。
服务器如果没有收到来自客户端的确认报文,将不断重复发送 FIN 片段。所以客户端不能立即关闭,它必须确认服务器接收到了它的确认报文。
所以 TIME_WAIT 状态用来重发可能丢失的 ACK 报文。
客户端会在发送出 ACK 报文之后进入到 TIME_WAIT 状态。它会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到 FIN报文,就说明服务器没有收到它的确认报文,因此客户端会重发 ACK 报文并再次等待 2MSL。
参考资料
TCP/IP 数据包报文格式(IP包、TCP报头、UDP报头)