玩转TCP

前言

最近在拉勾教育学习计算机网络相关知识,今天先来学习下TCP。

正文

什么是TCP

TCP(Transport Control Protocol)是一个传输层协议,提供 Host-To-Host 数据的可靠传输,支持全双工,是一个连接导向的协议。

TCP 的握手和挥手

TCP 是一个连接导向的协议,设计有建立连接(握手)和断开连接(挥手)的过程。

TCP 协议有这样几个基本操作:

  • 一个 Host 主动向另一个 Host 发起连接,称为 SYN(Synchronization),请求同步;
  • 一个 Host 主动断开请求,称为 FIN(Finish),请求完成;
  • 一个 Host 给另一个 Host 发送数据,称为 PSH(Push),数据推送。

以上 3 种情况,接收方收到数据后,都需要给发送方一个 ACK(Acknowledgement)响应。请求/响应的模型是可靠性的要求,如果一个请求没有响应,发送方可能会认为自己需要重发这个请求。

建立连接的过程(三次握手)

因为要保持连接和可靠性约束,TCP 协议要保证每一条发出的数据必须给返回,返回数据叫作 ACK(也就是响应)。

按照这个思路,你可以看看建立连接是不是需要 3 次握手:

玩转TCP_第1张图片

  1. 客户端发消息给服务端(SYN)
  2. 服务端准备好进行连接
  3. 服务端针对客户端的 SYN 给一个 ACK

你可以能会问,到这里不就可以了吗?2 次握手就足够了。但其实不是,因为服务端还没有确定客户端是否准备好了。比如步骤 3 之后,服务端马上给客户端发送数据,这个时候客户端可能还没有准备好接收数据。因此还需要增加一个过程。

接下来还会发生以下操作:

  1. 服务端发送一个 SYN 给客户端
  2. 客户端准备就绪
  3. 客户端给服务端发送一个 ACK

你可能会问,上面不是 6 个步骤吗? 怎么是 3 次握手呢?下面我们一起分析一下其中缘由。

步骤 1 是 1 次握手;

步骤 2 是服务端的准备,不是数据传输,因此不算握手;

步骤 3 和步骤 4,因为是同时发生的,可以合并成一个 SYN-ACK 响应,作为一条数据传递给客户端,因此是第 2 次握手;

步骤 5 不算握手;

步骤 6 是第 3 次握手。

为了方便你理解步骤 3 和步骤 4,这里我画了一张图。可以看到下图中 SYN 和 ACK 被合并了,因此建立连接一共需要 3 次握手,过程如下图所示:

玩转TCP_第2张图片

从上面的例子中,你可以进一步思考 SYN、ACK、PSH 这些常见的标识位(Flag)在传输中如何表示。

一种思路是为 TCP 协议增加协议头。在协议头中取多个位(bit),其中 SYN、ACK、PSH 都占有 1 个位。比如 SYN 位,1 表示 SYN 开启,0 表示关闭。因此,SYN-ACK 就是 SYN 位和 ACK 位都置 1。这种设计,我们也称为标识(Flag)。

断开连接的过程(4 次挥手)

继续上面的思路,如果断开连接需要几次握手?给你一些提示,你可以在脑海中这样构思。

  1. 客户端要求断开连接,发送一个断开的请求,这个叫作(FIN)。
  1. 服务端收到请求,然后给客户端一个 ACK,作为 FIN 的响应。
  1. 这里你需要思考一个问题,可不可以像握手那样马上传 FIN 回去?
    其实这个时候服务端不能马上传 FIN,因为断开连接要处理的问题比较多,比如说服务端可能还有发送出去的消息没有得到 ACK;也有可能服务端自己有资源要释放。因此断开连接不能像握手那样操作——将两条消息合并。所以,服务端经过一个等待,确定可以关闭连接了,再发一条 FIN 给客户端。
  1. 客户端收到服务端的 FIN,同时客户端也可能有自己的事情需要处理完,比如客户端有发送给服务端没有收到 ACK 的请求,客户端自己处理完成后,再给服务端发送一个 ACK。

玩转TCP_第3张图片

TIME_WAIT

相信大家都知道,TCP 主动关闭连接的那一方会最后进入TIME_WAIT 状态。

其实就是客户端发出确认报文后不是立马释放TCP连接,而是要经过2MSL(最长报文段寿命的2倍时长)后才释放TCP连接。

那么TIME_WAIT 状态是用来解决或避免什么问题呢?

这里同样是要考虑丢包的问题,如果第四次挥手的报文丢失,服务端没收到确认ack报文就会重发第三次挥手的报文,这样报文一去一回最长时间就是2MSL,所以需要等这么长时间来确认服务端确实已经收到了。

TCP的粘包和拆包

TCP 是一个传输层协议。TCP 发送数据的时候,往往不会将数据一次性发送,像下图这样:

玩转TCP_第4张图片

而是将数据拆分成很多个部分,然后再逐个发送。像下图这样:

玩转TCP_第5张图片

同样的,在目的地,TCP 协议又需要逐个接收数据。请你思考,TCP 为什么不一次发送完所有的数据?比如我们要传一个大小为 10M 的文件,对于应用层而言,就是一次传送完成的。而传输层的协议为什么不选择将这个文件一次发送完呢?

这里有很多原因,比如为了稳定性,一次发送的数据越多,出错的概率越大。再比如说为了效率,网络中有时候存在着并行的路径,拆分数据包就能更好地利用这些并行的路径。再有,比如发送和接收数据的时候,都存在着缓冲区。如下图所示:

玩转TCP_第6张图片

缓冲区是在内存中开辟的一块区域,目的是缓冲。因为大量的应用频繁地通过网卡收发数据,这个时候,网卡只能一个一个处理应用的请求。当网卡忙不过来的时候,数据就需要排队,也就是将数据放入缓冲区。如果每个应用都随意发送很大的数据,可能导致其他应用实时性遭到破坏。

总之,方方面面的原因:在传输层封包不能太大。这种限制,往往是以缓冲区大小为单位的。也就是 TCP 协议,会将数据拆分成不超过缓冲区大小的一个个部分。每个部分有一个独特的名词,叫作 TCP 段(TCP Segment)。

在接收数据的时候,一个个 TCP 段又被重组成原来的数据

像这样,数据经过拆分,然后传输,然后在目的地重组,俗称拆包。所以拆包是将数据拆分成多个 TCP 段传输。那么粘包是什么呢?有时候,如果发往一个目的地的多个数据太小了,为了防止多次发送占用资源,TCP 协议有可能将它们合并成一个 TCP 段发送,在目的地再还原成多个数据,这个过程俗称粘包。所以粘包是将多个数据合并成一个 TCP 段发送

Sequence Number 和 Acknowledgement Number

在 TCP 协议的设计当中,数据被拆分成很多个部分,部分增加了协议头。合并成为一个 TCP 段,进行传输。这个过程,我们俗称拆包。这些 TCP 段经过复杂的网络结构,由底层的 IP 协议,负责传输到目的地,然后再进行重组。

这里请你思考一个问题:稳定性要求数据无损地传输,也就是说拆包获得数据,又需要恢复到原来的样子。而在复杂的网络环境当中,即便所有的段是顺序发出的,也不能保证它们顺序到达,因此,发出的每一个 TCP 段都需要有序号。这个序号,就是 Sequence Number(Seq)。

玩转TCP_第7张图片

如上图所示。发送数据的时候,为每一个 TCP 段分配一个自增的 Sequence Number。接收数据的时候,虽然得到的是乱序的 TCP 段,但是可以通过 Seq 进行排序。

但是这样又会产生一个新的问题——接收方如果要回复发送方,也需要这个 Seq。而网络的两个终端,去同步一个自增的序号是非常困难的。因为任何两个网络主体间,时间都不能做到完全同步,又没有公共的存储空间,无法共享数据,更别说实现一个分布式的自增序号了。

其实这个问题的本质就好像两个人在说话一样,我们要确保他们说出去的话,和回答之间的顺序。因为 TCP 是一个双工的协议,两边可能会同时说话。所以聪明的科学家想到了确定一句话的顺序,需要两个值去描述——也就是发送的字节数和接收的字节数。

玩转TCP_第8张图片

我们重新定义一下 Seq(如上图所示),对于任何一个接收方,如果知道了发送者发送某个 TCP 段时,已经发送了多少字节的数据,那么就可以确定发送者发送数据的顺序。

但是这里有一个问题。如果接收方也向发送者发送了数据请求(或者说双方在对话),接收方就不知道发送者发送的数据到底对应哪一条自己发送的数据了。

因此我们还需要另一个数据,就是每个 TCP 段发送时,发送方已经接收了多少数据。用 Acknowledgement Number 表示,下面简写为 ACK。

下图中,终端发送了三条数据,并且接收到四条数据,通过观察,根据接收到的数据中的 Seq 和 ACK,将发送和接收的数据进行排序。

玩转TCP_第9张图片

例如上图中,发送方发送了 100 字节的数据,而接收到的(Seq = 0 和 Seq =100)的两个封包,都是针对发送方(Seq = 0)这个封包的。发送 100 个字节,所以接收到的 ACK 刚好是 100。说明(Seq= 0 和 Seq= 100)这两个封包是针对接收到第 100 个字节数据后,发送回来的。这样就确定了整体的顺序。

滑动窗口和流速控制

TCP 作为一个传输层协议,最核心的能力是传输。传输需要保证可靠性,还需要控制流速,这两个核心能力均由滑动窗口提供。

请求/响应模型

TCP 中每个发送的请求都需要响应。如果一个请求没有收到响应,发送方就会认为这次发送出现了故障,会触发重发。

大体的模型,和下图很像。但是如果完全和下图一样,每一个请求收到响应之后,再发送下一个请求,吞吐量会很低。因为这样的设计,会产生网络的空闲时间,说白了,就是浪费带宽。带宽没有用满,意味着可以同时发送更多的请求,接收更多的响应。

玩转TCP_第10张图片

一种改进的方式,就是让发送方有请求就发送出去,而不是等待响应。通过这样的处理方式,发送的数据连在了一起,响应的数据也连在了一起,吞吐量就提升了。

玩转TCP_第11张图片

但是如果可以同时发送的数据真的非常多呢?比如成百上千个 TCP 段都需要发送,这个时候带宽可能会不足。

滑动窗口(Sliding Window)

那么这时候就需要我们的滑动窗口来实现了,如下图:

玩转TCP_第12张图片

如上图所示:

  • 深绿色代表已经收到 ACK 的段
  • 浅绿色代表发送了,但是没有收到 ACK 的段
  • 白色代表没有发送的段
  • 紫色代表暂时不能发送的段

下面我们重新设计一下不同类型封包的顺序,将已发送的数据放到最左边,发送中的数据放到中间,未发送的数据放到右边。假设我们最多同时发送 5 个封包,也就是窗口大小 = 5。窗口中的数据被同时发送出去,然后等待 ACK。如果一个封包 ACK 到达,我们就将它标记为已接收(深绿色)。

如下图所示,有两个封包的 ACK 到达,因此标记为绿色。

玩转TCP_第13张图片

这个时候滑动窗口可以向右滑动,如下图所示:

玩转TCP_第14张图片

重传

如果发送过程中,部分数据没能收到 ACK 会怎样呢?这就可能发生重传。

如果发生下图这样的情况,段 4 迟迟没有收到 ACK。

玩转TCP_第15张图片

这个时候滑动窗口只能右移一个位置,如下图所示:

玩转TCP_第16张图片

在这个过程中,如果后来段 4 重传成功(接收到 ACK),那么窗口就会继续右移。如果段 4 发送失败,还是没能收到 ACK,那么接收方也会抛弃段 5、段 6、段 7。这样从段 4 开始之后的数据都需要重发。

快速重传

在 TCP 协议中,如果接收方想丢弃某个段,可以选择不发 ACK。发送端超时后,会重发这个 TCP 段。而有时候,接收方希望催促发送方尽快补发某个 TCP 段,这个时候可以使用快速重传能力。

例如段 1、段 2、段 4 到了,但是段 3 没有到。 接收方可以发送多次段 3 的 ACK。如果发送方收到多个段 3 的 ACK,就会重发段 3。这个机制称为快速重传。这和超时重发不同,是一种催促的机制。

为了不让发送方误以为段 3 已经收到了,在快速重传的情况下,接收方即便收到发来的段 4,依然会发段 3 的 ACK(不发段 4 的 ACK),直到发送方把段 3 重传。

思考:窗口大小的单位是?

请你思考另一个问题,窗口大小的单位是多少呢?在上面所有的图片中,窗口大小是 TCP 段的数量。实际操作中,每个 TCP 段的大小不同,限制数量会让接收方的缓冲区不好操作,因此实际操作中窗口大小单位是字节数

思考:滑动窗口是越大越好吗?

引用知乎的回答:

玩转TCP_第17张图片

流速控制

发送、接收窗口的大小可以用来控制 TCP 协议的流速。窗口越大,同时可以发送、接收的数据就越多,支持的吞吐量也就越大。当然,窗口越大,如果数据发生错误,损失也就越大,因为需要重传越多的数据。

总结

为了提高传输速率,TCP 协议选择将多个段同时发送,为了让这些段不至于被接收方拒绝服务,在发送前,双方要协商好发送的速率。但是我们不可能完全确定网速,所以协商的方式,就变成确定窗口大小。

有了窗口,发送方利用滑动窗口算法发送消息;接收方构造缓冲区接收消息,并给发送方 ACK。滑动窗口的实现只需要数组和少量的指针即可,是一个非常高效的算法。

那么,现在你可以尝试来回答本讲关联的面试题目:滑动窗口和流速控制是怎么回事

【解析】滑动窗口是 TCP 协议控制可靠性的核心。发送方将数据拆包,变成多个分组。然后将数据放入一个拥有滑动窗口的数组,依次发出,仍然遵循先入先出(FIFO)的顺序,但是窗口中的分组会一次性发送。窗口中序号最小的分组如果收到 ACK,窗口就会发生滑动;如果最小序号的分组长时间没有收到 ACK,就会触发整个窗口的数据重新发送。

另一方面,在多次传输中,网络的平均延迟往往是相对固定的,这样 TCP 协议可以通过双方协商窗口大小控制流速。

TCP和UDP

TCP 和 UDP 是今天应用最广泛的传输层协议,拥有最核心的垄断地位。TCP 最核心的价值是提供了可靠性,而 UDP 最核心的价值是灵活,你几乎可以用它来做任何事情。例如:HTTP 协议 1.1 和 2.0 都基于 TCP,而到了 HTTP 3.0 就开始用 UDP 了。

UDP 与 TCP的区别

接下来我们说说 UDP 和 TCP 的区别。

  1. 目的差异
    首先,这两个协议的目的不同:TCP 协议的核心目标是提供可靠的网络传输,而 UDP 的目标是在提供报文交换能力基础上尽可能地简化协议轻装上阵。
  1. 可靠性差异
    TCP 核心是要在保证可靠性提供更好的服务。TCP 会有握手的过程,需要建立连接,保证双方同时在线。而且TCP 有时间窗口持续收集无序的数据,直到这一批数据都可以合理地排序组成连续的结果。UDP 并不具备以上这些特性,它只管发送数据封包,而且 UDP 不需要 ACK,这意味着消息发送出去成功与否 UDP 是不管的。
  1. 连接 vs 无连接
    TCP 是一个面向连接的协议(Connection-oriented Protocol),传输数据必须先建立连接。 UDP 是一个无连接协议(Connection-less Protocol),数据随时都可以发送,只提供发送封包(Datagram)的能力。
  1. 流控技术(Flow Control)
    TCP 使用了流控技术来确保发送方不会因为一次发送过多的数据包而使接收方不堪重负。TCP 在发送缓冲区中存储数据,并在接收缓冲区中接收数据。当应用程序准备就绪时,它将从接收缓冲区读取数据。如果接收缓冲区已满,接收方将无法处理更多数据,并将其丢弃。UDP 没有提供类似的能力。
  1. 传输速度
    UDP 协议简化,封包小,没有连接、可靠性检查等,因此单纯从传输速度上讲,UDP 更快。
  1. 场景差异
    TCP 每个数据封包都需要确认,因此天然不适应高速数据传输场景,比如观看视频(流媒体应用)、网络游戏(TCP 有延迟)等。具体来说,如果网络游戏用 TCP,每个封包都需要确认,可能会造成一定的延迟;再比如音、视频传输天生就允许一定的丢包率;Ping 和 DNSLookup,这类型的操作只需要一次简单的请求/返回,不需要建立连接,用 UDP 就足够了。

总结

这次的TCP就学习到这里。

参考

《 计算机网络通关 29 讲》

你可能感兴趣的