为什么 TCP 需要三次握手

1,019 阅读9分钟

TCP 是一种可靠的流控制协议,它需要在双方交换信息之前,进行连接。而连接则是双方交换一些信息,这些信息包括:套接字(源IP,源端口,目的IP,目的端口)、序列号、窗口大小,这三个信息就是连接。

TCP 需要通过握手来获取到这些连接信息,而这些就会为了保证可靠性和流控制机制,所以,建立 TCP 连接的双方需要对上述三种类型的信息达成共识。连接中的套接字由 IP 地址 和 TCP 端口号组成,用于定位资源;窗口大小用于流控制;序列号则指示收到的数据如何进行重组,并且接收方可以通过序列号通知发送方已经接收了哪些数据,发送方也可以通过序列号追踪已经发送的数据。

现在我们已经知道 TCP 需要通过三次握手来同步套接字、序列号、窗口大小。那么现在问题就转变为 TCP 通过三次握手来同步这些连接信息的作用是什么?

通过三次握手才能阻止重复旧连接的初始化

在 RFC 793 中就指出了 TCP 连接使用三次握手的主要原因——为了阻止历史的重复连接初始化造成的混乱问题,防止使用 TCP 协议通信的双方建立错误的连接。

我们可以想象以下场景:如果通信的双方建立连接使用两次握手,那么发送方一旦发送了建立连接请求之后就无法撤销此次请求,如果在网络状况复杂或者较差的情况下,发送方连续发送多次建立连接的请求,如果 TCP 建立通信只能两次握手,那么接收方就只能选择接受或者拒绝此次请求,它并不知道此次请求是不是由于网络原因而过期的连接。

所以,TCP 选择使用三次握手来建立连接,并在连接引入 RST 这一控制信息。接收方当收到连接请求时,会将发送方发来的 SEQ + 1 发送回发送方,由发送方判断此次连接是否有效(是否为历史连接)。

如果当前连接是历史连接,即 SEQ 过期或者超时,那么发送方就会直接发送 RST 控制信息中止这一次连接;如果当前连接不是历史连接,那么发送方就会直接回复 ACK,则建立连接成功。

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

初始序列号

三次握手的另外一个重要作用是双方协商初始序列号,作为一个可靠的传输层协议,TCP 需要在不稳定的网络环境中构建一个可靠的传输层,网络的不确定性会导致数据包的缺失和顺序颠倒等问题,常见的问题如下:

  • 数据包被发送方多次发送造成接收方收到重复的数据包
  • 数据包在传输过程中被路由或其他节点丢弃
  • 数据包并没有按照发送的顺序到达接收方

为了解决上述问题,设计者在 TCP 中加入了【序列号】,加入序列号之后:

  • 通过序列号接收方可以知道哪一些数据包是重复的,对其去重
  • 通过序列号可以知道哪一些数据包未收到,即可能被丢弃,发送方可以通过 ACK 对可能丢失的数据包重复发送
  • 接收方可以通过序列号对数据进行排序重组。

随机初始序号

这里还引出另外一个问题,发送方如何判断连接是否有效?要判断连接是否有效的前提是需要一个随机的初始序号,如果发送方使用固定的序号作为初始序号,比如 0,如果发送方连续发送多次建立连接的请求,初始序号都为 0,接收方都会回复 ACK 为 1,那么发送方就无法判断此次回复的 ACK 是对哪一次连接的回复;如果使用的是随机初始序号,那么发送方每次建立连接使用的初始序号基本不可能相同,此时就可以根据回复的 ACK 找到初始序号对应的那一次请求,并对此次请求建立连接。

窗口大小

在建立连接过程中,发送方和接收方会将自己能够接收的字节数告知对方,因为不管是发送方还是接收方,都会将接收到的数据缓存起来,等待上层应用使用,但是这个缓存空间是有上限的,所以,如果接收的数据超过了缓存空间的上线,就会将多余的数据丢弃,这也会造成带宽上的浪费。

我们考虑一个场景,如果没有窗口大小的信息,接收方将缓存空间用完之后,发送方依然发送了大量数据过来,接收方因为没有缓存空间可用,所以只能将其丢弃,而发送方并不知道接收方已经没有了缓存空间,它只会在超时之后,将数据重传,但是接收方依然没有缓存空间,这部分重传的数据只会白白浪费带宽,并且还可能造成网络的堵塞。如果有窗口大小的信息,接收方在没有缓存空间可用时,就将自己窗口大小为 0 的信息告诉发送方,发送方得知接收方已经没有多余的缓存空间接收数据,便不再发送数据包了。

发送方如何知道接收方什么时候可以再次有空间接收数据呢?发送方会每两分钟发送一个至少包含一个字节数据的数据包,接收方如果有可用空间,则会重新报告新的窗口大小给发送方。

接收方在窗口大小为 0 时接收到数据包,仍然需要回复 ACK,报告它想要获取的下一个数据包序号和窗口大小(0)。

TCP 是否可以在第一次握手时携带数据?

有一种情况是 TCP 为什么不在发送端第一次握手时就携带数据,接收方在接收到数据后,先存储起来,等到握手完成之后再使用这些数据。这样做的弊端是如果一直发送第一次握手的请求,并且每次都带上大量的数据,这样接收端就需要将这些大量的数据先存储起来,将会很快就耗尽服务器的内存资源,从而拒绝访问。

但是每次发送信息都需要进行三次握手,这里带有的延迟造成的代价过大,于是就有了 TFO (TCP Fast Open),尽可能降低握手带来的延迟。

TFO 需要使用 TFO Cookie 来证明客户端与服务端之前的连接是有效的,因为上文说过,三次握手就是为了同步双方的连接信息,同时证明对方的网络是可达的。

它的工作流程:握手开始时的 SYN 包中的 TFO cookie(一个TCP选项)来验证一个之前连接过的客户端。如果验证成功,它可以在三次握手最终的 ACK 包收到之前就开始发送数据,这样便跳过了一个绕路的行为,更在传输开始时就降低了延迟。这个加密的 Cookie 被存储在客户端,在一开始的连接时被设定好。然后每当客户端连接时,这个 Cookie 被重复返回。

请求 Fast Open Cookie

sequenceDiagram
Client->>Server: SYN + Cookie Request
Server-->>Client: SYN + ACK + Cookie
Client-)Server: ACK
  1. 客户端在第一次连接的时候,在第一次握手时会带上 Cookie Request,向服务端请求 Cookie。
  2. 服务端收到 Cookie Request 时,会生成一个 Cookie 发送给客户端。
  3. 客户端收到这一个 Cookie 之后,保存起来,等待下一次三次握手时使用。

开始使用 TFO

sequenceDiagram
Client->>Server: SYN + Cookie + data
Server-->>Client: SYN + ACK
Server-->>Client: response data
Client-)Server: ACK
  1. 客户端需要向服务端进行连接,第一次握手会发送三个信息:SYN、保存在客户端的 Cookie、需要发送的数据。
  2. 服务端收到 Cookie,验证 Cookie 有效性,除了回复握手信息之外,也可以带上自己需要发送的数据。
  3. 客户端发送 ACK,完成握手和对数据的确认。

TFO 使得 TCP 在握手期间就可以进行数据的传送,减低了握手带来的延迟。但是他也是不安全的,Cookie 虽然使用加密算法得出,使得第三方无法仿造出来,但是并不能阻止中间人获取 Cookie。中间人获取到 Cookie 之后,通过伪造源 ip、源端口,就可以源源不断的在第一次握手中发送大量的数据,造成服务器内存溢出,暂停提供服务。

总结

发送方发起连接请求时需要包含信息套接字、随机初始序号、窗口大小,如果接收方没有在规定时间内回复信息,那么发送方会使用之前的信息重新发起连接请求,这虽然发起了多次连接请求,但都是属于同一个连接(重试的次数是有限制的)。如果是人为的重新发起连接请求,发送方会重新生成连接信息,即新的套接字、新的随机初始序号、新的窗口大小,这时候,就会存在两个不同的连接,如果只有两次握手,那么有一次连接就是无用的连接,这就浪费资源了。并且发送端是可以伪造源 IP 地址的,那么接收端根本就不是在和一个实际的发送端进行连接。

握手本质上就是在试探网络可不可达,从发送端发送连接请求到发送端接受到确认,在发送端看来确实证明了网络可达并且建立了连接,但是在接收端看来,仅仅接收到连接请求并不能说明什么,因为这个连接请求可能是旧的连接请求,客户端早已下线,也可能是无效的连接请求,伪造的 IP 地址,从接收端的角度来看,它也需要确认网络是否可达,所以,发送端收到确认之后,需要对接收端也进行确认,双向证明网络可达。