TCP(Transimission Control Protocol)是一种面向连接、可靠的字节流通信协议,位于OSI参考模型的传输层中,具备顺序控制、重发控制、流量控制和拥塞控制等众多功能,保证数据能够安全抵达目的地。

接下来简单了解一下用TCP进行数据传输的通信过程。首先通过三次握手建立连接;然后把发送窗口调整到合适的大小,既能避免网络拥塞,也能提高传输效率;在传输过程中,发出去的每个包都会得到对面的确认,当运送的数据包丢失时,可以执行超时重发,当数据包乱序时(有些数据包先送达目的地,有些后到),通过数据包中的序号可以按顺序排列,同时也能丢弃重复的包;再根据端口号将数据准确传送至通信中的应用程序,端口号相当于程序地址;等到所有数据安全到达后,执行四次挥手断开连接,本次传输完成。

连接管理

三次握手

客户端会先经历三次握手,然后才能建立连接,具体过程如下:

img

四次挥手

当要断开连接时,通信两端就会进行四次挥手的操作。由于连接是双向的,所以客户端和服务器端都要发送FIN标志位的包,才算彻底断开了连接,具体过程如下:

img

为什么需要 TIME_WAIT 状态?

  • 原因一:防止历史连接中的数据,被后面相同四元组的连接错误的接收

  • 原因二:保证被动关闭连接的一方,能被正确地关闭

确认应答

在TCP传输的过程中,发出去的每个包都会得到对面的确认,借助数据包的几个字段就能又快又准地通知对方发送的包已到达,再结合延迟确认、nagle算法等技术就能实现一套高效的应答机制。

1.字段

TCP中的每个数据包都包含三个字段:

  • Seq:每个包的序号,用来排列乱序的包

  • Len:数据的长度,不包括TCP头信息

  • Ack:确认号,用于确认已经收到的字节

2.延迟确认

延迟确认就是在一段时间内如果没有数据发送,就将几个确认信息合并成一个包,再一起确认。TCP采用延迟确认的目的是降低网络负担,提升传输效率

3.Nagle算法

Nagle算法是指在发出的数据没有得到确认之前,又有几块小数据要发送,就把它们合并成一个包,再一起发送。

延迟确认和nagle算法都能降低网络负担,提升传输效率,但如果将两者结合使用,却会降低性能。当启用nagle算法的客户端发出一个小的数据包后,启用延迟确认的服务器会接收并等待下一个包的到达。而客户端在未接收到第一个数据包的确认之前,不会再次发送,两端都在等待对方,这反而增加了延迟,降低了传输效率。

重传机制

TCP是一种可靠地通信协议,因此如果发送方通过一些技术手段(如超时重传、快速重传等)确认到某些数据包已经丢失了,那么就会再次发送这些丢失的包。

1.超时重传

2.快速重传

滑动窗口

数据包所能携带的最大数据量称为MSS(Maximum Segment Size)。当TCP传送大数据的时候,会先将其分割为多个MSS再进行传送。MSS是发送数据包的单位,重发时也是以MSS为单位。在建立连接时,两端会告诉对方自己所能接收的MSS的大小,然后再选择一个较小的值投入使用。

1.发送窗口

2.拥塞窗口

窗口大小由哪一方决定?

TCP头里有一个字段叫做window,也就是窗口大小。

这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。所以,通常窗口的大小是由接收方的决定的。发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。

发送方的滑动窗口

我们先来看看发送方的窗口,下图就是发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送窗口,紫色方框是可用窗口:

img

  • #1 是已发送并收到 ACK确认的数据:1~31 字节

  • #2 是已发送但未收到 ACK确认的数据:32~45 字节

  • #3 是未发送但总大小在接收方处理范围内(接收方还有空间):46~51字节

  • #4 是未发送但总大小超过接收方处理范围(接收方没有空间):52字节以后

在下图,当发送方把数据「全部」都一下发送出去后,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了。

img

可用窗口耗尽

在下图,当收到之前发送的数据 3236 字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 5256 字节又变成了可用窗口,那么后续也就可以发送
52~56 这 5 个字节的数据了。

img

程序是如何表示发送方的四个部分的?

TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。

img

  • SND.WND:表示发送窗口的大小(大小是由接收方指定的);

  • SND.UNA:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。

  • SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。

  • 指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量,就可以指向 #4 的第一个字节了。

那么可用窗口大小的计算就可以是:

可用窗口大 = SND.WND -(SND.NXT - SND.UNA)

接收方的滑动窗口

接下来我们看看接收方的窗口,接收窗口相对简单一些,根据处理的情况划分成三个部分:

  • #1 + #2 是已成功接收并确认的数据(等待应用进程读取);

  • #3 是未收到数据但可以接收的数据;

  • #4 未收到数据并不可以接收的数据;

img

其中三个接收部分,使用两个指针进行划分:

  • RCV.WND:表示接收窗口的大小,它会通告给发送方。

  • RCV.NXT:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。

  • 指向 #4 的第一个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移量,就可以指向 #4 的第一个字节了。

接收窗口和发送窗口大小是相等的吗?

并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。

因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows
字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。

流量控制

发送方不能无脑地发数据给接收方,要考虑接收方处理能力。

如果一直无脑的发数据给对方,但对方处理不过来,那么就会触发重发机制,从而导致网络流量无端地浪费。

为了解决这种现象发生,TCP提供一种机制可以让「发送方」根据「接收方」的实际接受能力控制发送的数据量,这就是所谓的流量控制。

下面举个栗子,为了简单起见,假设以下场景:

  • 客户端是接收方,服务端是发送方

  • 假设接收窗口和发送窗口相同,都为 200

  • 假设两个设备在整个传输过程中都保持相同的窗口大小,不受外界影响

img

根据上图的流量控制,说明下每个过程:

  1. 客户端向服务端发送请求数据报文。这里要说明下,本次例子是把服务端作为发送方,所以没有画出服务端的接收窗口。

  2. 服务端收到请求报文后,发送确认报文和 80 字节的数据,于是可用窗口 Usable 减少为 120 字节,同时 SND.NXT 指针也向右偏移 80 字节后,指向 321,这意味着下次发送数据的时候,序列号是 321。

  3. 客户端收到 80 字节数据后,于是接收窗口往右移动 80 字节,RCV.NXT 也就指向 321,这意味着客户端期望的下一个报文的序列号是 321,接着发送确认报文给服务端。

  4. 服务端再次发送了 120 字节数据,于是可用窗口耗尽为 0,服务端无法在继续发送数据。

  5. 客户端收到 120 字节的数据后,于是接收窗口往右移动 120 字节,RCV.NXT 也就指向 441,接着发送确认报文给服务端。

  6. 服务端收到对 80 字节数据的确认报文后,SND.UNA 指针往右偏移后指向 321,于是可用窗口 Usable 增大到 80。

  7. 服务端收到对 120 字节数据的确认报文后,SND.UNA 指针往右偏移后指向 441,于是可用窗口 Usable 增大到 200。

  8. 服务端可以继续发送了,于是发送了 160 字节的数据后,SND.NXT 指向 601,于是可用窗口 Usable 减少到 40。

  9. 客户端收到 160 字节后,接收窗口往右移动了 160 字节,RCV.NXT 也就是指向了 601,接着发送确认报文给服务端。

  10. 服务端收到对 160 字节数据的确认报文后,发送窗口往右移动了 160 字节,于是 SND.UNA 指针偏移了 160 后指向 601,可用窗口 Usable 也就增大至了 200。

拥塞控制

为什么需要拥塞控制?不是已经有流量控制了吗?

前面的流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。

一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。

在网络出现拥堵时,如果继续发送大量的数据包,可能会导致数据包时延、丢失等。这时TCP就会重传数据,但是一重传数据就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大。。。

所以,TCP 不能忽略网络上发生的事,它被设计成一个无私的协议,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。

于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。

为了在「发送方」调节所要发送数据的量,定义了一个叫做「拥塞窗口」的概念。

什么是拥塞窗口?和发送窗口有什么关系?

拥塞窗口 cwnd是发送方维护的一个 的状态变量,它会根据网络的拥塞程度动态变化的

我们在前面提到过发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。

拥塞窗口 cwnd 变化的规则:

  • 只要网络中没有出现拥塞,cwnd 就会增大;

  • 但网络中出现了拥塞,cwnd 就减少;

如何知道当前网络中出现了拥塞?

其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了用拥塞。

拥塞控制有哪些控制算法?

拥塞控制主要是四个算法:

  • 慢启动

  • 拥塞避免

  • 拥塞发生

  • 快速恢复