TCP 通信之三次握手与四次挥手

关于TCP通信中三次握手这个经常在面试中被问到,之前也了解过,以为懂了,真正阐述的时候才发现自己只是知其然,不知其所以然。看的时候基本是“对对对,是这样的”,但是基本没有考虑到为什么一定要这么设计?不这么做会带来那些后果?其实还是没透彻理解。

把三次握手和四次挥手放在一起学习、对比,能更好的明白其中的设计思路和原则。

TCPUDP

计算机网络书上说,TCPUDP的区别在于,前者建立了一条可靠的通信连接,保证双方能够收到发出的数据包,并且能够自动处理诸如数据包丢失等情况,而后者则完全不考虑实际的网络情况,只负责将数据包发出,不管对方是否收到。TCP的应用场景在提供可靠的通信,而UDP多倾向于低延迟的场景。

三次握手

关于TCP三次握手,大多数人应该都能信手拈来吧:

  1. 服务端启动一个监听端口,等待客户端连接
  2. 客户端发送一个SYN包,申请建立连接(第一次握手,申请)
  3. 服务端返回ACK确认包,并附上SYN(第二次握手,确认并同意)
  4. 客户端发送ACK包,确认(第三次握手,确认)

至此,一个 TCP 连接建立完毕。

tcp-handshake-1

下面,以一个实际的TCP握手过程为例,讲一下每一步详细的流程。

tcp-handshake-2

注意:Wireshark中默认显示的是相对Seq,需要取消勾选 edit–>preferences–>protocol下的TCP中相关选项来显示真实的Seq

第一次握手,客户端向服务端发出一个数据包,将SYN位置为1,表示请求建立连接,并协商相应的配置(协议版本、滑动窗口大小等等),除此之外附上一个Seq序号,这里只关注这几个值,其他的暂时忽略。
第二次握手,服务端向客户端返回一个数据包,将ACK和SYN置为1,表示已接收到连接请求,允许连接,附上自己的配置信息(版本、窗口大小等),除此之外附上自己的Seq序号,同时将客户端的Seq加 1 后放在Acknogement number字段中。
第三次握手,客户端向服务端发送一个数据包,将ACK位置为1,表示已接受到连接请求,确认建立连接。附上自己的Seq + 1,并将服务端的 Seq + 1 放在 Acknogement number 位置中。

四次挥手

与三次握手类似,挥手经过这么几个步骤:

  1. 客户端向服务端发送 FIN 包,告知服务端我的数据已经发送完毕,申请断开连接(第一次挥手,申请)
  2. 服务端马上向客户端回复 ACK 包,表示已经接收到请求(第二次挥手,确认)
  3. 等待应用层响应后,服务端向客户端发送 FIN 包,表示我的数据也已经发送完毕,同意断开连接(第三次挥手,同意)
  4. 客户端向服务端发送 ACK 包,表示我已接收到你的回复,即将断开连接,然后等待一定时间断开连接(第四次挥手,确认)
  5. 服务端收到 ACK 包后立即断开连接

tcp-wave-1

以下是一次四次挥手的实际挥手过程。

tcp-wave-2

多问一些为什么

为什么通信双方要存在 Seq 的概念?

这来源于 TCP 的可靠性要求。可靠性包括两个方面,第一个就是能应对丢包的情况。给每个数据包添加 Seq 标记,双方就能清楚的知道对方已经收到了自己的哪些包。

为什么 Seq 不是从 0 开始?

可靠性的第二个要求就是,能够保证通信双方的真实性,即第三方无法伪造其中一方与另外一方进行通信。考虑Seq从0开始的情况下,有服务端A、客户端B、坏人C,首先C向A发送SYN包,Seq=0,源ip设置成B,然后A回复B ACK+SYN包,等待一定时间后,C再向A发送ACK包,Seq=1,A发送给B的包在这里是可有可无的。于是一个连接就建立起来了,A以为自己是与B建立的连接,其实另一方是C。

那么如何来保证 C 无法伪造成B呢?一个好办法就是C必须收到A发给B的数据包才能继续建立连接,因为路由是可靠的,A发给B的数据包一定无法被C接收(局域网等环境下的数据包截获不在此处考虑),在A发给B的数据包中添加一个验证机制。于是,将Seq的初始值从0修改为一个随机值(其实并不是),只有A跟B知道,C在接收不到确认包的情况下是猜不出来Seq的,于是A在收到错误Seq的情况下直接拒绝继续连接,以此保证了连接双方的真实性。

为什么三次握手中 ACK 包与 SYN 包是合并发送的,而四次挥手时却又分开了?

总结一下,其实无论是握手还是挥手,通信双方都是在做这么一件事:确认自己的请求被对方收到且同意了,以及让对方知道自己收到并确认。分开说,握手的过程是保证通信双方的收和发都是正常的,挥手的过程是保证双方都发送完毕自己想要发送的信息并让双方安心的断开连接。

在挥手的时候,传输层无法知道应用层是否还有数据要传输,而又需要立即回复,所以先回复一个 ACK 包告知对方自己收到断开申请了,请稍等一会,我可能还有数据没有传输完毕,等应用层确认没有数据包继续传输后,再发送FIN包,确认自己没有数据需要发送可以断开连接。
而握手的时候,应用层是预先建立了监听,传输层可以肯定应用层是已经准备好建立连接的,所以合并ACK与SYN包,这样能降低延迟和网络开销。

所以问题的关键在于传输层是否知道应用层有没有准备好。

为什么四次挥手中,客户端回复完 ACK 包后,还等待一段时间,而不是马上断开连接?

TCP的可靠性要求保证双方共同连接与断开。考虑在客户端立即断开的情况下,客户端发出的ACK包丢失,服务端不知道自己的FIN包是否被收到,于是在超时后选择重发FIN包,这就造成了一方断开连接而另一方无法正常断开连接的情况。要求客户端继续开启一段时间(两个超时时间),如果没有收到服务端重发的FIN包,就认为服务端已经正常退出了,于是自己也能断开连接。当然了,是可能出现ACK包丢失,而且服务端重发的FIN包也丢失的情况,但是这种情况实在是太罕见了,如果什么奇葩情况都要考虑的话那这挥手过程就会跟两个异地恋人在车站的告别一样没完没了(^_^)。而且,工程上的做法一般都不会保证也不可能保证百分之一百可靠。

断开连接一定是四次吗?可不可以像三次握手一样只经过三次挥手?

当然可以,以下就是一个三次挥手的过程。

tcp-wave-3

正如前面提到的一样,传输层分开发送ACK包和FIN包的原因在于不知道应用层是否准备好,而数据包传输存在一个超时时间,必须在这段时间(二分之一超时时间)内回复。随着计算资源性能提升,应用层的响应已经能够做到很低了,如果应用层回复时间很短,那么就可以先等应用层响应后一起发送ACK包和FIN包。

这就是延迟确认优化,见 TCP中断可以用3次挥手吗?—知乎

图中的四次挥手中,为什么服务端的 FIN 中还是包含了 ACK ?

没有查相关文献,只是大胆的猜测一下。TCP数据包格式是一定的,报头的大小固定,是否设置有效值都不影响通信,而且前面也提到了网络环境很复杂,丢包的情况时有发生,如果前面的ACK包丢失了,那么客户端还得再重传一次,如果在FIN包中同时再确认一下,就可以避免这一次额外的重发包,而且这么做的代价基本是0。这就像是我们在偏僻的地方打车,能很容易的以远低于市场价成交,因为他们反正回程也是空车,能挣一点是一点。

参考

TCP 的特性

趣文:TCP/IP 之 大明王朝邮差

上面这篇文章用一个十分有趣的故事清晰地阐释了 TCP 通信中握手过程与双方交换数据的过程,我收藏了很久,值得一看。