TCP 三次握手的底层逻辑
TCP 三次握手是基础中的基础,所有工程师都对此熟悉不过。但这里的三次往往被忽视,究竟为何 TCP 需要三次握手?本文中,我们探讨 TCP 三次握手的底层逻辑。
这篇文章主要会从以下几个方面介绍为什么我们需要通过三次握手才可以初始化 Sockets、窗口大小、初始序列号并建立 TCP 连接:
- 通过三次握手才能阻止重复历史连接的初始化;
- 通过三次握手才能对通信双方的初始序列号进行初始化;
- 讨论其他次数握手建立连接的可能性;
这几个论点中的第一个是 TCP 选择使用三次握手的最主要原因,其他的几个原因相比之下都是次要的原因,我们在这里对它们的讨论只是为了让整个视角更加丰富,通过多方面理解这一有趣的设计决策。
RFC 793 - Transmission Control Protocol 文档中非常清楚地定义了 TCP 中的连接是什么,我们简单总结一下:用于保证可靠性和流控制机制的信息,包括 Socket、序列号以及窗口大小叫做连接。
所以,建立 TCP 连接就是通信的双方需要对上述的三种信息达成共识,连接中的一对 Socket 是由互联网地址标志符和端口组成的,窗口大小主要用来做流控制,最后的序列号是用来追踪通信发起方发送的数据包序号,接收方可以通过序列号向发送方确认某个数据包的成功接收。
到这里,我们将原有的问题转换成了『为什么需要通过三次握手才可以初始化 Sockets、窗口大小和初始序列号?』,那么接下来我们就开始对这个细化的问题进行分析并寻找解释。
设计
这篇文章主要会从以下几个方面介绍为什么我们需要通过三次握手才可以初始化 Sockets、窗口大小、初始序列号并建立 TCP 连接:
- 通过三次握手才能阻止重复历史连接的初始化;
- 通过三次握手才能对通信双方的初始序列号进行初始化;
- 讨论其他次数握手建立连接的可能性;
这几个论点中的第一个是 TCP 选择使用三次握手的最主要原因,其他的几个原因相比之下都是次要的原因,我们在这里对它们的讨论只是为了让整个视角更加丰富,通过多方面理解这一有趣的设计决策。
历史连接
RFC 793 - Transmission Control Protocol 其实就指出了 TCP 连接使用三次握手的首要原因 —— 为了阻止历史的重复连接初始化造成的混乱问题,防止使用 TCP 协议通信的双方建立了错误的连接。
想象一下这个场景,如果通信双方的通信次数只有两次,那么发送方一旦发出建立连接的请求之后它就没有办法撤回这一次请求,如果在网络状况复杂或者较差的网络中,发送方连续发送多次建立连接的请求,如果 TCP 建立连接只能通信两次,那么接收方只能选择接受或者拒绝发送方发起的请求,它并不清楚这一次请求是不是由于网络拥堵而早早过期的连接。
所以,TCP 选择使用三次握手来建立连接并在连接引入了 RST
这一控制消息,接收方当收到请求时会将发送方发来的 SEQ+1
发送给对方,这时由发送方来判断当前连接是否是历史连接:
- 如果当前连接是历史连接,即
SEQ
过期或者超时,那么发送方就会直接发送RST
控制消息中止这一次连接; - 如果当前连接不是历史连接,那么发送方就会发送
ACK
控制消息,通信双方就会成功建立连接;
使用三次握手和 RST
控制消息将是否建立连接的最终控制权交给了发送方,因为只有发送方有足够的上下文来判断当前连接是否是错误的或者过期的,这也是 TCP 使用三次握手建立连接的最主要原因。
初始序列号
另一个使用三次握手的重要的原因就是通信双方都需要获得一个用于发送信息的初始化序列号,作为一个可靠的传输层协议,TCP 需要在不稳定的网络环境中构建一个可靠的传输层,网络的不确定性可能会导致数据包的缺失和顺序颠倒等问题,常见的问题可能包括:
- 数据包被发送方多次发送造成数据的重复;
- 数据包在传输的过程中被路由或者其他节点丢失;
- 数据包到达接收方可能无法按照发送顺序;
为了解决上述这些可能存在的问题,TCP 协议要求发送方在数据包中加入『序列号』字段,有了数据包对应的序列号,我们就可以:
- 接收方可以通过序列号对重复的数据包进行去重;
- 发送方会在对应数据包未被 ACK 时进行重复发送;
- 接收方可以根据数据包的序列号对它们进行重新排序;
序列号在 TCP 连接中有着非常重要的作用,初始序列号作为 TCP 连接的一部分也需要在三次握手期间进行初始化,由于 TCP 连接通信的双方都需要获得初始序列号,所以它们其实需要向对方发送 SYN
控制消息并携带自己期望的初始化序列号 SEQ
,对方在收到 SYN
消息之后会通过 ACK
控制消息以及 SEQ+1
来进行确认。
如上图所示,通信双方的两个 TCP A/B
分别向对方发送 SYN
和 ACK
控制消息,等待通信双方都获取到了自己期望的初始化序列号之后就可以开始通信了,由于 TCP 消息头的设计,我们可以将中间的两次通信合成一个,TCP B
可以向 TCP A
同时发送 ACK
和 SYN
控制消息,这也就帮助我们将四次通信减少至三次。
除此之外,网络作为一个分布式的系统,其中并不存在一个用于计数的全局时钟,而 TCP 可以通过不同的机制来初始化序列号,作为 TCP 连接的接收方我们无法判断对方传来的初始化序列号是否过期,所以我们需要交由对方来判断,TCP 连接的发起方可以通过保存发出的序列号判断连接是否过期,如果让接收方来保存并判断序列号却是不现实的,这也再一次强化了我们在上一节中提出的观点 —— 避免历史错连接的初始化。
通信次数
当我们讨论 TCP 建立连接需要的通信次数时,我们经常会执着于为什么通信三次才可以建立连接,而不是两次或者四次;讨论使用更多的通信次数来建立连接往往是没有意义的,因为我们总可以 使用更多的通信次数交换相同的信息 ,所以使用四次、五次或者更多次数建立连接在技术上都是完全可以实现的。
这种增加 TCP 连接通信次数的问题往往没有讨论的必要性,我们追求的其实是用更少的通信次数(理论上的边界)完成信息的交换,也就是为什么我们在上两节中也一再强调使用『两次握手』没有办法建立 TCP 连接,使用三次握手是建立连接所需要的最小次数。
总结
我们在这篇文章中讨论了为什么 TCP 建立连接需要经过三次握手,在具体分析这个问题之前,我们首先重新思考了 TCP 连接究竟是什么,RFC 793 - Transmission Control Protocol - IETF Tools 对 TCP 连接有着非常清楚的定义 —— 用于保证可靠性和流控制机制的数据,包括 Socket、序列号以及窗口大小。
TCP 建立连接时通过三次握手可以有效地避免历史错误连接的建立,减少通信双方不必要的资源消耗,三次握手能够帮助通信双方获取初始化序列号,它们能够保证数据包传输的不重不丢,还能保证它们的传输顺序,不会因为网络传输的问题发生混乱,到这里不使用『两次握手』和『四次握手』的原因已经非常清楚了:
- 『两次握手』:无法避免历史错误连接的初始化,浪费接收方的资源;
- 『四次握手』:TCP 协议的设计可以让我们同时传递
ACK
和SYN
两个控制信息,减少了通信次数,所以不需要使用更多的通信次数传输相同的信息;
我们重新回到在文章开头提的问题,为什么使用类比解释 TCP 使用三次握手是错误的?这主要还是因为,这个类比没有解释清楚核心问题 —— 避免历史上的重复连接。到最后,我们还是来看一些比较开放的相关问题,有兴趣的读者可以仔细想一下下面的问题:
- 除了使用序列号是否还有其他方式保证消息的不重不丢?
- UDP 协议有连接的概念么,它能保证数据传输的可靠么?