Why’s THE Design

Posted on By Guanzhou Song

转载自: 为什么这么设计系列文章

网络

为什么 TCP 建立连接需要三次握手

TCP连接的目的

建立 TCP 连接就是通信的双方需要对上述的三种信息达成共识,连接中的一对 Socket 是由互联网地址标志符和端口组成的,窗口大小主要用来做流控制,最后的序列号是用来追踪通信发起方发送的数据包序号,接收方可以通过序列号向发送方确认某个数据包的成功接收。

阻止重复历史连接的初始化

TCP 连接使用三次握手的首要原因:

为了阻止历史的重复连接初始化造成的混乱问题,防止使用TCP协议通信的双方建立了错误的连接。

如果通信双方的通信次数只有两次,那么发送方一旦发出建立连接的请求之后它就没有办法撤回这一次请求。

如果在网络状况复杂或者较差的网络中,发送方连续发送多次建立连接的请求,如果 TCP 建立连接只能通信两次,那么接收方只能选择接受或者拒绝发送方发起的请求,它并不清楚这一次请求是不是由于网络拥堵而早早过期的连接。

所以,TCP 选择使用三次握手来建立连接并在连接引入了 RST 这一控制消息,接收方当收到请求时会将发送方发来的 SEQ+1 发送给对方,这时由发送方来判断当前连接是否是历史连接:

  • 如果当前连接是历史连接,即 SEQ 过期或者超时,那么发送方就会直接发送 RST 控制消息中止这一次连接;

  • 如果当前连接不是历史连接,那么发送方就会发送 ACK 控制消息,通信双方就会成功建立连接;

使用三次握手和 RST 控制消息将是否建立连接的最终控制权交给了发送方,因为只有发送方有足够的上下文来判断当前连接是否是错误的或者过期的,这也是 TCP 使用三次握手建立连接的最主要原因。

初始序列号

另一个使用三次握手的重要的原因就是通信双方都需要获得一个用于发送信息的初始化序列号。

作为一个可靠的传输层协议,TCP需要在不稳定的网络环境中构建一个可靠的传输层,网络的不确定性可能会导致数据包的缺失和顺序颠倒等问题,常见的问题可能包括:

  1. 数据包被发送方多次发送造成数据的重复;

  2. 数据包在传输的过程中被路由或者其他节点丢失;

  3. 数据包到达接收方可能无法按照发送顺序;

为了解决上述这些可能存在的问题,TCP 协议要求发送方在数据包中加入『序列号』字段,有了数据包对应的序列号,我们就可以:

  1. 接收方可以通过序列号对重复的数据包进行去重;

  2. 发送方会在对应数据包未被 ACK 时进行重复发送;

  3. 接收方可以根据数据包的序列号对它们进行重新排序;

序列号在 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 连接究竟是什么,RFC 793 - Transmission Control Protocol - IETF Tools 对 TCP 连接有着非常清楚的定义

用于保证可靠性和流控制机制的数据,包括 Socket、序列号以及窗口大小。

TCP 建立连接时通过三次握手可以有效地避免历史错误连接的建立,减少通信双方不必要的资源消耗,三次握手能够帮助通信双方获取初始化序列号,它们能够保证数据包传输的不重不丢,还能保证它们的传输顺序,不会因为网络传输的问题发生混乱,到这里不使用『两次握手』和『四次握手』的原因已经非常清楚了:

  • 『两次握手』:无法避免历史错误连接的初始化,浪费接收方的资源;
  • 『四次握手』:TCP 协议的设计可以让我们同时传递 ACK 和 SYN 两个控制信息,减少了通信次数,所以不需要使用更多的通信次数传输相同的信息;

为什么 DNS 使用 UDP 协议

在绝大多数情况下,DNS 都是使用 UDP 协议进行通信的,DNS 协议在设计之初也推荐我们在进行域名解析时首先使用 UDP,这确实能解决很多需求,但是不能解决全部的问题。

实际上,DNS 不仅使用了 UDP 协议,也使用了 TCP 协议 :DNS 查询的类型不止包含 A 记录、CNAME 记录等常见查询,还包含 AXFR 类型的特殊查询.

这种特殊查询主要用于 DNS 区域传输,它的作用就是在多个命名服务器之间快速迁移记录,由于查询返回的响应比较大,所以会使用 TCP 协议来传输数据包。

### 设计 #### UDP 作为互联网的标准,目前的绝大多数 DNS 请求和响应都会使用 UDP 协议进行数据的传输。

虽然每一个 UDP 数据包中都包含了很多以太网、IP、UDP 以及 DNS 协议的相关内容,但是DNS 请求大小往往只有几百字节,DNS 请求的响应也只有数百个字节,这对于今天其他的常见请求来讲都是非常小的数据包

TCP 建立连接会带来以下的额外开销:

  • TCP 建立连接需要进行三次网络通信;
    • TCP 建立连接需要传输 ~130 字节的数据;
  • TCP 销毁连接需要进行四次网络通信;
    • TCP 销毁连接需要传输 ~160 字节的数据;

所以当请求体和响应的大小比较小时,通过 TCP 协议进行传输不仅需要传输更多的数据,还会消耗更多的资源。

多次通信以及信息传输带来的时间成本在 DNS 查询较小时是无法被忽视的,TCP连接带来的可靠性在 DNS 的场景中没能发挥太大的作用。

TCP

DNS 协议需要处理的数据包越来越大、数据也越来越多,但是『为什么当需要传输的数据较多时我们就必须使用 TCP 协议呢?』,如果继续使用 UDP 协议就不能完成 DNS 解析么。

从理论上来说,一个 UDP 数据包的大小最多可以达到 64KB,这对于一个常见的 DNS 查询其实是一个非常大的数值;

但是在实际生产中,一旦数据包中的数据超过了传送链路的最大传输单元(MTU,也就是单个数据包大小的上限,一般为 1500 字节)

当前数据包就可能会被分片传输、丢弃,部分的网络设备甚至会直接拒绝处理包含 EDNS(0) 选项的请求,这就会导致使用 UDP 协议的 DNS 不稳定。

TCP 作为可靠的传输协议,可以非常好的解决这个问题。

通过序列号、重传等机制能够保证消息的不重不漏,消息接受方的 TCP 栈会对分片的数据重新进行拼装,DNS等应用层协议可以直接使用处理好的完整数据。

同时,当数据包足够大的时候,TCP 三次握手带来的额外开销比例就会越来越小,与整个包的大小相比就会趋近于 0。

所以,我们在 DNS 中存储较多的内容时,TCP 三次握手以及协议头带来的额外开销就不是关键因素了。

不过 TCP 三次握手带来的三次网络传输耗时还是没有办法避免的,这也是我们在目前的场景下不得不接受的问题。

总结

很多人认为 DNS 使用了 UDP 协议来获取域名对应的 IP 地址,这个观点虽然没错,但是还是有一些片面。

更加准确的说法其实是 DNS 查询在刚设计时主要使用 UDP 协议进行通信,而 TCP 协议也是在 DNS 的演进和发展中被加入到规范的:

  1. DNS 在设计之初就在区域传输中引入了 TCP 协议,在查询中使用 UDP 协议;

  2. 当 DNS 超过了 512 字节的限制,我们第一次在 DNS 协议中明确了『当 DNS 查询被截断时,应该使用 TCP 协议进行重试』这一规范;

  3. 随后引入的 EDNS 机制允许我们使用 UDP 最多传输 4096 字节的数据,但是由于 MTU 的限制导致的数据分片以及丢失,使得这一特性不够可靠;

  4. 在最近的几年,重新规定了 DNS 应该同时支持 UDP 和 TCP 协议,TCP 协议也不再只是重试时的选择;

DNS 查询选择 UDP 或者 TCP 两种不同协议时的主要原因:

  • UDP 协议

    • DNS 查询的数据包较小、机制简单;

    • UDP 协议的额外开销小、有着更好的性能表现;

  • TCP 协议

    • DNS 查询由于 DNSSEC 和 IPv6 的引入迅速膨胀,导致 DNS 响应经常超过 MTU 造成数据的分片和丢失,我们需要依靠更加可靠的 TCP 协议完成数据的传输;

    • 随着 DNS 查询中包含的数据不断增加,TCP 协议头以及三次握手带来的额外开销比例逐渐降低,不再是占据总传输数据大小的主要部分;

无论是选择 UDP 还是 TCP,最核心的矛盾就在于需要传输的数据包大小,如果数据包小到一定程度,UDP 协议绝对最佳的选择

但是当数据包逐渐增大直到突破 512 字节以及 MTU 1500 字节的限制时,我们也只能选择使用更可靠的 TCP 协议来传输 DNS 查询和相应。