一、引言

在这个数字时代,互联网已成为我们生活中不可或缺的一部分。无论是刷社交媒体,观看视频,还是进行在线购物,我们的日常活动几乎都依赖于网络的稳定运行。而在这一切背后,有一个默默无闻但极其重要的协议在默默地确保数据可靠地传输,它就是传输控制协议(TCP)。

想象一下,你正处于一场跨越大洋的国际视频会议中,会议的每一个细节都关乎你即将达成的重要商业合作。在这种情况下,任何一点点的数据丢失或错误都会导致严重的后果。令人惊讶的是,正是 TCP 这种看似简单的协议,保障了数据在如此复杂的网络环境中,能够毫无差错地传递。

本文将深入解剖 TCP 协议,揭示它是如何通过一系列精妙的机制,来实现数据传输的高可靠性。我们将一同探讨那些隐藏在网络深处的技术细节,理解 TCP 如何在不完美的网络世界中,提供近乎完美的数据传输体验。


二、TCP协议段格式

想要理解 TCP 的核心特性,首先大家要理解一张图,TCP 数据包的头部结构图,如下:

传输层重点协议(TCP 协议)深度解剖_重传

首先大家看到这个图都开始蒙了,毕竟很多东西都不知道嘛,但是也有知道的。

  1. 第一个:源/目的 端口,很简单,从哪儿来,到哪儿去?
  2. 第二个:四位首部长度,这个也很简单,TCP 的报头是多长,4bit 表示的意思是 0 - 15,但是这个单位是 4字节,说明一共表示最大可以有60个字节。
  3. 第三个:保留位,这个相当于是 reserved 设计 TCP 的那个大佬,比设计 UDP 的大佬多长了个心眼,多设计了6位,怕以后不够用了,先占个位置,但是现在 TCP 这么多年了,大概率也是用不到了,但是话又说回来了,我可以不用,但是我不能没有!传输层重点协议(TCP 协议)深度解剖_数据_02 所以,这个保留位呢就是给未来留下了可以升级扩展的空间。
  4. 第四个:16位校验和,用于检验首部和数据部分的正确性,接收方根据该值进行验证。
  5. 也到最后一个了:数据,这个更简单了,就是实际传输的数据内容。

其他的东西都是些什么鬼啊???(内心独白)别着急,下面会一一介绍到。


先说关于 TCP 传输的特点:1. 有连接。2. 可靠传输。3. 面向字节流。4.全双工。那我们 TCP 最具有特点地方,就是实现了可靠传输,但是可靠传输是计算机内核实现的,写代码的时候程序猿是感知不到的 (人家就是故意这么设计的,让砸门感知成本低,使用成本也就相对降低了)。实现可靠传输最核心的机制就是确认应答!!!

三、确认应答(实现 TCP 可靠性最核心的机制)

首先确认应答是什么意思呢?举个例子:在很早之前,互联网刚兴起的时候,都是使用短信来发消息,但是短信发出去的消息有时候会丢了,比如说在某一天,我给我的女神发消息,我说:女神女神,我请你吃饭好不好?这时候,如果她看到了就会回一个:好呀好呀。如下图:

传输层重点协议(TCP 协议)深度解剖_数据_03

在这个时候,女神给我回的这个消息在 TCP 中,就是应答报文,而应答报文中,ACK 的值会是1,普通报文里面是 0。

还是上面这个例子,假设我发两条消息,如下:

传输层重点协议(TCP 协议)深度解剖_重传_04

针对上面这个图,那如果是正常收到的消息,应该是先收到 "好啊好啊" ,然后再收到 "滚" ,那我就明白了 "吃饭可以,做女朋友不行!!!", 但是在我们连续发多条数据的时候,这多条数据不一定就一定会先发的这个数据先到,还可能会出现 "后发先至"  这种情况。如下图:

传输层重点协议(TCP 协议)深度解剖_数据_05

如上,出现这种情况,好啊好啊在后面才显示出来,这是我多半会以为 "好啊好啊" 是答应当我女朋友的,那误会就大了,那为什么会出现后发先至这种情况呢?很简单,又是一个小例子:

大家都知道结婚吧?里面就有个接亲的环节,先从新郎家出发,然后去新娘家接她回到新郎家,然后接亲这个环节是有车队的,车队的第一辆车是 "领车",按照理想情况来说,一般是领车先到,然后后面的车俩跟在领车屁股后面依次到达,但是路上会有很多变数会导致车队变换队形了,比如说红绿灯,前面的车先走了,但是后面的车被红灯堵住了走不了,然后等完红绿灯之后,后面的车就发现前面的车已经没影了,这时候后面的车就会各走各的,而且一般来说发生这种情况,后面的车一般都会比前面的车先到达,因为前面的车会表演一些节目,并且速度会开的比较慢,所以就产生了这种后发先至的情况,而我们这种发消息的情况也是类似,因为互联网的初心就是冗余,不怕核弹,所以就会有很多的路径,两个数据报走的路线不一定相同,而且每个节点(路由器/交换机)繁忙程度也不一样,此时这样的转发过程,也会存在差异,就和等红绿灯一样。

那出现这种情况,咋办呢???很简单,我们设置一个编号不就行了。如下:

传输层重点协议(TCP 协议)深度解剖_TCP_06

那这时候我们加上这个编号,就不会产生误会了,我发的第一条信息,编号是 1,这个在 TCP 中叫做 32 位序号,然后她回我的这些消息中的 "针对1",就是 32 位确认序号,当且仅当 ACK 为 1 的时候,这个 32 位确认序号才有效。

由于 TCP 是面向字节流,所以在数据传输中,可不是以 "条" 为传输单位的,是以序列号,如下:

传输层重点协议(TCP 协议)深度解剖_数据_07

这第一条数据中的 1,说明第一条是从 1-1000,这个 1 说明这个数据中第一个字节的序列号是 1,然后一直到 1000 是这个数据的长度,所以可以得到一个公式:首字节的开始序号 + 长度 - 1 = 最后一个字节的序号。在 TCP 报文中,是不会保存 "最后一个序号" 的,而在后面的 IP 协议的报头中,我们是可以知道载荷长度的。而我们确认应答中的数字 1001,有两种理解方式,第一种是:在1001之前的数据我都收到了。第二种:你下一条给我发的消息要从1001开始发。我们确认这个报文是普通报文还是确认应答报文的方法就是看 ACK 的首字节的值,为 1 为应答报文,为 0 为普通报文。

综上所述,确认应答是 TCP 实现可靠传输的最核心的机制!!!

四、超时重传(对实现 TCP 可靠性的有效补充)

那只负责传数据肯定是不行的呀,在数据传输中,有个词叫 "丢包",意思就是,数据在传输的过程中,传着传着数据丢失了,那这时候应该怎么办呢?欸,这时候我们的确认应答(ACK)就起作用了,如果主机 A,长时间收不到主机 B 的ACK,这时候就会重新传一个数据过去,这就是 TCP 中超时重传机制,超时重传又分两种情况:

  1. 第一种:发的消息本身丢了。
  2. 第二种:确认应答返回的 ACK 丢了。

首先分析第一种情况:发的消息本身丢了,如下:

传输层重点协议(TCP 协议)深度解剖_TCP十个特性_08

这种情况就很简单了,如果等的实践很久都等不到应答报文的话,主机 A 直接重发就行了,但是第二种应答报文丢了要麻烦一点。如下:

传输层重点协议(TCP 协议)深度解剖_TCP_09

如果应答报文丢包了,那这时候 A 重新发一份数据的话,B 就收到两份数据了吗?就好比说,我约女神吃饭,发了一条消息,等了很久都没回复,有两种可能,一种是她根本就没收到,另一种是她收到了,但是她回的应答报文丢包了,那我这边我又重新多次的发消息的话,发的同样的消息人家也会烦,这肯定不行.

3.3、去重(根据数据的序号去重)

如上,是发个消息还好,最多也就拉个黑,那如果是充值服务呢?我本来省吃俭用存了个 1 块钱,想要充值一下,结果由于重传的问题,直接给我充 2 块,这不是要我老命吗???

所以这时候主机 B 的 TCP 内核中,给每一个 socket 对象都安排一个内存空间,相当于一个队列,也被称为 "接收缓冲区",收到的数据首先会被放到接收缓冲区里,并且按照序号排列好顺序,然后就会针对这些数据进行去重处理。

并且这个有序排列,也是十分有意义的,后发先至的问题在这里也给解决了,如下图:

传输层重点协议(TCP 协议)深度解剖_TCP_10

有红色格子的就是接收到数据了,那这时候如果 1-1000 不在,返回不了 ACK ,发送端就会重传,只有等 1-1000 到了,才会出这些数据,3001-4000也是类似。

这个队列也就类似于生产者消费者模型,收到数据,接收端的网卡把数据放到对应的 socket 的接收缓冲区(内核)中。引用程序调用 read 方法,就是从这个接收缓冲区消费数据,当数据被 read 走了之后,就可以从队列中删除了。(read 的时候,存在两种模式:第一种是读取到就删除[默认的]。第二种是读取到不删除)。

提问:为什么重传之后,就一定会传输成功呢?

首先,丢包这件事是一个概论性问题,假设一下我们的丢包概论是 10%(这个数据已经非常大了,正常情况下的丢包率是零点几,好一点的甚至存在零点零几),就算第一次丢了,那第二次还丢的概论就是 1% 了呀,如果就是这么背,第二次也丢了,那第三次就是 0.1% 了,重传个几次就肯定过了。那是否会存在重传多次,仍然丢包的情况呢?当然是存在的,那就是当前丢包概率极高的情况,达到了 100% (网络断开了),不管怎么传,都是丢包的。那这时候硬件出现问题了,肯定就不能维持 TCP 的可靠性了。

接下来我要说明的是超时重传中的等待超时时间,那这个时间具体是等多久呢???其实是不必记数值的,因为数值都是可配置、可变的,重点要理解这个策略,首先我们要明白,随着重传的轮次的增加,等待超时重传的时间也会越来越长,正常情况下,第二次重传的概率就有极大的概率重传成功,如果好几次都没成功,说明当前网络本身的丢包率已经极高了,网络怕是遇到了非常严重的故障,此时再进行频繁的重传也是白费力气。但是重传的轮次也不是无限多的,打到一定的次数之后,也就放弃了,此时会尝试 "重置"  TCP 连接,而我们的 TCP 复位报文就派上用场了,那就是 "RST",当 RST 为 1 时,表示这个 TCP 报文是一个复位报文,那肯定就会有人问了,那要是复位报文又丢了怎么办?当然也是会出发重传,重传了几次不成功(说明网络严重故障),复位操作/重置操作 也无法成功进行,最终就只能放弃连接,把保存的对端的信息就删除了。因为这时候想要重新连接已经没有什么意义了,网都断了还连啥???

所以综上所述,超时重传是对于 TCP 实现可靠性的有效补充(也就是为了更可靠)。


五、连接管理(可靠性)

5.1、建立连接 —— 三次握手传输层重点协议(TCP 协议)深度解剖_TCP十个特性_11传输层重点协议(TCP 协议)深度解剖_TCP_12传输层重点协议(TCP 协议)深度解剖_TCP_13

首先我们要明白,为什么要握手?三次握手是为了什么??握两次可不可以???四次呢????带着这些疑问,听我慢慢给你分析。

a)为什么要握手?传输层重点协议(TCP 协议)深度解剖_重传_14

先解决第一个问题:为什么要三次握手?

握手的这个词在我们国人的认知中是一种打招呼的方式,在计算机中也是一样,这个握手就是为了给对方打个招呼,我要和你建立连接!!!

b)三次握手是为了什么?传输层重点协议(TCP 协议)深度解剖_TCP_15

然后再来看第二个问题:三次握手是为了什么??

很简单,"投石问路"。举个生活中的小例子,地铁大家都知道吧?首先清晨的时候,地铁站那时候还没开,但是会先跑一趟空车,因为地铁的轨道其实是有个叫第三轨的东西,那玩意儿是有电的,地铁电能就是从这儿获取的,这种系统很常见,有好处也有坏处,坏处就是以为第三轨是位于地面上的,容易收到杂物和积水的影响,需要定期维护,这时候地铁跑一趟空车其实就是为了验证轨道是否畅通无阻,不然到时候拉着一车的人,出了问题,停在轨道中间不太好处理。

三次握手也是为了解决这个问题,先让这个数据报跑一次 "空车",辨别是不是 "打招呼" 的数据报,就是我们 "SYN" 这个标志位该管理的事情了,如果这个标志位为 1,就证明这是一个 "握手" 数据报,不传输数据。三次握手的交互过程图如下:

传输层重点协议(TCP 协议)深度解剖_重传_16

欸,肯定有人会说,你这不是四次吗?哪里三次了?其实我们的第二次应答报文和请求建立连接报文可以合并成一条报文发过去,如下:

传输层重点协议(TCP 协议)深度解剖_TCP十个特性_17

这就是我们 TCP 三次握手的过程了。

c)为什么非要三次?传输层重点协议(TCP 协议)深度解剖_数据_18

第三第四个问题就放在一起讲了,为什么握手一定是三次?

想弄明白这个问题,就得先明白三次握手是怎么个握法,为什么需要三次?还是举个例子,我约女神一起开黑,肯定要确定对方的麦克风和耳机是不是都是正常的,如下图:

传输层重点协议(TCP 协议)深度解剖_TCP十个特性_19

首先逐步分析问题:

  1. 第一条报文:女神女神,听得到吗?

首先我说出这句话的时候,我是不知道我的耳麦是否是都是正常的,但是女神听到我说话,在女神的认知中,我的麦克风是没有问题的,这时候她回了我一个应答报文:听得到,并且反问我你呢?这个反问就相当于是 SYN。

  1. 第二条报文:听得到,你呢?

女神这时候回我了,就说明女神是听到我说话了的,说明我的麦克风还有女神的耳机是没有问题,又从我能听到她回我可推断出我的耳机和女神的麦克风也没有问题,当然,这只是站在我这边的角度。

站在女神从的角度,因为只听到了我说话,也就仅仅只能推断出我的麦克风和女神自己的耳机没问题。

  1. 第三条报文:OK,开黑走起!!!

这句回应就是给女神说明我听到你说的话了,女神也就知道,她的麦克风和我的耳机是没问题的。

分析完这些,再说两次行不行?答案是肯定不行的,如果说少了最后一次,也就仅仅只是我这边知道双方的耳麦都没问题,但是女神不知道呀。然后再说四次,答案是可以,但是没必要,四次就太多了,明明三次就能解决的事,虽然我知道你很想跟女神多说几句话传输层重点协议(TCP 协议)深度解剖_重传_20,但是我们这说的是网络编程,就没这个必要了。

注意:三次握手的第一次 SYN 一定是客户端先发起的,然后三次握手对应的代码关系如下:

传输层重点协议(TCP 协议)深度解剖_TCP_21

这时候就得拿出我们之前写的客户端的代码了,在 new Socket 这个对象的时候,new 操作完成之后,三次握手也就完成了,这是计算机内核完成的工作,应用程序这里无法干预,同时,服务器针对这三次握手的配合,也是不需要涉及到任何应用层代码的,知道你这个进程绑定了对应的 TCP 端口,就可以在内核中自动配合完成三次握手,无论你服务器代码怎么写都是这样。

传输层重点协议(TCP 协议)深度解剖_TCP十个特性_22

三次握手完成之后,形成了 "连接",这段代码是拿到缓冲区队列中里队首的首元素,进一步获取到其中的 socket 对象,来和对端进行通信。这段代码并不会参与三次握手的过程。

5.2、断开连接 —— 四次挥手传输层重点协议(TCP 协议)深度解剖_重传_23传输层重点协议(TCP 协议)深度解剖_重传_24传输层重点协议(TCP 协议)深度解剖_数据_25

建立连接说完了,得说说如何断开连接了,我们断开连接,也是要两台机器相互协商好的,标志位中 "FIN" 代表的就是结束报文段。如果为 1 ,就代表这个报文时结束报文段,四次挥手过程图如下:

传输层重点协议(TCP 协议)深度解剖_数据_26

在客户端收到服务器发过来的 FIN 之后(会等待一小会儿,具体原因后面说),就会把服务器的服务器的信息给删除,当服务器收到客户端返回的 ACK 之后,也会把客户端的信息给删除。

那问题来了,为什么非要把服务器的 ACK 和 FIN 分两次发呢?不能也和三次挥手一样合并成一次吗?

a)非得四次的原因

首先我们来看一张图:

传输层重点协议(TCP 协议)深度解剖_TCP_27

首先假设左边是客户端,右边是服务器,在客户端发送 FIN 请求的时候,服务器这边会进入一个 CLOSE_WAIT 的一个阻塞状态并返回 ACK,然后我们是要根据应用的代码来进入 LAST_ACK 这个状态的,具体是什么代码呢?如下:

传输层重点协议(TCP 协议)深度解剖_数据_28



传输层重点协议(TCP 协议)深度解剖_TCP十个特性_29

是不是就只能是四步了呀?

b)先请求终止端需要等待的时间

那好,解决了上面这个问题,那我详细来说说 "会等待一小会儿" 这个等待是为什么以及一小会儿的这个等待时间。

先说等待的原因吧,举个例子:假设我送女神回家,但是女神比较胆小,如果说家里没人的话不敢一个人在家过夜,这时候就得到我家去过,那我们吃好饭,我是不是得送她回去呀?直接送到家门口又不太好,如果到时候人家父母在,我又比较内向,不好处理,所以这时候我就只送到楼下,那么问题来了,我能把女神送到楼下就直接走吗?那肯定不行,如果说她家里有人还好,如果说没人,我不得把她带到我家里去过夜吗?所以这时候,我是必然要在楼下等她的。

传输层重点协议(TCP 协议)深度解剖_重传_30

还是这张图,客户端也是一样,你给对端回了个 ACK,难道就能直接删除对端信息了吗?那肯定不行,如果说这时候 ACK 丢包了呢??是不是服务器会触发超时重传的机制呀?所以我们得等一等,等一段时间之后,确认没有重传,才能删除。

还有另外几种情况

  1. LAST_ACK 发送过来的 FIN 丢包,这个丢了就丢了,无所谓,大不了重传就行了,一直重传一直丢那说明网络问题,就会触发申请重新连接 RST 报文,那要是一直不成功就会自动删除,客户端这边也是一样,如果说长时间没有收到对端的 FIN 也会自动删除对端信息。
  2. ACK 一直丢,这更好解决了,你 ACK 都一直丢了,重传的报文也肯定一直丢,那一直收不到对方重传的 FIN,客户端这边就说明对端以及收到自己发送的 ACK 了,服务器那边由于一直丢包会启动 RST,重新连接一直不成功也会自动删除对端信息。
  3. 更极端一点的情况,代码出 BUG 了,或者 close 就忘写了,站在客户端的角度,迟迟收不到对方的 FIN,也会进行等待,如果等了很久都等不到,此时也会单方面放弃连接,直接删除对端信息

所以,释放资源对端的资源,双方都能顺利进行,固然是最好的,如果条件不允许,也不影响单方面释放。

c)具体的等待时间

还是送女神回家那个例子,那我具体等多久呢?很简单,假设从楼下到女神家里的时间是 n,那我等 2n 不就好了吗。所以,等待时间就是网络上任意两点之间传输的最大时间 * 2,我们把这个时间定义成 MSL,超时重传时间必然是 <= MSL 的,因为超时重传时间仅仅只是和这一条网线中,相邻两点传输的时间有关,而我的 MSL 是任意两点,说明是最长时间的两点,在此基础上还要乘以二,这个 MSL 也是程序猿拍脑门想出来的,不需要刻意去记忆。


六、滑动窗口(TCP 提高效率的有效方法)

6.1、认识滑动窗口

传输层重点协议(TCP 协议)深度解剖_TCP_31

首先看第一张图,这一看就是老实人嘛,在这么多数据的情况下,还一个一个发,只有收到了 ACK,才敢发后面的数据,这么做固然安全,可是大部分的时间是不是都在等待 ACK 呢?再来看下面这张图:

传输层重点协议(TCP 协议)深度解剖_重传_32

欸,这张图就聪明多了,选择多条数据一起发,这样子多条数据就只需要等待一次 ACK 了(在不考虑丢包和后发先至的情况下),然后拿到一个 ACK 需要发送的数据就往后挪一个,这么看可能不太能理解为什么把这个叫滑动窗口,看下图就知道了:

传输层重点协议(TCP 协议)深度解剖_TCP十个特性_33

这么看是不是就明显多了呢?两个窗口慢慢向后滑动。

6.2、丢包问题与解决方案(快速重传)

首先丢包问题分两种,一种是数据包丢了,一种是 ACK 丢了,柿子先挑软的捏,我们先讨论 ACK 丢了的情况。如下图:

传输层重点协议(TCP 协议)深度解剖_数据_34

这种情况不要紧,因为有后面的 ACK 补上,我们前面说到 ACK 的确认需要就是说明这个序号之前的数据我都收到了,假设是 1-1000 的 ACK 丢了,那我后面不是还有 1001-2000 数据包的 ACK 吗,这时候主机 A 看到后面的 ACK 都来了,那就说明之前的数据都收,不需要重传。

紧接着我们来讨论数据包丢了的情况,还是如下图:

传输层重点协议(TCP 协议)深度解剖_TCP十个特性_35

如上图,主机 A 传过去的 1001-2000 数据包丢失了,那我们主机 B 的确认应答里面,就会一直有 1001,提醒主机 A 得发送1001-2000 的数据,但是后面发送的数据也会保存在缓冲区中,如果主机 A 多次收到的确认应答都是这样的话,那就会重传 1001-2000 这部分数据,并且因为已经收到了后面的数据,所以应答报文发送的是已收到数据的最后一个序号 + 1,也就是 7001。也是提醒主机 A,7001之前的数据都已经完整收到了,发送 7001 之后的数据就行了,这种重传我们就叫做快速重传。

滑动窗口一般是需要传输数据规模较大的时候使用,如果说你传输的数据较少,断断续续的那种,还是采用正常的超时重传来做,因为假设你一分钟发个三四条消息,肯定不能等这几条消息,等了一分钟再发出去嘛。


七、流量控制(确保可靠性,间接提升传输效率)

上面介绍了 TCP 提高效率的一种方法,滑动窗口。那话又说回来了,我们这个窗口能无限的大吗???肯定是不行的,如果说窗口太大,就可能使接收方处理不过来,或者是传输的中间链路处理不过来,这样就会出现丢包的情况,丢包之后就得重传了,那这样还适得其反了,反而影响效率。

所以,那我们怎么设置这个窗口大小呢?其实这个窗口的大小呀,是实时变化的,根据接收端剩余缓冲区的大小,来告诉发送端具体这个窗口要设置的多大,那这个剩余的缓冲区大小是什么呢?举个例子:一个水池,左边进水口右边出水口,如下图:

传输层重点协议(TCP 协议)深度解剖_数据_36

首先是一个小学生都会的数学题,当进水口的进水量大于出水口时,正常情况下,此时的水位一定是升高的,然后水池剩余的空间大小一定是减少的,数据传输中也是,当窗口足够大时,接收缓冲区的剩余空间大小也是减少的,理解了这个,再来看下图:

传输层重点协议(TCP 协议)深度解剖_数据_37

在返回应答报文 1001   3000 的时候,这个 3000 就是存储在16位窗口中跟着 ACK 一起返回的,所以这个字段只是对 ACK 报文才有意义,目的就是告诉主机 A 我还能存 3000 大小数据,然后主机 A 根据这个 16位窗口大小来判断传几个数据过去,这不?紧接着就传了三个数据,然后三个数据吗,满了,16位窗口大小传了个 0,主机 A 就知道接收端的接收缓冲区已经满了,不能再传了,但是等了一会儿,主机 A 肯定也不能干等着呀,虽然说如果主机 B 的接收缓冲区有空位的话会传一个窗口更新通知过来,但是要是这时候网络出现故障,主机 B 发送的窗口更新通知丢包了,主机 B 断开连接了怎么办?所以等了一会儿之后,这个时间过了重发的超时的时间,主机 A 就会发送一个窗口探测包,主要是问问主机 B 接收缓冲区剩余空间的大小,这时候主机 B 就会返回具体情况。

但是我们这个 16 位窗口大小并不是仅仅只有 64KB的哈,请看下面的选项这个字段,选项中有一个选项,就是窗口大小的扩展因子,公式:(实际的窗口大小 = 16位窗口大小 左移 扩展因子)的大小,而我们都知道,左移一位就是乘以 2,这是指数增长的,此时能表示的窗口大小就非常的大了,但是确定窗口大小并不是全看流量控制的,还有下面要介绍的拥塞控制。

综上所述,流量控制的主要目的是防止发送方发送的数据超过接收方的处理能力,从而避免网络拥塞和数据丢失。虽然流量控制本身并不直接提高传输效率,但它通过确保数据在传输过程中的稳定性和可靠性,从而间接地提高了整体传输效率。所以,流量控制也可以视为是保证可靠性的机制


八、拥塞控制(可靠性)

拥塞这个词,看上去就给人很拥堵的感觉,其实这和我即将要说的 "拥塞控制" 差不多,就是根据网络路径上的拥塞情况,根据拥塞情况,再结合流量控制的缓冲区大小,相互配合着控制窗口大小的。

知道了拥塞控制基本上是怎么回事之后,那具体是根据拥塞情况,怎么个控制法呢?举个例子,假设两台主机之间有很多交换机、和交换机类似的这些东西。如下图:

传输层重点协议(TCP 协议)深度解剖_数据_38

那我们一眼看过去,你觉得如果此时 主机A 给 主机B 发送消息的话,那你觉得是根据 "很通畅" 的那个交换机来决定发送效率还是根据那个 "很拥塞" 的那个交换机来决定发送效率呢?很简单嘛,有个词叫木桶效应,最低的 "挡板" 决定 "盛水" 高度,这里也是一样,你要是发块了,其他的交换机肯定处理的过来,但是最慢的那个呢?是不是就会堵着了?然后会丢包,这样子又得重传,更浪费时间。

大家又会说了,那虽然说这次的传输是这个交换机效率最低,但是因为一个交换机要给很多个端口服务啊,下一次说不定就是另外一个了,并且每次传输的路线又不一定相同,所以并不好直接测试交换机的传输速率,这怎么好控制具体得多慢呢?

欸,我们发明 TCP 的那个大佬也肯定想到了这点,可以通过实践的方式来解决,直接看看整条路径的时间,就是第一次先给个很小的窗口先试试水,然后先看看时间,后面再慢慢调整(我们把前面这种小窗口先是水称作慢启动),前面的增长一般是指数增长,就是直接乘以 2,这样子增长那可太快了,窗口到后期岂不是无限大了?那肯定不行,这样的话,又会产生拥塞,然后丢包,因此为了避免这种情况的发生,这个大佬设置了一个阈值,达到这个阈值之后就会线性增长(加法增长)了,就是每次加 n。这样子到后面也会网络拥塞,到达网络拥塞之后,记录一下这个数值,然后除以二,下次阈值就是这个,最后,这个拥塞窗口又会调整成最开始的小窗口,循环往复,如下图:

传输层重点协议(TCP 协议)深度解剖_TCP_39

看到这里,相信很多人都会有这样一个疑问:为什么流量控制和拥塞控制时保证可靠性的呢?这明明就是提高传输效率的呀。我当时也是这么想的,根本想不明白,后面我是这么理解的:首先这两个控制都是为了降低丢包率,丢包率高是不是就不可靠了呢?所以从根本上来说还是保证可靠性,然后间接的提高传输效率。

最后我们可得:滑动窗口的大小 = min(流量控制窗口,拥塞窗口); 也是木桶效应。


九、延迟应答(提高效率)

紧接着,迎面向我们走来的是第二种提高 TCP 效率的办法(bushi),延迟应答又是怎么回事呢?很简单,延迟就行了,那传输中的应答,能联想到什么呢?那肯定是是我们的应答报文啊。那如何延迟呢?这就要请出我们的老伙伴,例子先生了,假设你现在是一个大学生,作业一般都是交到学习通上,但是你现在差了10次作业都没交,老师这时候发信息给你:XXX同学啊,你这作业10次都没交,怎么回事啊?现在有两种解决方案:

第一种:立马回消息说 “哎呀,老师不好意思,马上写。”

第二种:先假装没看到,然后疯狂补作业,到晚上就说 “老师啊,我就还差一两次了。”

我相信大多数人都会选择第二种吧,第一种压力还是有点大的哈。

所以这个应答报文也聪明呀,直接回去的话那到时候的窗口不就比较小了嘛,所以可以拖延一点时间,让缓冲区空一点,然后窗口就大一点,如下图:

传输层重点协议(TCP 协议)深度解剖_重传_40

这是一个接收缓冲区,已经用了 7KB 了,还剩 3KB,如果这时候 ACK 立马回去,后面的窗口大小只能是 3KB 了,这时 ACK 就会拖延一点时间,等到还剩 5KB 的时候回去,窗口是不是就变大了呢?那肯定会有人说,欸,你这也妹提高效率啊,不还是拖延迟间了吗?我们要清楚,如果不拖延迟间,虽然说能多跑几次 ACK 可是每次传输,效率都是很慢的啊,所以还是这样等一等效率更高一点。:

那么问题来了,所有的包都可以延迟应答吗?那必然不是

  • 数量限制(滑动窗口):每隔N个包就应答一次
  • 时间限制(非滑动窗口):超过最大延迟时间就应答一次

具体的数量和超时时间,根据操作系统的不同也是有差异的。

传输层重点协议(TCP 协议)深度解剖_TCP_41


十、捎带应答(提高效率)

还有 TCP 最后一种提高效率的办法了,那就是 "捎带应答",这个词也很好理解,应答的时候,捎/带 点东西。这个捎带应答也是一样,如下图:

传输层重点协议(TCP 协议)深度解剖_TCP_42

启动捎带应答的话,是不是 ACK 就可以和 响应 合并到一起了呀?这是不是非常的像我们的三次握手呢?其实三次握手也是这个原理,因为延迟应答和捎带应答的功劳,大大的提高了传输效率。

数据报从两个合并成一个,效率会明显提示主要是因为每次传输数据都是要封装分用的。

能合并的原因:一方面是实际可以是同时的,另一方面 ACK 响应报文本身是不需要携带数据载荷的,一个响应报文能用到的地方也就 "ACK 标志位、窗口大小、确认序号",其他的地方空着也是空着,完全可以利用起来,让这个数据报又携带载荷数据又带有 ACK 的基本信息。

大家要注意哈,即使 TCP 增加了这么多提高效率的办法,但是还是比不过 UDP 的。主要还是因为这个 "可靠传输" 要实现可靠太占用时间资源了。


十一、面向字节流 -> 粘包问题(注意事项)

ab们写代码的时候,已经深刻的认识到了什么是面向字节流,但是在这样面向字节流的情况下,会产生一些其他问题,举个例子:发送方这边一次性发了很多个应用层数据报,但是接受的时候,应该如何区分呢?从哪里到哪里是一个完整的应用层数据报呢?如果每设计好,接收方就很难区分出来,甚至会产生bug!如下图:

传输层重点协议(TCP 协议)深度解剖_TCP十个特性_43

假设我发了三个数据报过去,下面这是接收缓冲区:

传输层重点协议(TCP 协议)深度解剖_数据_44

那你接收方能接收到这种数据吗?肯定不行啊,read 可能是一次读一个字节或者一次读若干个字节的,它是没有办法一次性读 "应用层数据报" 的。那怎么办?很好解决,此处最争取的做法是合理的设计应用层协议,因为传输层这边已经无解了,你没办法控制它一次读取一个包。

那设计怎样的应用层协议呢?两种解决方案:

  1. 应用层协议中,引入分隔符,区分包之间的边界。
  2. 应用层协议中,引入 "包长度",区分包之间的边界。

先说第一种,添加分隔符。这在我们写代码的时候就已经写了的呀,我们打包成 PrintWriter 这个数据报,然后发送这个数据报的时候,用的是不是 println 这个方法呀,目的就是为了使发送过去的包携带 "\n",此时 "\n" 就是这个分隔符。那么接收缓冲区会变成下面这样

传输层重点协议(TCP 协议)深度解剖_重传_45

那我们读取的时候就知道了,读到 "\n" 就是一个数据报了。

再说第二种,引入包长度,也很简单,每次传输数据的时候,规定前面两个字节是这个数据报的长度不就行了?接收缓冲区如下

传输层重点协议(TCP 协议)深度解剖_重传_46

读取的时候也非常好读取,根据前面的数字,读到长度为这么长就是一个数据报,然后紧接着读取第二个数据报的数字,循环往复。


十二、异常情况处理 -> 心跳包(异常情况)

首先异常情况分为两种情况,硬件异常和软件异常,软件异常就是应用程序崩溃呀这些,硬件异常又是有机器一场和网络异常。下面细说。

12.1、进程崩溃

这个很简单,进程崩溃了的话就会导致 PCB 没了,那 PCB 都没了文件描述符表肯定也释放了,这就相当于调用了 close 方法,崩溃的这一方就会发出 FIN,进一步触发四次挥手,就和正常退出没区别。

12.2、主机关机(正常关机)

如果是正常关机,电脑肯定是会有一段关机时间的嘛,这时候电脑就会关闭所有进程(强制关闭),和进程崩溃是一样的解决方法,但是不一定能挥完,但是没关系,就算没挥完,FIN 肯定是能发出去的,就算收不到对面的 FIN,也不影响,因为不会影响对面单方面删除对端的信息。

12.3、主机掉电(非正常关机)

主机掉电就是假设你电脑突然就没电了,直接关机,处理这个异常得分为两种情况,发送端关机还是接收端关机。

先说接收方掉电吧,这种情况好办,发送方发的消息,接收不到对端的 ACK 了,那就会触发超时重传,重传几次不成功就会触发复位报文(RST 字段为 1),尝试重连,重连操作仍然失败,也就会单方面释放连接了,

再说说第二种,发送方掉电。假设 主机A 给 主机B 发消息,发着发着,A 就掉电了,B 这时候收不到消息,但是 B 也不知道这是 A 掉电了还是业务需求(现在没数据,等下就有数据了),所以这时候 B 就会周期性发送一个不携带任何业务数据载荷的 TCP 数据报,发这个包的目的就是为了触发 ACK,确认一下 A 是否能正常工作/确认网络是否通畅,如果 A 不能返回 ACK,那多半是挂了,这个探测对端是否存在的数据报我们称为 "心跳包",有点像我们流量控制里面的窗口探测报文哈,其实两个的本质上是一样的。

虽然 TCP 里面有心跳包的支持了,但是还是不够的,往往还需要在应用层应用程序中重新实现一个心跳包,一位内 TCP 的心跳包周期太长了,是分钟级别的,而现在我们是高并发,分钟肯定是不够的,需要实现秒级别甚至毫秒级的心跳包。

12.4、网络断开

网络断开和主机掉电一样的,如果是接收端断开就是主机掉电的第一种情况,发送端断开就是主机掉电的第二种情况,在这里就不多说了。


十三、总结

通过本文对 TCP 协议各个方面的深入分析,我们对其在网络通信中的重要作用有了全面的认识。TCP 协议不仅实现了可靠的数据传输,还通过多种机制提高了传输效率和可靠性。

  1. TCP协议段格式:理解了 TCP 报头中的各个字段及其功能,从而掌握了 TCP 数据包的基本构成。
  2. 确认应答(可靠性):通过确认应答机制,TCP 确保了数据传输的可靠性和顺序性,保障了数据的完整性。
  3. 超时重传(可靠性):当数据丢失或损坏时,TCP 通过超时重传机制重新发送数据包,确保数据的最终传递。
  4. 连接管理(可靠性):详细分析了 TCP 连接的建立(三次握手)和断开(四次挥手)过程,解释了这些机制如何保证连接的稳定性和可靠性。
  5. 滑动窗口(效率):滑动窗口机制提高了 TCP 的传输效率,通过控制数据流量避免网络拥塞。
  6. 流量控制(可靠性):TCP 通过流量控制机制,确保发送方和接收方之间的数据传输速率匹配,避免网络拥堵。
  7. 拥塞控制(可靠性):TCP 采用多种拥塞控制算法(如慢启动、拥塞避免)来动态调整数据传输速率,优化网络资源使用。
  8. 延迟应答(效率):通过延迟应答机制,TCP 减少了网络负载,提高了传输效率。
  9. 捎带应答(效率):捎带应答机制进一步优化了数据传输效率,通过在数据段中附带确认信息,减少了纯确认段的发送次数。
  10. 面向字节流(注意事项):TCP 通过粘包和拆包技术,实现了面向字节流的数据传输,确保数据的完整性和顺序性。
  11. 异常情况处理(异常情况):在异常情况(如进程崩溃、主机关闭、网络断开)下,TCP 通过多种机制保障数据的可靠传输。

十四、结语

TCP 协议作为网络通信的核心协议,通过其精妙的设计和完善的机制,成功地实现了可靠、高效的数据传输。本文对 TCP 协议的各个方面进行了详细的解析,希望读者能够通过本文的学习,对 TCP 协议有一个全面而深入的理解。

在未来的网络通信和应用开发中,希望大家能够充分利用 TCP 协议的各种机制,实现更加高效、可靠的数据传输。TCP 协议的成功经验也为我们在其他领域的研究和开发提供了宝贵的借鉴。感谢大家的阅读,希望本文对您的学习和工作有所帮助。